Skip to content

Commit

Permalink
[cffLib] Add remove_hints() and remove_unused_subroutines() methods
Browse files Browse the repository at this point in the history
From subset.cff.
  • Loading branch information
behdad committed May 21, 2024
1 parent a851d02 commit d757bfa
Show file tree
Hide file tree
Showing 3 changed files with 389 additions and 361 deletions.
10 changes: 10 additions & 0 deletions Lib/fontTools/cffLib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,16 @@ def desubroutinize(self):

desubroutinize(self)

def remove_hints(self):
from .transforms import remove_hints

remove_hints(self)

def remove_unused_subroutines(self):
from .transforms import remove_unused_subroutines

remove_unused_subroutines(self)


class CFFWriter(object):
"""Helper class for serializing CFF data to binary. Used by
Expand Down
372 changes: 371 additions & 1 deletion Lib/fontTools/cffLib/transforms.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
from fontTools.misc.psCharStrings import SimpleT2Decompiler
from fontTools.misc.psCharStrings import (
SimpleT2Decompiler,
T2WidthExtractor,
calcSubrBias,
)


def _uniq_sort(l):
return sorted(set(l))


class StopHintCountEvent(Exception):
Expand Down Expand Up @@ -113,3 +121,365 @@ def desubroutinize(cff):
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 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:
_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 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 = calcSubrBias(subrs)
subrs._new_bias = calcSubrBias(subrs._used)

# Renumber glyph charstrings
for g in font.charset:
c, _ = cs.getItemAndSelector(g)
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
Loading

0 comments on commit d757bfa

Please sign in to comment.