Merge pull request #2447 from fonttools/merge-cff-rebased

Merge CFF rebased
This commit is contained in:
Khaled Hosny 2021-11-19 14:20:26 +02:00 committed by GitHub
commit 0a7164a452
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 60460 additions and 120 deletions

View File

@ -38,6 +38,85 @@ maxStackLimit = 513
# maxstack operator has been deprecated. max stack is now always 513.
class StopHintCountEvent(Exception):
pass
class _DesubroutinizingT2Decompiler(psCharStrings.SimpleT2Decompiler):
stop_hintcount_ops = ("op_hintmask", "op_cntrmask", "op_rmoveto", "op_hmoveto",
"op_vmoveto")
def __init__(self, localSubrs, globalSubrs, private=None):
psCharStrings.SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs,
private)
def execute(self, charString):
self.need_hintcount = True # until proven otherwise
for op_name in self.stop_hintcount_ops:
setattr(self, op_name, self.stop_hint_count)
if hasattr(charString, '_desubroutinized'):
# If a charstring has already been desubroutinized, we will still
# need to execute it if we need to count hints in order to
# compute the byte length for mask arguments, and haven't finished
# counting hints pairs.
if self.need_hintcount and self.callingStack:
try:
psCharStrings.SimpleT2Decompiler.execute(self, charString)
except StopHintCountEvent:
del self.callingStack[-1]
return
charString._patches = []
psCharStrings.SimpleT2Decompiler.execute(self, charString)
desubroutinized = charString.program[:]
for idx, expansion in reversed(charString._patches):
assert idx >= 2
assert desubroutinized[idx - 1] in ['callsubr', 'callgsubr'], desubroutinized[idx - 1]
assert type(desubroutinized[idx - 2]) == int
if expansion[-1] == 'return':
expansion = expansion[:-1]
desubroutinized[idx-2:idx] = expansion
if not self.private.in_cff2:
if 'endchar' in desubroutinized:
# Cut off after first endchar
desubroutinized = desubroutinized[:desubroutinized.index('endchar') + 1]
else:
if not len(desubroutinized) or desubroutinized[-1] != 'return':
desubroutinized.append('return')
charString._desubroutinized = desubroutinized
del charString._patches
def op_callsubr(self, index):
subr = self.localSubrs[self.operandStack[-1]+self.localBias]
psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
self.processSubr(index, subr)
def op_callgsubr(self, index):
subr = self.globalSubrs[self.operandStack[-1]+self.globalBias]
psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
self.processSubr(index, subr)
def stop_hint_count(self, *args):
self.need_hintcount = False
for op_name in self.stop_hintcount_ops:
setattr(self, op_name, None)
cs = self.callingStack[-1]
if hasattr(cs, '_desubroutinized'):
raise StopHintCountEvent()
def op_hintmask(self, index):
psCharStrings.SimpleT2Decompiler.op_hintmask(self, index)
if self.need_hintcount:
self.stop_hint_count()
def processSubr(self, index, subr):
cs = self.callingStack[-1]
if not hasattr(cs, '_desubroutinized'):
cs._patches.append((index, subr._desubroutinized))
class CFFFontSet(object):
"""A CFF font "file" can contain more than one font, although this is
extremely rare (and not allowed within OpenType fonts).
@ -368,6 +447,35 @@ class CFFFontSet(object):
file.seek(0)
self.decompile(file, otFont, isCFF2=True)
def desubroutinize(self):
for fontName in self.fontNames:
font = self[fontName]
cs = font.CharStrings
for g in font.charset:
c, _ = cs.getItemAndSelector(g)
c.decompile()
subrs = getattr(c.private, "Subrs", [])
decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs, c.private)
decompiler.execute(c)
c.program = c._desubroutinized
del c._desubroutinized
# Delete all the local subrs
if hasattr(font, 'FDArray'):
for fd in font.FDArray:
pd = fd.Private
if hasattr(pd, 'Subrs'):
del pd.Subrs
if 'Subrs' in pd.rawDict:
del pd.rawDict['Subrs']
else:
pd = font.Private
if hasattr(pd, 'Subrs'):
del pd.Subrs
if 'Subrs' in pd.rawDict:
del pd.rawDict['Subrs']
# as well as the global subrs
self.GlobalSubrs.clear()
class CFFWriter(object):
"""Helper class for serializing CFF data to binary. Used by

View File

