From f10e278d7413392e18c4b1d4e20ef32b8329593c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 7 Sep 2019 12:50:59 +0530 Subject: [PATCH] PDF Output: Fix incorrect rendering when the same font is used at multiple sizes in the document Chromium produces width arrays in the font descriptors that contain different values for the same glyph in different font subsets, so rather than merging the arrays, use the actual width values from the font. As per the PDF spec the values in these arrays must match the underlying font anyway. --- src/calibre/ebooks/pdf/html_writer.py | 32 +++++++++++-- src/calibre/utils/fonts/sfnt/container.py | 37 +++++++-------- src/calibre/utils/fonts/sfnt/head.py | 58 ++++++++++++++++++++--- src/calibre/utils/fonts/sfnt/loca.py | 12 +++-- src/calibre/utils/fonts/sfnt/merge.py | 22 ++++++++- 5 files changed, 126 insertions(+), 35 deletions(-) diff --git a/src/calibre/ebooks/pdf/html_writer.py b/src/calibre/ebooks/pdf/html_writer.py index 8552a89286..761571a724 100644 --- a/src/calibre/ebooks/pdf/html_writer.py +++ b/src/calibre/ebooks/pdf/html_writer.py @@ -657,6 +657,22 @@ class Range(object): return len(self.widths) == 1 +def all_glyph_ids_in_w_arrays(arrays): + ans = set() + for w in arrays: + i = 0 + while i + 1 < len(w): + elem = w[i] + next_elem = w[i+1] + if isinstance(next_elem, list): + ans |= set(range(elem, elem + len(next_elem))) + i += 2 + else: + ans |= set(range(elem, next_elem + 1)) + i += 3 + return sorted(ans) + + def merge_w_arrays(arrays): ranges = [] for w in arrays: @@ -822,10 +838,18 @@ def merge_font(fonts): cmaps = list(filter(None, (f['ToUnicode'] for f in t0_fonts))) if cmaps: t0_font['ToUnicode'] = as_bytes(merge_cmaps(cmaps)) - for key in ('W', 'W2'): - arrays = tuple(filter(None, (f[key] for f in descendant_fonts))) - base_font[key] = merge_w_arrays(arrays) - base_font['sfnt'] = merge_truetype_fonts_for_pdf(*(f['sfnt'] for f in descendant_fonts)) + base_font['sfnt'], width_for_glyph_id, height_for_glyph_id = merge_truetype_fonts_for_pdf(*(f['sfnt'] for f in descendant_fonts)) + widths = [] + arrays = tuple(filter(None, (f['W'] for f in descendant_fonts))) + if arrays: + for gid in all_glyph_ids_in_w_arrays(arrays): + widths.append(gid), widths.append(gid), widths.append(1000*width_for_glyph_id(gid)) + base_font['W'] = merge_w_arrays((widths,)) + arrays = tuple(filter(None, (f['W2'] for f in descendant_fonts))) + if arrays: + for gid in all_glyph_ids_in_w_arrays(arrays): + widths.append(gid), widths.append(gid), widths.append(1000*height_for_glyph_id(gid)) + base_font['W2'] = merge_w_arrays((widths,)) return t0_font, base_font, references_to_drop diff --git a/src/calibre/utils/fonts/sfnt/container.py b/src/calibre/utils/fonts/sfnt/container.py index bac85f17e7..394a9572ac 100644 --- a/src/calibre/utils/fonts/sfnt/container.py +++ b/src/calibre/utils/fonts/sfnt/container.py @@ -1,29 +1,25 @@ #!/usr/bin/env python2 # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +# License: GPLv3 Copyright: 2012, Kovid Goyal from __future__ import absolute_import, division, print_function, unicode_literals -__license__ = 'GPL v3' -__copyright__ = '2012, Kovid Goyal ' -__docformat__ = 'restructuredtext en' - -from struct import pack, calcsize -from io import BytesIO from collections import OrderedDict +from io import BytesIO +from struct import calcsize, pack -from calibre.utils.fonts.utils import (get_tables, checksum_of_block, - verify_checksums) -from calibre.utils.fonts.sfnt import align_block, UnknownTable, max_power_of_two -from calibre.utils.fonts.sfnt.errors import UnsupportedFont - -from calibre.utils.fonts.sfnt.head import (HeadTable, HorizontalHeader, - OS2Table, PostTable) -from calibre.utils.fonts.sfnt.maxp import MaxpTable -from calibre.utils.fonts.sfnt.loca import LocaTable -from calibre.utils.fonts.sfnt.glyf import GlyfTable -from calibre.utils.fonts.sfnt.cmap import CmapTable -from calibre.utils.fonts.sfnt.kern import KernTable -from calibre.utils.fonts.sfnt.gsub import GSUBTable +from calibre.utils.fonts.sfnt import UnknownTable, align_block, max_power_of_two from calibre.utils.fonts.sfnt.cff.table import CFFTable +from calibre.utils.fonts.sfnt.cmap import CmapTable +from calibre.utils.fonts.sfnt.errors import UnsupportedFont +from calibre.utils.fonts.sfnt.glyf import GlyfTable +from calibre.utils.fonts.sfnt.gsub import GSUBTable +from calibre.utils.fonts.sfnt.head import ( + HeadTable, HorizontalHeader, OS2Table, PostTable +) +from calibre.utils.fonts.sfnt.kern import KernTable +from calibre.utils.fonts.sfnt.loca import LocaTable +from calibre.utils.fonts.sfnt.maxp import MaxpTable +from calibre.utils.fonts.utils import checksum_of_block, get_tables, verify_checksums # OpenType spec: http://www.microsoft.com/typography/otspec/otff.htm @@ -100,6 +96,9 @@ class Sfnt(object): def pop(self, key, default=None): return self.tables.pop(key, default) + def get(self, key, default=None): + return self.tables.get(key, default) + def sizes(self): ans = OrderedDict() for tag in self: diff --git a/src/calibre/utils/fonts/sfnt/head.py b/src/calibre/utils/fonts/sfnt/head.py index 5abb0c5cf0..a9a5c69113 100644 --- a/src/calibre/utils/fonts/sfnt/head.py +++ b/src/calibre/utils/fonts/sfnt/head.py @@ -10,6 +10,7 @@ from struct import unpack_from, pack, calcsize from calibre.utils.fonts.sfnt import UnknownTable, DateTimeProperty, FixedProperty from calibre.utils.fonts.sfnt.errors import UnsupportedFont +from calibre.utils.fonts.sfnt.loca import read_array from polyglot.builtins import zip @@ -67,7 +68,7 @@ class HorizontalHeader(UnknownTable): 'descender', 'h', 'line_gap', 'h', 'advance_width_max', 'H', - 'min_left_size_bearing', 'h', + 'min_left_side_bearing', 'h', 'min_right_side_bearing', 'h', 'x_max_extent', 'h', 'caret_slope_rise', 'h', @@ -92,12 +93,55 @@ class HorizontalHeader(UnknownTable): if len(raw) < 4*num: raise UnsupportedFont('The hmtx table has insufficient data') long_hor_metric = raw[:4*num] - fmt = '>%dH'%(2*num) - entries = unpack_from(fmt.encode('ascii'), long_hor_metric) - self.advance_widths = entries[0::2] - fmt = '>%dh'%(2*num) - entries = unpack_from(fmt.encode('ascii'), long_hor_metric) - self.left_side_bearings = entries[1::2] + a = read_array(long_hor_metric) + self.advance_widths = a[0::2] + a = read_array(long_hor_metric, 'h') + self.left_side_bearings = a[1::2] + + +class VericalHeader(UnknownTable): + + version_number = FixedProperty('_version_number') + + def read_data(self, vmtx): + if hasattr(self, 'ascender'): + return + field_types = ( + '_version_number' , 'l', + 'ascender', 'h', + 'descender', 'h', + 'line_gap', 'h', + 'advance_height_max', 'H', + 'min_top_side_bearing', 'h', + 'min_bottom_side_bearing', 'h', + 'y_max_extent', 'h', + 'caret_slope_rise', 'h', + 'caret_slop_run', 'h', + 'caret_offset', 'h', + 'r1', 'h', + 'r2', 'h', + 'r3', 'h', + 'r4', 'h', + 'metric_data_format', 'h', + 'number_of_v_metrics', 'H', + ) + + self._fmt = ('>%s'%(''.join(field_types[1::2]))).encode('ascii') + self._fields = field_types[0::2] + + for f, val in zip(self._fields, unpack_from(self._fmt, self.raw)): + setattr(self, f, val) + + raw = vmtx.raw + num = self.number_of_h_metrics + if len(raw) < 4*num: + raise UnsupportedFont('The vmtx table has insufficient data') + long_hor_metric = raw[:4*num] + long_hor_metric = raw[:4*num] + a = read_array(long_hor_metric) + self.advance_heights = a[0::2] + a = read_array(long_hor_metric, 'h') + self.top_side_bearings = a[1::2] class OS2Table(UnknownTable): diff --git a/src/calibre/utils/fonts/sfnt/loca.py b/src/calibre/utils/fonts/sfnt/loca.py index ff1d2e7206..04a9c37480 100644 --- a/src/calibre/utils/fonts/sfnt/loca.py +++ b/src/calibre/utils/fonts/sfnt/loca.py @@ -21,14 +21,18 @@ def four_byte_type_code(): return c +def read_array(data, fmt='H'): + ans = array.array(fmt, data) + if sys.byteorder != 'big': + ans.byteswap() + return ans + + class LocaTable(UnknownTable): def load_offsets(self, head_table, maxp_table): fmt = 'H' if head_table.index_to_loc_format == 0 else four_byte_type_code() - locs = array.array(fmt) - locs.fromstring(self.raw) - if sys.byteorder != "big": - locs.byteswap() + locs = read_array(self.raw, fmt) self.offset_map = locs.tolist() if fmt == 'H': self.offset_map = [2*i for i in self.offset_map] diff --git a/src/calibre/utils/fonts/sfnt/merge.py b/src/calibre/utils/fonts/sfnt/merge.py index 163a60f227..8c73d576ea 100644 --- a/src/calibre/utils/fonts/sfnt/merge.py +++ b/src/calibre/utils/fonts/sfnt/merge.py @@ -30,6 +30,26 @@ def merge_truetype_fonts_for_pdf(*fonts): head = ans[b'head'] loca = ans[b'loca'] maxp = ans[b'maxp'] + advance_widths = advance_heights = (0,) + hhea = ans.get(b'hhea') + if hhea is not None: + hhea.read_data(ans[b'hmtx']) + advance_widths = tuple(x/head.units_per_em for x in hhea.advance_widths) + vhea = ans.get(b'vhea') + if vhea is not None: + vhea.read_data(ans[b'vmtx']) + advance_heights = tuple(x/head.units_per_em for x in hhea.advance_heights) + + def width_for_glyph_id(gid): + if gid >= len(advance_widths): + gid = -1 + return advance_widths[gid] + + def height_for_glyph_id(gid): + if gid >= len(advance_widths): + gid = -1 + return advance_heights[gid] + gmap = OrderedDict() for glyph_id in sorted(all_glyphs): gmap[glyph_id] = partial(all_glyphs.__getitem__, glyph_id) @@ -39,4 +59,4 @@ def merge_truetype_fonts_for_pdf(*fonts): head.update() maxp.num_glyphs = len(loca.offset_map) - 1 maxp.update() - return ans + return ans, width_for_glyph_id, height_for_glyph_id