|
@@ -0,0 +1,482 @@
|
|
|
+from fontTools.misc.psCharStrings import (
|
|
|
+ SimpleT2Decompiler,
|
|
|
+ T2WidthExtractor,
|
|
|
+ calcSubrBias,
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+def _uniq_sort(l):
|
|
|
+ return sorted(set(l))
|
|
|
+
|
|
|
+
|
|
|
+class StopHintCountEvent(Exception):
|
|
|
+ pass
|
|
|
+
|
|
|
+
|
|
|
+class _DesubroutinizingT2Decompiler(SimpleT2Decompiler):
|
|
|
+ stop_hintcount_ops = (
|
|
|
+ "op_hintmask",
|
|
|
+ "op_cntrmask",
|
|
|
+ "op_rmoveto",
|
|
|
+ "op_hmoveto",
|
|
|
+ "op_vmoveto",
|
|
|
+ )
|
|
|
+
|
|
|
+ def __init__(self, localSubrs, globalSubrs, private=None):
|
|
|
+ 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:
|
|
|
+ SimpleT2Decompiler.execute(self, charString)
|
|
|
+ except StopHintCountEvent:
|
|
|
+ del self.callingStack[-1]
|
|
|
+ return
|
|
|
+
|
|
|
+ charString._patches = []
|
|
|
+ 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
|
|
|
+ ]
|
|
|
+
|
|
|
+ charString._desubroutinized = desubroutinized
|
|
|
+ del charString._patches
|
|
|
+
|
|
|
+ def op_callsubr(self, index):
|
|
|
+ subr = self.localSubrs[self.operandStack[-1] + self.localBias]
|
|
|
+ SimpleT2Decompiler.op_callsubr(self, index)
|
|
|
+ self.processSubr(index, subr)
|
|
|
+
|
|
|
+ def op_callgsubr(self, index):
|
|
|
+ subr = self.globalSubrs[self.operandStack[-1] + self.globalBias]
|
|
|
+ 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):
|
|
|
+ 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))
|
|
|
+
|
|
|
+
|
|
|
+def desubroutinize(cff):
|
|
|
+ for fontName in cff.fontNames:
|
|
|
+ font = cff[fontName]
|
|
|
+ cs = font.CharStrings
|
|
|
+ for c in cs.values():
|
|
|
+ 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()
|
|
|
+
|
|
|
+
|
|
|
+class _MarkingT2Decompiler(SimpleT2Decompiler):
|
|
|
+ def __init__(self, localSubrs, globalSubrs, private):
|
|
|
+ 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)
|
|
|
+ SimpleT2Decompiler.op_callsubr(self, index)
|
|
|
+
|
|
|
+ def op_callgsubr(self, index):
|
|
|
+ self.globalSubrs._used.add(self.operandStack[-1] + self.globalBias)
|
|
|
+ SimpleT2Decompiler.op_callgsubr(self, index)
|
|
|
+
|
|
|
+
|
|
|
+class _DehintingT2Decompiler(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
|
|
|
+ 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()
|
|
|
+
|
|
|
+ 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]
|
|
|
+ T2WidthExtractor.op_callsubr(self, index)
|
|
|
+ self.processSubr(index, subr)
|
|
|
+
|
|
|
+ def op_callgsubr(self, index):
|
|
|
+ subr = self.globalSubrs[self.operandStack[-1] + self.globalBias]
|
|
|
+ T2WidthExtractor.op_callgsubr(self, index)
|
|
|
+ self.processSubr(index, subr)
|
|
|
+
|
|
|
+ def op_hstem(self, index):
|
|
|
+ T2WidthExtractor.op_hstem(self, index)
|
|
|
+ self.processHint(index)
|
|
|
+
|
|
|
+ def op_vstem(self, index):
|
|
|
+ T2WidthExtractor.op_vstem(self, index)
|
|
|
+ self.processHint(index)
|
|
|
+
|
|
|
+ def op_hstemhm(self, index):
|
|
|
+ T2WidthExtractor.op_hstemhm(self, index)
|
|
|
+ self.processHint(index)
|
|
|
+
|
|
|
+ def op_vstemhm(self, index):
|
|
|
+ T2WidthExtractor.op_vstemhm(self, index)
|
|
|
+ self.processHint(index)
|
|
|
+
|
|
|
+ def op_hintmask(self, index):
|
|
|
+ rv = T2WidthExtractor.op_hintmask(self, index)
|
|
|
+ self.processHintmask(index)
|
|
|
+ return rv
|
|
|
+
|
|
|
+ def op_cntrmask(self, index):
|
|
|
+ rv = 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)
|
|
|
+
|
|
|
+
|
|
|
+def _cs_subset_subroutines(charstring, subrs, gsubrs):
|
|
|
+ p = charstring.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
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _cs_drop_hints(charstring):
|
|
|
+ hints = charstring._hints
|
|
|
+
|
|
|
+ if hints.deletions:
|
|
|
+ p = charstring.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]
|
|
|
+ charstring.program = charstring.program[hints.last_hint :]
|
|
|
+ if not charstring.program:
|
|
|
+ # TODO CFF2 no need for endchar.
|
|
|
+ charstring.program.append("endchar")
|
|
|
+ if hasattr(charstring, "width"):
|
|
|
+ # Insert width back if needed
|
|
|
+ if charstring.width != charstring.private.defaultWidthX:
|
|
|
+ # For CFF2 charstrings, this should never happen
|
|
|
+ assert (
|
|
|
+ charstring.private.defaultWidthX is not None
|
|
|
+ ), "CFF2 CharStrings must not have an initial width value"
|
|
|
+ charstring.program.insert(
|
|
|
+ 0, charstring.width - charstring.private.nominalWidthX
|
|
|
+ )
|
|
|
+
|
|
|
+ if hints.has_hintmask:
|
|
|
+ i = 0
|
|
|
+ p = charstring.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(charstring.program)
|
|
|
+
|
|
|
+ del charstring._hints
|
|
|
+
|
|
|
+
|
|
|
+def remove_hints(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 c in cs.values():
|
|
|
+ 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:
|
|
|
+ _cs_drop_hints(charstring)
|
|
|
+ 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)
|
|
|
+ remove_unused_subroutines(cff)
|
|
|
+
|
|
|
+
|
|
|
+def _pd_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
|
|
|
+
|
|
|
+
|
|
|
+def remove_unused_subroutines(cff):
|
|
|
+ for fontname in cff.keys():
|
|
|
+ font = cff[fontname]
|
|
|
+ cs = font.CharStrings
|
|
|
+ # Renumber subroutines to remove unused ones
|
|
|
+
|
|
|
+ # Mark all used subroutines
|
|
|
+ for c in cs.values():
|
|
|
+ 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 = calcSubrBias(subrs)
|
|
|
+ subrs._new_bias = calcSubrBias(subrs._used)
|
|
|
+
|
|
|
+ # Renumber glyph charstrings
|
|
|
+ for c in cs.values():
|
|
|
+ subrs = getattr(c.private, "Subrs", None)
|
|
|
+ _cs_subset_subroutines(c, 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:
|
|
|
+ _cs_subset_subroutines(subr, local_subrs, font.GlobalSubrs)
|
|
|
+
|
|
|
+ # Delete local SubrsIndex if empty
|
|
|
+ if hasattr(font, "FDArray"):
|
|
|
+ for fd in font.FDArray:
|
|
|
+ _pd_delete_empty_subrs(fd.Private)
|
|
|
+ else:
|
|
|
+ _pd_delete_empty_subrs(font.Private)
|
|
|
+
|
|
|
+ # Cleanup
|
|
|
+ for subrs in all_subrs:
|
|
|
+ del subrs._used, subrs._old_bias, subrs._new_bias
|