| from fontTools.misc import psCharStrings |
| from fontTools import ttLib |
| from fontTools.pens.basePen import NullPen |
| from fontTools.misc.fixedTools import otRound |
| from fontTools.varLib.varStore import VarStoreInstancer |
| |
| def _add_method(*clazzes): |
| """Returns a decorator function that adds a new method to one or |
| more classes.""" |
| def wrapper(method): |
| done = [] |
| for clazz in clazzes: |
| if clazz in done: continue # Support multiple names of a clazz |
| done.append(clazz) |
| assert clazz.__name__ != 'DefaultTable', \ |
| 'Oops, table class not found.' |
| assert not hasattr(clazz, method.__name__), \ |
| "Oops, class '%s' has method '%s'." % (clazz.__name__, |
| method.__name__) |
| setattr(clazz, method.__name__, method) |
| return None |
| return wrapper |
| |
| def _uniq_sort(l): |
| return sorted(set(l)) |
| |
| 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.decompile() |
| c.program = [] 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 |
| # XXX We want to set sel.format to None, such that the |
| # most compact format is selected. However, OTS was |
| # broken and couldn't parse a FDSelect format 0 that |
| # happened before CharStrings. As such, always force |
| # format 3 until we fix cffLib to always generate |
| # FDSelect after CharStrings. |
| # https://github.com/khaledhosny/ots/pull/31 |
| #sel.format = None |
| sel.format = 3 |
| sel.gidArray = [sel.gidArray[i] for i in indices] |
| cs.charStrings = {g:indices.index(v) |
| for g,v in cs.charStrings.items() |
| if g in glyphs} |
| 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) |
| |
| 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): |
| 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: |
| self.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 |
| |
| @_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() |
| |
| |
| @_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", []) |
| 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 |