|
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 |
|
for op_name in self.stop_hintcount_ops: |
|
setattr(self, op_name, self.stop_hint_count) |
|
|
|
if hasattr(charString, "_desubroutinized"): |
|
|
|
|
|
|
|
|
|
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: |
|
|
|
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 |
|
|
|
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"] |
|
|
|
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): |
|
|
|
|
|
|
|
|
|
self.has_hint = False |
|
|
|
self.last_hint = 0 |
|
|
|
|
|
self.last_checked = 0 |
|
|
|
|
|
|
|
|
|
|
|
|
|
self.status = 0 |
|
|
|
self.has_hintmask = False |
|
|
|
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: |
|
|
|
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 |
|
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: |
|
|
|
for i in range(hints.last_checked, index - 1): |
|
if isinstance(cs.program[i], str): |
|
hints.status = 2 |
|
break |
|
else: |
|
|
|
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 |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
if subr_hints.status == 0: |
|
hints.last_hint = index |
|
else: |
|
hints.last_hint = index - 2 |
|
|
|
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: |
|
|
|
charstring.program.append("endchar") |
|
if hasattr(charstring, "width"): |
|
|
|
if charstring.width != charstring.private.defaultWidthX: |
|
|
|
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, *, removeUnusedSubrs: bool = True): |
|
for fontname in cff.keys(): |
|
font = cff[fontname] |
|
cs = font.CharStrings |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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) |
|
if removeUnusedSubrs: |
|
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 |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
for c in cs.values(): |
|
subrs = getattr(c.private, "Subrs", None) |
|
_cs_subset_subroutines(c, subrs, font.GlobalSubrs) |
|
|
|
|
|
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) |
|
|
|
|
|
if hasattr(font, "FDArray"): |
|
for fd in font.FDArray: |
|
_pd_delete_empty_subrs(fd.Private) |
|
else: |
|
_pd_delete_empty_subrs(font.Private) |
|
|
|
|
|
for subrs in all_subrs: |
|
del subrs._used, subrs._old_bias, subrs._new_bias |
|
|