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.
This commit is contained in:
Kovid Goyal 2019-09-07 12:50:59 +05:30
parent 7366d8f57a
commit f10e278d74
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 126 additions and 35 deletions

View File

@ -657,6 +657,22 @@ class Range(object):
return len(self.widths) == 1 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): def merge_w_arrays(arrays):
ranges = [] ranges = []
for w in arrays: for w in arrays:
@ -822,10 +838,18 @@ def merge_font(fonts):
cmaps = list(filter(None, (f['ToUnicode'] for f in t0_fonts))) cmaps = list(filter(None, (f['ToUnicode'] for f in t0_fonts)))
if cmaps: if cmaps:
t0_font['ToUnicode'] = as_bytes(merge_cmaps(cmaps)) t0_font['ToUnicode'] = as_bytes(merge_cmaps(cmaps))
for key in ('W', 'W2'): base_font['sfnt'], width_for_glyph_id, height_for_glyph_id = merge_truetype_fonts_for_pdf(*(f['sfnt'] for f in descendant_fonts))
arrays = tuple(filter(None, (f[key] for f in descendant_fonts))) widths = []
base_font[key] = merge_w_arrays(arrays) arrays = tuple(filter(None, (f['W'] for f in descendant_fonts)))
base_font['sfnt'] = merge_truetype_fonts_for_pdf(*(f['sfnt'] 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 return t0_font, base_font, references_to_drop

View File

@ -1,29 +1,25 @@
#!/usr/bin/env python2 #!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
# License: GPLv3 Copyright: 2012, Kovid Goyal <kovid at kovidgoyal.net>
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from struct import pack, calcsize
from io import BytesIO
from collections import OrderedDict from collections import OrderedDict
from io import BytesIO
from struct import calcsize, pack
from calibre.utils.fonts.utils import (get_tables, checksum_of_block, from calibre.utils.fonts.sfnt import UnknownTable, align_block, max_power_of_two
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.cff.table import CFFTable 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 # OpenType spec: http://www.microsoft.com/typography/otspec/otff.htm
@ -100,6 +96,9 @@ class Sfnt(object):
def pop(self, key, default=None): def pop(self, key, default=None):
return self.tables.pop(key, default) return self.tables.pop(key, default)
def get(self, key, default=None):
return self.tables.get(key, default)
def sizes(self): def sizes(self):
ans = OrderedDict() ans = OrderedDict()
for tag in self: for tag in self:

View File

@ -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 import UnknownTable, DateTimeProperty, FixedProperty
from calibre.utils.fonts.sfnt.errors import UnsupportedFont from calibre.utils.fonts.sfnt.errors import UnsupportedFont
from calibre.utils.fonts.sfnt.loca import read_array
from polyglot.builtins import zip from polyglot.builtins import zip
@ -67,7 +68,7 @@ class HorizontalHeader(UnknownTable):
'descender', 'h', 'descender', 'h',
'line_gap', 'h', 'line_gap', 'h',
'advance_width_max', 'H', 'advance_width_max', 'H',
'min_left_size_bearing', 'h', 'min_left_side_bearing', 'h',
'min_right_side_bearing', 'h', 'min_right_side_bearing', 'h',
'x_max_extent', 'h', 'x_max_extent', 'h',
'caret_slope_rise', 'h', 'caret_slope_rise', 'h',
@ -92,12 +93,55 @@ class HorizontalHeader(UnknownTable):
if len(raw) < 4*num: if len(raw) < 4*num:
raise UnsupportedFont('The hmtx table has insufficient data') raise UnsupportedFont('The hmtx table has insufficient data')
long_hor_metric = raw[:4*num] long_hor_metric = raw[:4*num]
fmt = '>%dH'%(2*num) a = read_array(long_hor_metric)
entries = unpack_from(fmt.encode('ascii'), long_hor_metric) self.advance_widths = a[0::2]
self.advance_widths = entries[0::2] a = read_array(long_hor_metric, 'h')
fmt = '>%dh'%(2*num) self.left_side_bearings = a[1::2]
entries = unpack_from(fmt.encode('ascii'), long_hor_metric)
self.left_side_bearings = entries[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): class OS2Table(UnknownTable):

View File

@ -21,14 +21,18 @@ def four_byte_type_code():
return c return c
def read_array(data, fmt='H'):
ans = array.array(fmt, data)
if sys.byteorder != 'big':
ans.byteswap()
return ans
class LocaTable(UnknownTable): class LocaTable(UnknownTable):
def load_offsets(self, head_table, maxp_table): def load_offsets(self, head_table, maxp_table):
fmt = 'H' if head_table.index_to_loc_format == 0 else four_byte_type_code() fmt = 'H' if head_table.index_to_loc_format == 0 else four_byte_type_code()
locs = array.array(fmt) locs = read_array(self.raw, fmt)
locs.fromstring(self.raw)
if sys.byteorder != "big":
locs.byteswap()
self.offset_map = locs.tolist() self.offset_map = locs.tolist()
if fmt == 'H': if fmt == 'H':
self.offset_map = [2*i for i in self.offset_map] self.offset_map = [2*i for i in self.offset_map]

View File

@ -30,6 +30,26 @@ def merge_truetype_fonts_for_pdf(*fonts):
head = ans[b'head'] head = ans[b'head']
loca = ans[b'loca'] loca = ans[b'loca']
maxp = ans[b'maxp'] 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() gmap = OrderedDict()
for glyph_id in sorted(all_glyphs): for glyph_id in sorted(all_glyphs):
gmap[glyph_id] = partial(all_glyphs.__getitem__, glyph_id) gmap[glyph_id] = partial(all_glyphs.__getitem__, glyph_id)
@ -39,4 +59,4 @@ def merge_truetype_fonts_for_pdf(*fonts):
head.update() head.update()
maxp.num_glyphs = len(loca.offset_map) - 1 maxp.num_glyphs = len(loca.offset_map) - 1
maxp.update() maxp.update()
return ans return ans, width_for_glyph_id, height_for_glyph_id