diff --git a/Lib/fontTools/cffLib/__init__.py b/Lib/fontTools/cffLib/__init__.py index aef6dd205..07d0d5136 100644 --- a/Lib/fontTools/cffLib/__init__.py +++ b/Lib/fontTools/cffLib/__init__.py @@ -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 diff --git a/Lib/fontTools/merge.py b/Lib/fontTools/merge.py index d3334fe7a..9f8106156 100644 --- a/Lib/fontTools/merge.py +++ b/Lib/fontTools/merge.py @@ -14,32 +14,11 @@ import time import operator import logging import os -from fontTools import subset + log = logging.getLogger("fontTools.merge") timer = Timer(logger=logging.getLogger(__name__+".timer"), level=logging.INFO) -def _desubtoutinize(otf): - # Desubroutinize with Subsetter (like compreffor does) - from io import BytesIO - - stream = BytesIO() - otf.save(stream, reorderTables=False) - stream.flush() - stream.seek(0) - tmpfont = ttLib.TTFont(stream) - - options = subset.Options() - options.desubroutinize = True - options.notdef_outline = True - subsetter = subset.Subsetter(options=options) - subsetter.populate(glyphs=tmpfont.getGlyphOrder()) - subsetter.subset(tmpfont) - data = tmpfont['CFF '].compile(tmpfont) - table = ttLib.newTable('CFF ') - table.decompile(data, otf) - otf['CFF '] = table - tmpfont.close() def _add_method(*clazzes, **kwargs): """Returns a decorator function that adds a new method to one or @@ -1090,7 +1069,7 @@ class Merger(object): cffTables = [] if sfntVersion == "OTTO": for i, font in enumerate(fonts): - _desubtoutinize(font) + font['CFF '].cff.desubroutinize() cffTables.append(font['CFF ']) glyphOrders = [font.getGlyphOrder() for font in fonts] diff --git a/Lib/fontTools/subset/cff.py b/Lib/fontTools/subset/cff.py index ae59d4453..b81faf2b6 100644 --- a/Lib/fontTools/subset/cff.py +++ b/Lib/fontTools/subset/cff.py @@ -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 '))