The transforms.py changes are for them to work with fonts without font.charset. I'm not sure how we didn't catch these before.
483 lines
17 KiB
Python
483 lines
17 KiB
Python
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
|