Spaces:
Runtime error
Runtime error
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) | |
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"] | |
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) | |
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()) | |
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 | |
) | |
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) | |
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 | |
def desubroutinize(self): | |
self.cff.desubroutinize() | |
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() | |
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 | |