@ -13,6 +13,7 @@ import sys
import time
import operator
import logging
import os
log = logging.getLogger("fontTools.merge")
@ -200,7 +201,7 @@ ttLib.getTableClass('head').mergeMap = {
'macStyle': first,
'lowestRecPPEM': max,
'fontDirectionHint': lambda lst: 2,
'indexToLocFormat': recalculate,
'indexToLocFormat': first,
'glyphDataFormat': equal,
}
@ -371,6 +372,51 @@ ttLib.getTableClass('fpgm').mergeMap = lambda self, lst: first(lst)
ttLib.getTableClass('cvt ').mergeMap = lambda self, lst: first(lst)
ttLib.getTableClass('gasp').mergeMap = lambda self, lst: first(lst) # FIXME? Appears irreconcilable
@_add_method(ttLib.getTableClass('CFF '))
def merge(self, m, tables):
if any(hasattr(table, "FDSelect") for table in tables):
raise NotImplementedError(
"Merging CID-keyed CFF tables is not supported yet"
)
newcff = tables[0]
newfont = newcff.cff[0]
private = newfont.Private
storedNamesStrings = []
glyphOrderStrings = []
glyphOrder = set(newfont.getGlyphOrder())
for name in newfont.strings.strings:
if name not in glyphOrder:
storedNamesStrings.append(name)
else:
glyphOrderStrings.append(name)
chrset = list(newfont.charset)
newcs = newfont.CharStrings
log.debug("FONT 0 CharStrings: %d.", len(newcs))
for i, table in enumerate(tables[1:], start=1):
font = table.cff[0]
font.Private = private
fontGlyphOrder = set(font.getGlyphOrder())
for name in font.strings.strings:
if name in fontGlyphOrder:
glyphOrderStrings.append(name)
cs = font.CharStrings
gs = table.cff.GlobalSubrs
log.debug("Font %d CharStrings: %d.", i, len(cs))
chrset.extend(font.charset)
if newcs.charStringsAreIndexed:
for i, name in enumerate(cs.charStrings, start=len(newcs)):
newcs.charStrings[name] = i
newcs.charStringsIndex.items.append(None)
for name in cs.charStrings:
newcs[name] = cs[name]
newfont.charset = chrset
newfont.numGlyphs = len(chrset)
newfont.strings.strings = glyphOrderStrings + storedNamesStrings
return newcff
def _glyphsAreSame(glyphSet1, glyphSet2, glyph1, glyph2):
pen1 = DecomposingRecordingPen(glyphSet1)
pen2 = DecomposingRecordingPen(glyphSet2)
@ -865,9 +911,10 @@ class Options(object):
op = k[-1]+'=' # Ops is '-=' or '+=' now.
k = k[:-1]
v = a[i+1:]
ok = k
k = k.replace('-', '_')
if not hasattr(self, k):
if ignore_unknown is True or k in ignore_unknown:
if ignore_unknown is True or ok in ignore_unknown:
ret.append(orig_a)
continue
else:
@ -993,20 +1040,35 @@ class Merger(object):
A :class:`fontTools.ttLib.TTFont` object. Call the ``save`` method on
this to write it out to an OTF file.
"""
mega = ttLib.TTFont()
#
# Settle on a mega glyph order.
#
fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
glyphOrders = [font.getGlyphOrder() for font in fonts]
megaGlyphOrder = self._mergeGlyphOrders(glyphOrders)
# Take first input file sfntVersion
sfntVersion = fonts[0].sfntVersion
cffTables = [None] * len(fonts)
if sfntVersion == "OTTO":
for i, font in enumerate(fonts):
font['CFF '].cff.desubroutinize()
cffTables[i] = font['CFF ']
# Reload fonts and set new glyph names on them.
# TODO Is it necessary to reload font? I think it is. At least
# it's safer, in case tables were loaded to provide glyph names.
fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
for font,glyphOrder in zip(fonts, glyphOrders):
for font, glyphOrder, cffTable in zip(fonts, glyphOrders, cffTables):
font.setGlyphOrder(glyphOrder)
if cffTable:
# Rename CFF CharStrings to match the new glyphOrder.
# Using cffTable from before reloading the fonts, because reasons.
self._renameCFFCharStrings(glyphOrder, cffTable)
font['CFF '] = cffTable
mega = ttLib.TTFont(sfntVersion=sfntVersion)
mega.setGlyphOrder(megaGlyphOrder)
for font in fonts:
@ -1064,6 +1126,15 @@ class Merger(object):
mega[glyphName] = 1
return list(mega.keys())
def _renameCFFCharStrings(self, glyphOrder, cffTable):
"""Rename topDictIndex charStrings based on glyphOrder."""
td = cffTable.cff.topDictIndex[0]
charStrings = {}
for i, v in enumerate(td.CharStrings.charStrings.values()):
glyphName = glyphOrder[i]
charStrings[glyphName] = v
cffTable.cff.topDictIndex[0].CharStrings.charStrings = charStrings
def mergeObjects(self, returnTable, logic, tables):
# Right now we don't use self at all. Will use in the future
# for options and logging.
@ -1182,7 +1253,14 @@ def main(args=None):
args = sys.argv[1:]
options = Options()
args = options.parse_opts(args)
args = options.parse_opts(args, ignore_unknown=['output-file'])
outfile = 'merged.ttf'
fontfiles = []
for g in args:
if g.startswith('--output-file='):
outfile = g[14:]
continue
fontfiles.append(g)
if len(args) < 1:
print("usage: pyftmerge font...", file=sys.stderr)
@ -1195,8 +1273,7 @@ def main(args=None):
timer.logger.disabled = True
merger = Merger(options=options)
font = merger.merge(args)
outfile = 'merged.ttf'
font = merger.merge(fontfiles)
with timer("compile and save font"):
font.save(outfile)

View File

@ -2,6 +2,7 @@ from fontTools.misc import psCharStrings
from fontTools import ttLib
from fontTools.pens.basePen import NullPen
from fontTools.misc.roundTools import otRound
from fontTools.misc.loggingTools import deprecateFunction
from fontTools.varLib.varStore import VarStoreInstancer
from fontTools.subset.util import _add_method, _uniq_sort
@ -345,86 +346,6 @@ class _DehintingT2Decompiler(psCharStrings.T2WidthExtractor):
hints.status = max(hints.status, subr_hints.status)
class StopHintCountEvent(Exception):
pass
class _DesubroutinizingT2Decompiler(psCharStrings.SimpleT2Decompiler):
stop_hintcount_ops = ("op_hintmask", "op_cntrmask", "op_rmoveto", "op_hmoveto",
"op_vmoveto")
def __init__(self, localSubrs, globalSubrs, private=None):
psCharStrings.SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs,
private)
def execute(self, charString):
self.need_hintcount = True # until proven otherwise
for op_name in self.stop_hintcount_ops:
setattr(self, op_name, self.stop_hint_count)
if hasattr(charString, '_desubroutinized'):
# If a charstring has already been desubroutinized, we will still
# need to execute it if we need to count hints in order to
# compute the byte length for mask arguments, and haven't finished
# counting hints pairs.
if self.need_hintcount and self.callingStack:
try:
psCharStrings.SimpleT2Decompiler.execute(self, charString)
except StopHintCountEvent:
del self.callingStack[-1]
return
charString._patches = []
psCharStrings.SimpleT2Decompiler.execute(self, charString)
desubroutinized = charString.program[:]
for idx, expansion in reversed(charString._patches):
assert idx >= 2
assert desubroutinized[idx - 1] in ['callsubr', 'callgsubr'], desubroutinized[idx - 1]
assert type(desubroutinized[idx - 2]) == int
if expansion[-1] == 'return':
expansion = expansion[:-1]
desubroutinized[idx-2:idx] = expansion
if not self.private.in_cff2:
if 'endchar' in desubroutinized:
# Cut off after first endchar
desubroutinized = desubroutinized[:desubroutinized.index('endchar') + 1]
else:
if not len(desubroutinized) or desubroutinized[-1] != 'return':
desubroutinized.append('return')
charString._desubroutinized = desubroutinized
del charString._patches
def op_callsubr(self, index):
subr = self.localSubrs[self.operandStack[-1]+self.localBias]
psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
self.processSubr(index, subr)
def op_callgsubr(self, index):
subr = self.globalSubrs[self.operandStack[-1]+self.globalBias]
psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
self.processSubr(index, subr)
def stop_hint_count(self, *args):
self.need_hintcount = False
for op_name in self.stop_hintcount_ops:
setattr(self, op_name, None)
cs = self.callingStack[-1]
if hasattr(cs, '_desubroutinized'):
raise StopHintCountEvent()
def op_hintmask(self, index):
psCharStrings.SimpleT2Decompiler.op_hintmask(self, index)
if self.need_hintcount:
self.stop_hint_count()
def processSubr(self, index, subr):
cs = self.callingStack[-1]
if not hasattr(cs, '_desubroutinized'):
cs._patches.append((index, subr._desubroutinized))
@_add_method(ttLib.getTableClass('CFF '))
def prune_post_subset(self, ttfFont, options):
@ -444,7 +365,7 @@ def prune_post_subset(self, ttfFont, options):
# Desubroutinize if asked for
if options.desubroutinize:
self.desubroutinize()
cff.desubroutinize()
# Drop hints if not needed
if not options.hinting:
@ -460,36 +381,11 @@ def _delete_empty_subrs(private_dict):
del private_dict.rawDict['Subrs']
del private_dict.Subrs
@deprecateFunction("use 'CFFFontSet.desubroutinize()' instead", category=DeprecationWarning)
@_add_method(ttLib.getTableClass('CFF '))
def desubroutinize(self):
cff = self.cff
for fontname in cff.keys():
font = cff[fontname]
cs = font.CharStrings
for g in font.charset:
c, _ = cs.getItemAndSelector(g)
c.decompile()
subrs = getattr(c.private, "Subrs", [])
decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs, c.private)
decompiler.execute(c)
c.program = c._desubroutinized
del c._desubroutinized
# Delete all the local subrs
if hasattr(font, 'FDArray'):
for fd in font.FDArray:
pd = fd.Private
if hasattr(pd, 'Subrs'):
del pd.Subrs
if 'Subrs' in pd.rawDict:
del pd.rawDict['Subrs']
else:
pd = font.Private
if hasattr(pd, 'Subrs'):
del pd.Subrs
if 'Subrs' in pd.rawDict:
del pd.rawDict['Subrs']
# as well as the global subrs
cff.GlobalSubrs.clear()
self.cff.desubroutinize()
@_add_method(ttLib.getTableClass('CFF '))

23445
Tests/merge/data/CFFFont1.ttx Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,14 +3,82 @@ import itertools
from fontTools import ttLib
from fontTools.ttLib.tables._g_l_y_f import Glyph
from fontTools.fontBuilder import FontBuilder
from fontTools.merge import Merger
from fontTools.merge import Merger, main as merge_main
import difflib
import os
import re
import shutil
import sys
import tempfile
import unittest
import pathlib
import pytest
class MergeIntegrationTest(unittest.TestCase):
# TODO
pass
def setUp(self):
self.tempdir = None
self.num_tempfiles = 0
def tearDown(self):
if self.tempdir:
shutil.rmtree(self.tempdir)
@staticmethod
def getpath(testfile):
path, _ = os.path.split(__file__)
return os.path.join(path, "data", testfile)
def temp_path(self, suffix):
if not self.tempdir:
self.tempdir = tempfile.mkdtemp()
self.num_tempfiles += 1
return os.path.join(self.tempdir, "tmp%d%s" % (self.num_tempfiles, suffix))
IGNORED_LINES_RE = re.compile(
"^(<ttFont | <(checkSumAdjustment|created|modified) ).*"
)
def read_ttx(self, path):
lines = []
with open(path, "r", encoding="utf-8") as ttx:
for line in ttx.readlines():
# Elide lines with data that often change.
if self.IGNORED_LINES_RE.match(line):
lines.append("\n")
else:
lines.append(line.rstrip() + "\n")
return lines
def expect_ttx(self, font, expected_ttx, tables=None):
path = self.temp_path(suffix=".ttx")
font.saveXML(path, tables=tables)
actual = self.read_ttx(path)
expected = self.read_ttx(expected_ttx)
if actual != expected:
for line in difflib.unified_diff(
expected, actual, fromfile=expected_ttx, tofile=path):
sys.stdout.write(line)
self.fail("TTX output is different from expected")
def compile_font(self, path, suffix):
savepath = self.temp_path(suffix=suffix)
font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
font.importXML(path)
font.save(savepath, reorderTables=None)
return font, savepath
# -----
# Tests
# -----
def test_merge_cff(self):
_, fontpath1 = self.compile_font(self.getpath("CFFFont1.ttx"), ".otf")
_, fontpath2 = self.compile_font(self.getpath("CFFFont2.ttx"), ".otf")
mergedpath = self.temp_path(".otf")
merge_main([fontpath1, fontpath2, "--output-file=%s" % mergedpath])
mergedfont = ttLib.TTFont(mergedpath)
self.expect_ttx(mergedfont, self.getpath("CFFFont_expected.ttx"))
class gaspMergeUnitTest(unittest.TestCase):
def setUp(self):