diff options
Diffstat (limited to 'Lib/fontTools/subset/cff.py')
-rw-r--r-- | Lib/fontTools/subset/cff.py | 952 |
1 files changed, 493 insertions, 459 deletions
diff --git a/Lib/fontTools/subset/cff.py b/Lib/fontTools/subset/cff.py index d6872f39..dd79f6db 100644 --- a/Lib/fontTools/subset/cff.py +++ b/Lib/fontTools/subset/cff.py @@ -7,496 +7,530 @@ 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 __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 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 + 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 ')) + 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] + 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) - 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" - # 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) - return True # bool(cff.fontNames) -@_add_method(ttLib.getTableClass('CFF ')) +@_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()) + 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 + 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 + 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 __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_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) - 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 ')) + 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 + 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 + 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 ')) +@deprecateFunction( + "use 'CFFFontSet.desubroutinize()' instead", category=DeprecationWarning +) +@_add_method(ttLib.getTableClass("CFF ")) def desubroutinize(self): - self.cff.desubroutinize() + self.cff.desubroutinize() -@_add_method(ttLib.getTableClass('CFF ')) +@_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 ')) + 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", []) - 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 = [] - 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 + 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", []) + 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 = [] + 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 |