Merge pull request #2447 from fonttools/merge-cff-rebased
Merge CFF rebased
This commit is contained in:
commit
0a7164a452
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
23445
Tests/merge/data/CFFFont1.ttx
Normal file
File diff suppressed because it is too large
Load Diff
6682
Tests/merge/data/CFFFont2.ttx
Normal file
6682
Tests/merge/data/CFFFont2.ttx
Normal file
File diff suppressed because it is too large
Load Diff
30064
Tests/merge/data/CFFFont_expected.ttx
Normal file
30064
Tests/merge/data/CFFFont_expected.ttx
Normal file
File diff suppressed because it is too large
Load Diff
@ -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):
|
Loading…
x
Reference in New Issue
Block a user