123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536 |
- 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
|