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.subset.util import _add_method, _uniq_sort class _ClosureGlyphsT2Decompiler(psCharStrings.SimpleT2Decompiler): def __init__(self, components, localSubrs, globalSubrs): psCharStrings.SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs) self.components = components def op_endchar(self, index): args = self.popall() if len(args) >= 4: from fontTools.encodings.StandardEncoding import StandardEncoding # endchar can do seac accent bulding; The T2 spec says it's deprecated, # but recent software that shall remain nameless does output it. adx, ady, bchar, achar = args[-4:] baseGlyph = StandardEncoding[bchar] accentGlyph = StandardEncoding[achar] self.components.add(baseGlyph) self.components.add(accentGlyph) @_add_method(ttLib.getTableClass("CFF ")) def closure_glyphs(self, s): cff = self.cff assert len(cff) == 1 font = cff[cff.keys()[0]] glyphSet = font.CharStrings decompose = s.glyphs while decompose: components = set() for g in decompose: if g not in glyphSet: continue gl = glyphSet[g] subrs = getattr(gl.private, "Subrs", []) decompiler = _ClosureGlyphsT2Decompiler(components, subrs, gl.globalSubrs) decompiler.execute(gl) components -= s.glyphs s.glyphs.update(components) decompose = components def _empty_charstring(font, glyphName, isCFF2, ignoreWidth=False): c, fdSelectIndex = font.CharStrings.getItemAndSelector(glyphName) if isCFF2 or ignoreWidth: # CFF2 charstrings have no widths nor 'endchar' operators c.setProgram([] if isCFF2 else ["endchar"]) else: if hasattr(font, "FDArray") and font.FDArray is not None: private = font.FDArray[fdSelectIndex].Private else: private = font.Private dfltWdX = private.defaultWidthX nmnlWdX = private.nominalWidthX pen = NullPen() c.draw(pen) # this will set the charstring's width if c.width != dfltWdX: c.program = [c.width - nmnlWdX, "endchar"] else: c.program = ["endchar"] @_add_method(ttLib.getTableClass("CFF ")) def prune_pre_subset(self, font, options): cff = self.cff # CFF table must have one font only cff.fontNames = cff.fontNames[:1] if options.notdef_glyph and not options.notdef_outline: isCFF2 = cff.major > 1 for fontname in cff.keys(): font = cff[fontname] _empty_charstring(font, ".notdef", isCFF2=isCFF2) # Clear useless Encoding for fontname in cff.keys(): font = cff[fontname] # https://github.com/fonttools/fonttools/issues/620 font.Encoding = "StandardEncoding" return True # bool(cff.fontNames) @_add_method(ttLib.getTableClass("CFF ")) def subset_glyphs(self, s): cff = self.cff for fontname in cff.keys(): font = cff[fontname] cs = font.CharStrings glyphs = s.glyphs.union(s.glyphs_emptied) # Load all glyphs for g in font.charset: if g not in glyphs: continue c, _ = cs.getItemAndSelector(g) if cs.charStringsAreIndexed: indices = [i for i, g in enumerate(font.charset) if g in glyphs] csi = cs.charStringsIndex csi.items = [csi.items[i] for i in indices] del csi.file, csi.offsets if hasattr(font, "FDSelect"): sel = font.FDSelect sel.format = None sel.gidArray = [sel.gidArray[i] for i in indices] newCharStrings = {} for indicesIdx, charsetIdx in enumerate(indices): g = font.charset[charsetIdx] if g in cs.charStrings: newCharStrings[g] = indicesIdx cs.charStrings = newCharStrings else: cs.charStrings = {g: v for g, v in cs.charStrings.items() if g in glyphs} font.charset = [g for g in font.charset if g in glyphs] font.numGlyphs = len(font.charset) if s.options.retain_gids: isCFF2 = cff.major > 1 for g in s.glyphs_emptied: _empty_charstring(font, g, isCFF2=isCFF2, ignoreWidth=True) return True # any(cff[fontname].numGlyphs for fontname in cff.keys()) @_add_method(psCharStrings.T2CharString) def subset_subroutines(self, subrs, gsubrs): p = self.program for i in range(1, len(p)): if p[i] == "callsubr": assert isinstance(p[i - 1], int) p[i - 1] = subrs._used.index(p[i - 1] + subrs._old_bias) - subrs._new_bias elif p[i] == "callgsubr": assert isinstance(p[i - 1], int) p[i - 1] = ( gsubrs._used.index(p[i - 1] + gsubrs._old_bias) - gsubrs._new_bias ) @_add_method(psCharStrings.T2CharString) def drop_hints(self): hints = self._hints if hints.deletions: p = self.program for idx in reversed(hints.deletions): del p[idx - 2 : idx] if hints.has_hint: assert not hints.deletions or hints.last_hint <= hints.deletions[0] self.program = self.program[hints.last_hint :] if not self.program: # TODO CFF2 no need for endchar. self.program.append("endchar") if hasattr(self, "width"): # Insert width back if needed if self.width != self.private.defaultWidthX: # For CFF2 charstrings, this should never happen assert ( self.private.defaultWidthX is not None ), "CFF2 CharStrings must not have an initial width value" self.program.insert(0, self.width - self.private.nominalWidthX) if hints.has_hintmask: i = 0 p = self.program while i < len(p): if p[i] in ["hintmask", "cntrmask"]: assert i + 1 <= len(p) del p[i : i + 2] continue i += 1 assert len(self.program) del self._hints class _MarkingT2Decompiler(psCharStrings.SimpleT2Decompiler): def __init__(self, localSubrs, globalSubrs, private): psCharStrings.SimpleT2Decompiler.__init__( self, localSubrs, globalSubrs, private ) for subrs in [localSubrs, globalSubrs]: if subrs and not hasattr(subrs, "_used"): subrs._used = set() def op_callsubr(self, index): self.localSubrs._used.add(self.operandStack[-1] + self.localBias) psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) def op_callgsubr(self, index): self.globalSubrs._used.add(self.operandStack[-1] + self.globalBias) psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) class _DehintingT2Decompiler(psCharStrings.T2WidthExtractor): class Hints(object): def __init__(self): # Whether calling this charstring produces any hint stems # Note that if a charstring starts with hintmask, it will # have has_hint set to True, because it *might* produce an # implicit vstem if called under certain conditions. self.has_hint = False # Index to start at to drop all hints self.last_hint = 0 # Index up to which we know more hints are possible. # Only relevant if status is 0 or 1. self.last_checked = 0 # The status means: # 0: after dropping hints, this charstring is empty # 1: after dropping hints, there may be more hints # continuing after this, or there might be # other things. Not clear yet. # 2: no more hints possible after this charstring self.status = 0 # Has hintmask instructions; not recursive self.has_hintmask = False # List of indices of calls to empty subroutines to remove. self.deletions = [] pass def __init__( self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None ): self._css = css psCharStrings.T2WidthExtractor.__init__( self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX ) self.private = private def execute(self, charString): old_hints = charString._hints if hasattr(charString, "_hints") else None charString._hints = self.Hints() psCharStrings.T2WidthExtractor.execute(self, charString) hints = charString._hints if hints.has_hint or hints.has_hintmask: self._css.add(charString) if hints.status != 2: # Check from last_check, make sure we didn't have any operators. for i in range(hints.last_checked, len(charString.program) - 1): if isinstance(charString.program[i], str): hints.status = 2 break else: hints.status = 1 # There's *something* here hints.last_checked = len(charString.program) if old_hints: assert hints.__dict__ == old_hints.__dict__ def op_callsubr(self, index): subr = self.localSubrs[self.operandStack[-1] + self.localBias] psCharStrings.T2WidthExtractor.op_callsubr(self, index) self.processSubr(index, subr) def op_callgsubr(self, index): subr = self.globalSubrs[self.operandStack[-1] + self.globalBias] psCharStrings.T2WidthExtractor.op_callgsubr(self, index) self.processSubr(index, subr) def op_hstem(self, index): psCharStrings.T2WidthExtractor.op_hstem(self, index) self.processHint(index) def op_vstem(self, index): psCharStrings.T2WidthExtractor.op_vstem(self, index) self.processHint(index) def op_hstemhm(self, index): psCharStrings.T2WidthExtractor.op_hstemhm(self, index) self.processHint(index) def op_vstemhm(self, index): psCharStrings.T2WidthExtractor.op_vstemhm(self, index) self.processHint(index) def op_hintmask(self, index): rv = psCharStrings.T2WidthExtractor.op_hintmask(self, index) self.processHintmask(index) return rv def op_cntrmask(self, index): rv = psCharStrings.T2WidthExtractor.op_cntrmask(self, index) self.processHintmask(index) return rv def processHintmask(self, index): cs = self.callingStack[-1] hints = cs._hints hints.has_hintmask = True if hints.status != 2: # Check from last_check, see if we may be an implicit vstem for i in range(hints.last_checked, index - 1): if isinstance(cs.program[i], str): hints.status = 2 break else: # We are an implicit vstem hints.has_hint = True hints.last_hint = index + 1 hints.status = 0 hints.last_checked = index + 1 def processHint(self, index): cs = self.callingStack[-1] hints = cs._hints hints.has_hint = True hints.last_hint = index hints.last_checked = index def processSubr(self, index, subr): cs = self.callingStack[-1] hints = cs._hints subr_hints = subr._hints # Check from last_check, make sure we didn't have # any operators. if hints.status != 2: for i in range(hints.last_checked, index - 1): if isinstance(cs.program[i], str): hints.status = 2 break hints.last_checked = index if hints.status != 2: if subr_hints.has_hint: hints.has_hint = True # Decide where to chop off from if subr_hints.status == 0: hints.last_hint = index else: hints.last_hint = index - 2 # Leave the subr call in elif subr_hints.status == 0: hints.deletions.append(index) hints.status = max(hints.status, subr_hints.status) @_add_method(ttLib.getTableClass("CFF ")) def prune_post_subset(self, ttfFont, options): cff = self.cff for fontname in cff.keys(): font = cff[fontname] cs = font.CharStrings # Drop unused FontDictionaries if hasattr(font, "FDSelect"): sel = font.FDSelect indices = _uniq_sort(sel.gidArray) sel.gidArray = [indices.index(ss) for ss in sel.gidArray] arr = font.FDArray arr.items = [arr[i] for i in indices] del arr.file, arr.offsets # Desubroutinize if asked for if options.desubroutinize: cff.desubroutinize() # Drop hints if not needed if not options.hinting: self.remove_hints() elif not options.desubroutinize: self.remove_unused_subroutines() return True def _delete_empty_subrs(private_dict): if hasattr(private_dict, "Subrs") and not private_dict.Subrs: if "Subrs" in private_dict.rawDict: del private_dict.rawDict["Subrs"] del private_dict.Subrs @deprecateFunction( "use 'CFFFontSet.desubroutinize()' instead", category=DeprecationWarning ) @_add_method(ttLib.getTableClass("CFF ")) def desubroutinize(self): self.cff.desubroutinize() @_add_method(ttLib.getTableClass("CFF ")) def remove_hints(self): cff = self.cff for fontname in cff.keys(): font = cff[fontname] cs = font.CharStrings # This can be tricky, but doesn't have to. What we do is: # # - Run all used glyph charstrings and recurse into subroutines, # - For each charstring (including subroutines), if it has any # of the hint stem operators, we mark it as such. # Upon returning, for each charstring we note all the # subroutine calls it makes that (recursively) contain a stem, # - Dropping hinting then consists of the following two ops: # * Drop the piece of the program in each charstring before the # last call to a stem op or a stem-calling subroutine, # * Drop all hintmask operations. # - It's trickier... A hintmask right after hints and a few numbers # will act as an implicit vstemhm. As such, we track whether # we have seen any non-hint operators so far and do the right # thing, recursively... Good luck understanding that :( css = set() for g in font.charset: c, _ = cs.getItemAndSelector(g) c.decompile() subrs = getattr(c.private, "Subrs", []) decompiler = _DehintingT2Decompiler( css, subrs, c.globalSubrs, c.private.nominalWidthX, c.private.defaultWidthX, c.private, ) decompiler.execute(c) c.width = decompiler.width for charstring in css: charstring.drop_hints() del css # Drop font-wide hinting values all_privs = [] if hasattr(font, "FDArray"): all_privs.extend(fd.Private for fd in font.FDArray) else: all_privs.append(font.Private) for priv in all_privs: for k in [ "BlueValues", "OtherBlues", "FamilyBlues", "FamilyOtherBlues", "BlueScale", "BlueShift", "BlueFuzz", "StemSnapH", "StemSnapV", "StdHW", "StdVW", "ForceBold", "LanguageGroup", "ExpansionFactor", ]: if hasattr(priv, k): setattr(priv, k, None) self.remove_unused_subroutines() @_add_method(ttLib.getTableClass("CFF ")) def remove_unused_subroutines(self): cff = self.cff for fontname in cff.keys(): font = cff[fontname] cs = font.CharStrings # Renumber subroutines to remove unused ones # Mark all used subroutines for g in font.charset: c, _ = cs.getItemAndSelector(g) subrs = getattr(c.private, "Subrs", []) decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs, c.private) decompiler.execute(c) all_subrs = [font.GlobalSubrs] if hasattr(font, "FDArray"): all_subrs.extend( fd.Private.Subrs for fd in font.FDArray if hasattr(fd.Private, "Subrs") and fd.Private.Subrs ) elif hasattr(font.Private, "Subrs") and font.Private.Subrs: all_subrs.append(font.Private.Subrs) subrs = set(subrs) # Remove duplicates # Prepare for subrs in all_subrs: if not hasattr(subrs, "_used"): subrs._used = set() subrs._used = _uniq_sort(subrs._used) subrs._old_bias = psCharStrings.calcSubrBias(subrs) subrs._new_bias = psCharStrings.calcSubrBias(subrs._used) # Renumber glyph charstrings for g in font.charset: c, _ = cs.getItemAndSelector(g) subrs = getattr(c.private, "Subrs", None) c.subset_subroutines(subrs, font.GlobalSubrs) # Renumber subroutines themselves for subrs in all_subrs: if subrs == font.GlobalSubrs: if not hasattr(font, "FDArray") and hasattr(font.Private, "Subrs"): local_subrs = font.Private.Subrs else: local_subrs = None else: local_subrs = subrs subrs.items = [subrs.items[i] for i in subrs._used] if hasattr(subrs, "file"): del subrs.file if hasattr(subrs, "offsets"): del subrs.offsets for subr in subrs.items: subr.subset_subroutines(local_subrs, font.GlobalSubrs) # Delete local SubrsIndex if empty if hasattr(font, "FDArray"): for fd in font.FDArray: _delete_empty_subrs(fd.Private) else: _delete_empty_subrs(font.Private) # Cleanup for subrs in all_subrs: del subrs._used, subrs._old_bias, subrs._new_bias