Merge pull request #1377 from fonttools/varlib-interpolate-cff2
Varlib interpolate cff2
This commit is contained in:
commit
361e1e6aa0
@ -1151,6 +1151,13 @@ class T2CharString(object):
|
||||
class CFF2Subr(T2CharString):
|
||||
isCFF2 = True
|
||||
|
||||
def draw(self, pen):
|
||||
subrs = getattr(self.private, "Subrs", [])
|
||||
extractor = self.outlineExtractor(pen, subrs, self.globalSubrs,
|
||||
0, 0)
|
||||
extractor.execute(self)
|
||||
self.width = 0
|
||||
|
||||
class T1CharString(T2CharString):
|
||||
|
||||
operandEncoding = t1OperandEncoding
|
||||
|
@ -7,9 +7,9 @@ from fontTools.misc.py23 import *
|
||||
from fontTools.misc.fixedTools import otRound
|
||||
from fontTools import ttLib
|
||||
from fontTools.ttLib.tables import otTables
|
||||
from fontTools.misc import psCharStrings
|
||||
from fontTools.pens.basePen import NullPen
|
||||
from fontTools.misc.loggingTools import Timer
|
||||
from fontTools.subset.cff import *
|
||||
from fontTools.varLib import varStore
|
||||
import sys
|
||||
import struct
|
||||
@ -2081,522 +2081,6 @@ def prune_post_subset(self, font, options):
|
||||
return True
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@_add_method(ttLib.getTableClass('CFF '))
|
||||
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
|
||||
|
||||
@_add_method(ttLib.getTableClass('CFF '))
|
||||
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:
|
||||
for fontname in cff.keys():
|
||||
font = cff[fontname]
|
||||
c, fdSelectIndex = font.CharStrings.getItemAndSelector('.notdef')
|
||||
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']
|
||||
|
||||
# Clear useless Encoding
|
||||
for fontname in cff.keys():
|
||||
font = cff[fontname]
|
||||
# https://github.com/behdad/fonttools/issues/620
|
||||
font.Encoding = "StandardEncoding"
|
||||
|
||||
return True # bool(cff.fontNames)
|
||||
|
||||
@_add_method(ttLib.getTableClass('CFF '))
|
||||
def subset_glyphs(self, s):
|
||||
cff = self.cff
|
||||
for fontname in cff.keys():
|
||||
font = cff[fontname]
|
||||
cs = font.CharStrings
|
||||
|
||||
# Load all glyphs
|
||||
for g in font.charset:
|
||||
if g not in s.glyphs: continue
|
||||
c, _ = cs.getItemAndSelector(g)
|
||||
|
||||
if cs.charStringsAreIndexed:
|
||||
indices = [i for i,g in enumerate(font.charset) if g in s.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
|
||||
# XXX We want to set sel.format to None, such that the
|
||||
# most compact format is selected. However, OTS was
|
||||
# broken and couldn't parse a FDSelect format 0 that
|
||||
# happened before CharStrings. As such, always force
|
||||
# format 3 until we fix cffLib to always generate
|
||||
# FDSelect after CharStrings.
|
||||
# https://github.com/khaledhosny/ots/pull/31
|
||||
#sel.format = None
|
||||
sel.format = 3
|
||||
sel.gidArray = [sel.gidArray[i] for i in indices]
|
||||
cs.charStrings = {g:indices.index(v)
|
||||
for g,v in cs.charStrings.items()
|
||||
if g in s.glyphs}
|
||||
else:
|
||||
cs.charStrings = {g:v
|
||||
for g,v in cs.charStrings.items()
|
||||
if g in s.glyphs}
|
||||
font.charset = [g for g in font.charset if g in s.glyphs]
|
||||
font.numGlyphs = len(font.charset)
|
||||
|
||||
return True # any(cff[fontname].numGlyphs for fontname in cff.keys())
|
||||
|
||||
@_add_method(psCharStrings.T2CharString)
|
||||
def subset_subroutines(self, subrs, gsubrs):
|
||||
p = self.program
|
||||
assert len(p)
|
||||
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
|
||||
|
||||
@_add_method(psCharStrings.T2CharString)
|
||||
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:
|
||||
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):
|
||||
psCharStrings.SimpleT2Decompiler.__init__(self,
|
||||
localSubrs,
|
||||
globalSubrs)
|
||||
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):
|
||||
self._css = css
|
||||
psCharStrings.T2WidthExtractor.__init__(
|
||||
self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX)
|
||||
|
||||
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)
|
||||
|
||||
class _DesubroutinizingT2Decompiler(psCharStrings.SimpleT2Decompiler):
|
||||
|
||||
def __init__(self, localSubrs, globalSubrs):
|
||||
psCharStrings.SimpleT2Decompiler.__init__(self,
|
||||
localSubrs,
|
||||
globalSubrs)
|
||||
|
||||
def execute(self, charString):
|
||||
# Note: Currently we recompute _desubroutinized each time.
|
||||
# This is more robust in some cases, but in other places we assume
|
||||
# that each subroutine always expands to the same code, so
|
||||
# maybe it doesn't matter. To speed up we can just not
|
||||
# recompute _desubroutinized if it's there. For now I just
|
||||
# double-check that it desubroutinized to the same thing.
|
||||
old_desubroutinized = charString._desubroutinized if hasattr(charString, '_desubroutinized') else None
|
||||
|
||||
charString._patches = []
|
||||
psCharStrings.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 'endchar' in desubroutinized:
|
||||
# Cut off after first endchar
|
||||
desubroutinized = desubroutinized[:desubroutinized.index('endchar') + 1]
|
||||
else:
|
||||
if not len(desubroutinized) or desubroutinized[-1] != 'return':
|
||||
desubroutinized.append('return')
|
||||
|
||||
charString._desubroutinized = desubroutinized
|
||||
del charString._patches
|
||||
|
||||
if old_desubroutinized:
|
||||
assert desubroutinized == old_desubroutinized
|
||||
|
||||
def op_callsubr(self, index):
|
||||
subr = self.localSubrs[self.operandStack[-1]+self.localBias]
|
||||
psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
|
||||
self.processSubr(index, subr)
|
||||
|
||||
def op_callgsubr(self, index):
|
||||
subr = self.globalSubrs[self.operandStack[-1]+self.globalBias]
|
||||
psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
|
||||
self.processSubr(index, subr)
|
||||
|
||||
def processSubr(self, index, subr):
|
||||
cs = self.callingStack[-1]
|
||||
cs._patches.append((index, subr._desubroutinized))
|
||||
|
||||
|
||||
@_add_method(ttLib.getTableClass('CFF '))
|
||||
def prune_post_subset(self, font, 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:
|
||||
for g in font.charset:
|
||||
c, _ = cs.getItemAndSelector(g)
|
||||
c.decompile()
|
||||
subrs = getattr(c.private, "Subrs", [])
|
||||
decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs)
|
||||
decompiler.execute(c)
|
||||
c.program = c._desubroutinized
|
||||
|
||||
# Drop hints if not needed
|
||||
if not options.hinting:
|
||||
|
||||
# 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)
|
||||
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, 'FDSelect'):
|
||||
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)
|
||||
|
||||
# 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)
|
||||
decompiler.execute(c)
|
||||
|
||||
all_subrs = [font.GlobalSubrs]
|
||||
if hasattr(font, 'FDSelect'):
|
||||
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, 'FDSelect') 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, 'FDSelect'):
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
@_add_method(ttLib.getTableClass('cmap'))
|
||||
def closure_glyphs(self, s):
|
||||
tables = [t for t in self.tables if t.isUnicode()]
|
||||
|
559
Lib/fontTools/subset/cff.py
Normal file
559
Lib/fontTools/subset/cff.py
Normal file
@ -0,0 +1,559 @@
|
||||
from fontTools.misc import psCharStrings
|
||||
from fontTools import ttLib
|
||||
from fontTools.pens.basePen import NullPen
|
||||
from fontTools.misc.fixedTools import otRound
|
||||
from fontTools.varLib.varStore import VarStoreInstancer
|
||||
|
||||
def _add_method(*clazzes):
|
||||
"""Returns a decorator function that adds a new method to one or
|
||||
more classes."""
|
||||
def wrapper(method):
|
||||
done = []
|
||||
for clazz in clazzes:
|
||||
if clazz in done: continue # Support multiple names of a clazz
|
||||
done.append(clazz)
|
||||
assert clazz.__name__ != 'DefaultTable', \
|
||||
'Oops, table class not found.'
|
||||
assert not hasattr(clazz, method.__name__), \
|
||||
"Oops, class '%s' has method '%s'." % (clazz.__name__,
|
||||
method.__name__)
|
||||
setattr(clazz, method.__name__, method)
|
||||
return None
|
||||
return wrapper
|
||||
|
||||
def _uniq_sort(l):
|
||||
return sorted(set(l))
|
||||
|
||||
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)
|
||||
|
||||
@_add_method(ttLib.getTableClass('CFF '))
|
||||
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
|
||||
|
||||
@_add_method(ttLib.getTableClass('CFF '))
|
||||
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:
|
||||
for fontname in cff.keys():
|
||||
font = cff[fontname]
|
||||
c, fdSelectIndex = font.CharStrings.getItemAndSelector('.notdef')
|
||||
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']
|
||||
|
||||
# Clear useless Encoding
|
||||
for fontname in cff.keys():
|
||||
font = cff[fontname]
|
||||
# https://github.com/behdad/fonttools/issues/620
|
||||
font.Encoding = "StandardEncoding"
|
||||
|
||||
return True # bool(cff.fontNames)
|
||||
|
||||
@_add_method(ttLib.getTableClass('CFF '))
|
||||
def subset_glyphs(self, s):
|
||||
cff = self.cff
|
||||
for fontname in cff.keys():
|
||||
font = cff[fontname]
|
||||
cs = font.CharStrings
|
||||
|
||||
# Load all glyphs
|
||||
for g in font.charset:
|
||||
if g not in s.glyphs: continue
|
||||
c, _ = cs.getItemAndSelector(g)
|
||||
|
||||
if cs.charStringsAreIndexed:
|
||||
indices = [i for i,g in enumerate(font.charset) if g in s.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
|
||||
# XXX We want to set sel.format to None, such that the
|
||||
# most compact format is selected. However, OTS was
|
||||
# broken and couldn't parse a FDSelect format 0 that
|
||||
# happened before CharStrings. As such, always force
|
||||
# format 3 until we fix cffLib to always generate
|
||||
# FDSelect after CharStrings.
|
||||
# https://github.com/khaledhosny/ots/pull/31
|
||||
#sel.format = None
|
||||
sel.format = 3
|
||||
sel.gidArray = [sel.gidArray[i] for i in indices]
|
||||
cs.charStrings = {g:indices.index(v)
|
||||
for g,v in cs.charStrings.items()
|
||||
if g in s.glyphs}
|
||||
else:
|
||||
cs.charStrings = {g:v
|
||||
for g,v in cs.charStrings.items()
|
||||
if g in s.glyphs}
|
||||
font.charset = [g for g in font.charset if g in s.glyphs]
|
||||
font.numGlyphs = len(font.charset)
|
||||
|
||||
return True # any(cff[fontname].numGlyphs for fontname in cff.keys())
|
||||
|
||||
@_add_method(psCharStrings.T2CharString)
|
||||
def subset_subroutines(self, subrs, gsubrs):
|
||||
p = self.program
|
||||
assert len(p)
|
||||
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
|
||||
|
||||
@_add_method(psCharStrings.T2CharString)
|
||||
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:
|
||||
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):
|
||||
psCharStrings.SimpleT2Decompiler.__init__(self,
|
||||
localSubrs,
|
||||
globalSubrs)
|
||||
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):
|
||||
self._css = css
|
||||
psCharStrings.T2WidthExtractor.__init__(
|
||||
self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX)
|
||||
|
||||
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)
|
||||
|
||||
class _DesubroutinizingT2Decompiler(psCharStrings.SimpleT2Decompiler):
|
||||
|
||||
def __init__(self, localSubrs, globalSubrs, private=None):
|
||||
psCharStrings.SimpleT2Decompiler.__init__(self,
|
||||
localSubrs,
|
||||
globalSubrs, private)
|
||||
|
||||
def execute(self, charString):
|
||||
if hasattr(charString, '_desubroutinized'):
|
||||
return
|
||||
|
||||
charString._patches = []
|
||||
psCharStrings.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 charString.isCFF2:
|
||||
if 'endchar' in desubroutinized:
|
||||
# Cut off after first endchar
|
||||
desubroutinized = desubroutinized[:desubroutinized.index('endchar') + 1]
|
||||
else:
|
||||
if not len(desubroutinized) or desubroutinized[-1] != 'return':
|
||||
desubroutinized.append('return')
|
||||
|
||||
charString._desubroutinized = desubroutinized
|
||||
del charString._patches
|
||||
|
||||
def op_callsubr(self, index):
|
||||
subr = self.localSubrs[self.operandStack[-1]+self.localBias]
|
||||
psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
|
||||
self.processSubr(index, subr)
|
||||
|
||||
def op_callgsubr(self, index):
|
||||
subr = self.globalSubrs[self.operandStack[-1]+self.globalBias]
|
||||
psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
|
||||
self.processSubr(index, subr)
|
||||
|
||||
def processSubr(self, index, subr):
|
||||
cs = self.callingStack[-1]
|
||||
cs._patches.append((index, subr._desubroutinized))
|
||||
|
||||
|
||||
@_add_method(ttLib.getTableClass('CFF '))
|
||||
def prune_post_subset(self, font, 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:
|
||||
for g in font.charset:
|
||||
c, _ = cs.getItemAndSelector(g)
|
||||
c.decompile()
|
||||
subrs = getattr(c.private, "Subrs", [])
|
||||
decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs)
|
||||
decompiler.execute(c)
|
||||
c.program = c._desubroutinized
|
||||
del c._desubroutinized
|
||||
|
||||
# Drop hints if not needed
|
||||
if not options.hinting:
|
||||
|
||||
# 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)
|
||||
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, 'FDSelect'):
|
||||
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)
|
||||
|
||||
# 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)
|
||||
decompiler.execute(c)
|
||||
|
||||
all_subrs = [font.GlobalSubrs]
|
||||
if hasattr(font, 'FDSelect'):
|
||||
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, 'FDSelect') 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, 'FDSelect'):
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@_add_method(ttLib.getTableClass('CFF2'))
|
||||
def desubroutinize(self, font):
|
||||
cff = self.cff
|
||||
for fontname in cff.keys():
|
||||
font = cff[fontname]
|
||||
cs = font.CharStrings
|
||||
for g in font.charset:
|
||||
c, _ = cs.getItemAndSelector(g)
|
||||
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 Subrs!!!
|
||||
if font.GlobalSubrs:
|
||||
del font.GlobalSubrs
|
||||
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']
|
@ -6,17 +6,21 @@ $ fonttools varLib.mutator ./NotoSansArabic-VF.ttf wght=140 wdth=85
|
||||
from __future__ import print_function, division, absolute_import
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.misc.fixedTools import floatToFixedToFloat, otRound, floatToFixed
|
||||
from fontTools.pens.boundsPen import BoundsPen
|
||||
from fontTools.ttLib import TTFont, newTable
|
||||
from fontTools.ttLib.tables import ttProgram
|
||||
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
|
||||
from fontTools.varLib import _GetCoordinates, _SetCoordinates
|
||||
from fontTools.varLib.models import (
|
||||
supportScalar, normalizeLocation, piecewiseLinearMap
|
||||
supportScalar,
|
||||
normalizeLocation,
|
||||
piecewiseLinearMap,
|
||||
)
|
||||
from fontTools.varLib.merger import MutatorMerger
|
||||
from fontTools.varLib.varStore import VarStoreInstancer
|
||||
from fontTools.varLib.mvar import MVAR_ENTRIES
|
||||
from fontTools.varLib.iup import iup_delta
|
||||
import fontTools.subset.cff
|
||||
import os.path
|
||||
import logging
|
||||
|
||||
@ -31,6 +35,116 @@ for i, (prev, curr) in enumerate(zip(percents[:-1], percents[1:]), start=1):
|
||||
OS2_WIDTH_CLASS_VALUES[half] = i
|
||||
|
||||
|
||||
def interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas):
|
||||
pd_blend_lists = ("BlueValues", "OtherBlues", "FamilyBlues",
|
||||
"FamilyOtherBlues", "StemSnapH",
|
||||
"StemSnapV")
|
||||
pd_blend_values = ("BlueScale", "BlueShift",
|
||||
"BlueFuzz", "StdHW", "StdVW")
|
||||
for fontDict in topDict.FDArray:
|
||||
pd = fontDict.Private
|
||||
vsindex = pd.vsindex if (hasattr(pd, 'vsindex')) else 0
|
||||
for key, value in pd.rawDict.items():
|
||||
if (key in pd_blend_values) and isinstance(value, list):
|
||||
delta = interpolateFromDeltas(vsindex, value[1:])
|
||||
pd.rawDict[key] = otRound(value[0] + delta)
|
||||
elif (key in pd_blend_lists) and isinstance(value[0], list):
|
||||
"""If any argument in a BlueValues list is a blend list,
|
||||
then they all are. The first value of each list is an
|
||||
absolute value. The delta tuples are calculated from
|
||||
relative master values, hence we need to append all the
|
||||
deltas to date to each successive absolute value."""
|
||||
delta = 0
|
||||
for i, val_list in enumerate(value):
|
||||
delta += otRound(interpolateFromDeltas(vsindex,
|
||||
val_list[1:]))
|
||||
value[i] = val_list[0] + delta
|
||||
|
||||
|
||||
def interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder):
|
||||
charstrings = topDict.CharStrings
|
||||
for gname in glyphOrder:
|
||||
# Interpolate charstring
|
||||
charstring = charstrings[gname]
|
||||
pd = charstring.private
|
||||
vsindex = pd.vsindex if (hasattr(pd, 'vsindex')) else 0
|
||||
num_regions = pd.getNumRegions(vsindex)
|
||||
numMasters = num_regions + 1
|
||||
new_program = []
|
||||
last_i = 0
|
||||
for i, token in enumerate(charstring.program):
|
||||
if token == 'blend':
|
||||
num_args = charstring.program[i - 1]
|
||||
""" The stack is now:
|
||||
..args for following operations
|
||||
num_args values from the default font
|
||||
num_args tuples, each with numMasters-1 delta values
|
||||
num_blend_args
|
||||
'blend'
|
||||
"""
|
||||
argi = i - (num_args*numMasters + 1)
|
||||
end_args = tuplei = argi + num_args
|
||||
while argi < end_args:
|
||||
next_ti = tuplei + num_regions
|
||||
deltas = charstring.program[tuplei:next_ti]
|
||||
delta = interpolateFromDeltas(vsindex, deltas)
|
||||
charstring.program[argi] += otRound(delta)
|
||||
tuplei = next_ti
|
||||
argi += 1
|
||||
new_program.extend(charstring.program[last_i:end_args])
|
||||
last_i = i + 1
|
||||
if last_i != 0:
|
||||
new_program.extend(charstring.program[last_i:])
|
||||
charstring.program = new_program
|
||||
|
||||
|
||||
def interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc):
|
||||
"""Unlike TrueType glyphs, neither advance width nor bounding box
|
||||
info is stored in a CFF2 charstring. The width data exists only in
|
||||
the hmtx and HVAR tables. Since LSB data cannot be interpolated
|
||||
reliably from the master LSB values in the hmtx table, we traverse
|
||||
the charstring to determine the actual bound box. """
|
||||
|
||||
charstrings = topDict.CharStrings
|
||||
boundsPen = BoundsPen(glyphOrder)
|
||||
hmtx = varfont['hmtx']
|
||||
hvar_table = None
|
||||
if 'HVAR' in varfont:
|
||||
hvar_table = varfont['HVAR'].table
|
||||
fvar = varfont['fvar']
|
||||
varStoreInstancer = VarStoreInstancer(hvar_table.VarStore, fvar.axes, loc)
|
||||
|
||||
for gid, gname in enumerate(glyphOrder):
|
||||
entry = list(hmtx[gname])
|
||||
# get width delta.
|
||||
if hvar_table:
|
||||
if hvar_table.AdvWidthMap:
|
||||
width_idx = hvar_table.AdvWidthMap.mapping[gname]
|
||||
else:
|
||||
width_idx = gid
|
||||
width_delta = otRound(varStoreInstancer[width_idx])
|
||||
else:
|
||||
width_delta = 0
|
||||
|
||||
# get LSB.
|
||||
boundsPen.init()
|
||||
charstring = charstrings[gname]
|
||||
charstring.draw(boundsPen)
|
||||
if boundsPen.bounds is None:
|
||||
# Happens with non-marking glyphs
|
||||
lsb_delta = 0
|
||||
else:
|
||||
lsb = boundsPen.bounds[0]
|
||||
lsb_delta = entry[1] - lsb
|
||||
|
||||
if lsb_delta or width_delta:
|
||||
if width_delta:
|
||||
entry[0] += width_delta
|
||||
if lsb_delta:
|
||||
entry[1] = lsb
|
||||
hmtx[gname] = tuple(entry)
|
||||
|
||||
|
||||
def instantiateVariableFont(varfont, location, inplace=False):
|
||||
""" Generate a static instance from a variable TTFont and a dictionary
|
||||
defining the desired location along the variable font's axes.
|
||||
@ -59,31 +173,34 @@ def instantiateVariableFont(varfont, location, inplace=False):
|
||||
# Location is normalized now
|
||||
log.info("Normalized location: %s", loc)
|
||||
|
||||
log.info("Mutating glyf/gvar tables")
|
||||
gvar = varfont['gvar']
|
||||
glyf = varfont['glyf']
|
||||
# get list of glyph names in gvar sorted by component depth
|
||||
glyphnames = sorted(
|
||||
gvar.variations.keys(),
|
||||
key=lambda name: (
|
||||
glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
|
||||
if glyf[name].isComposite() else 0,
|
||||
name))
|
||||
for glyphname in glyphnames:
|
||||
variations = gvar.variations[glyphname]
|
||||
coordinates,_ = _GetCoordinates(varfont, glyphname)
|
||||
origCoords, endPts = None, None
|
||||
for var in variations:
|
||||
scalar = supportScalar(loc, var.axes)
|
||||
if not scalar: continue
|
||||
delta = var.coordinates
|
||||
if None in delta:
|
||||
if origCoords is None:
|
||||
origCoords,control = _GetCoordinates(varfont, glyphname)
|
||||
endPts = control[1] if control[0] >= 1 else list(range(len(control[1])))
|
||||
delta = iup_delta(delta, origCoords, endPts)
|
||||
coordinates += GlyphCoordinates(delta) * scalar
|
||||
_SetCoordinates(varfont, glyphname, coordinates)
|
||||
if 'gvar' in varfont:
|
||||
log.info("Mutating glyf/gvar tables")
|
||||
gvar = varfont['gvar']
|
||||
glyf = varfont['glyf']
|
||||
# get list of glyph names in gvar sorted by component depth
|
||||
glyphnames = sorted(
|
||||
gvar.variations.keys(),
|
||||
key=lambda name: (
|
||||
glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
|
||||
if glyf[name].isComposite() else 0,
|
||||
name))
|
||||
for glyphname in glyphnames:
|
||||
variations = gvar.variations[glyphname]
|
||||
coordinates,_ = _GetCoordinates(varfont, glyphname)
|
||||
origCoords, endPts = None, None
|
||||
for var in variations:
|
||||
scalar = supportScalar(loc, var.axes)
|
||||
if not scalar: continue
|
||||
delta = var.coordinates
|
||||
if None in delta:
|
||||
if origCoords is None:
|
||||
origCoords,control = _GetCoordinates(varfont, glyphname)
|
||||
endPts = control[1] if control[0] >= 1 else list(range(len(control[1])))
|
||||
delta = iup_delta(delta, origCoords, endPts)
|
||||
coordinates += GlyphCoordinates(delta) * scalar
|
||||
_SetCoordinates(varfont, glyphname, coordinates)
|
||||
else:
|
||||
glyf = None
|
||||
|
||||
if 'cvar' in varfont:
|
||||
log.info("Mutating cvt/cvar tables")
|
||||
@ -99,6 +216,20 @@ def instantiateVariableFont(varfont, location, inplace=False):
|
||||
for i, delta in deltas.items():
|
||||
cvt[i] += otRound(delta)
|
||||
|
||||
if 'CFF2' in varfont:
|
||||
log.info("Mutating CFF2 table")
|
||||
glyphOrder = varfont.getGlyphOrder()
|
||||
CFF2 = varfont['CFF2']
|
||||
topDict = CFF2.cff.topDictIndex[0]
|
||||
vsInstancer = VarStoreInstancer(topDict.VarStore.otVarStore, fvar.axes, loc)
|
||||
interpolateFromDeltas = vsInstancer.interpolateFromDeltas
|
||||
interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas)
|
||||
CFF2.desubroutinize(varfont)
|
||||
interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder)
|
||||
interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc)
|
||||
del topDict.rawDict['VarStore']
|
||||
del topDict.VarStore
|
||||
|
||||
if 'MVAR' in varfont:
|
||||
log.info("Mutating MVAR table")
|
||||
mvar = varfont['MVAR'].table
|
||||
@ -168,15 +299,15 @@ def instantiateVariableFont(varfont, location, inplace=False):
|
||||
(gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)):
|
||||
del varfont['GDEF']
|
||||
|
||||
|
||||
addidef = False
|
||||
for glyph in glyf.glyphs.values():
|
||||
if hasattr(glyph, "program"):
|
||||
instructions = glyph.program.getAssembly()
|
||||
# If GETVARIATION opcode is used in bytecode of any glyph add IDEF
|
||||
addidef = any(op.startswith("GETVARIATION") for op in instructions)
|
||||
if addidef:
|
||||
break
|
||||
if glyf:
|
||||
for glyph in glyf.glyphs.values():
|
||||
if hasattr(glyph, "program"):
|
||||
instructions = glyph.program.getAssembly()
|
||||
# If GETVARIATION opcode is used in bytecode of any glyph add IDEF
|
||||
addidef = any(op.startswith("GETVARIATION") for op in instructions)
|
||||
if addidef:
|
||||
break
|
||||
if addidef:
|
||||
log.info("Adding IDEF to fpgm table for GETVARIATION opcode")
|
||||
asm = []
|
||||
|
@ -162,19 +162,27 @@ class VarStoreInstancer(object):
|
||||
self._scalars[regionIdx] = scalar
|
||||
return scalar
|
||||
|
||||
def __getitem__(self, varidx):
|
||||
|
||||
major, minor = varidx >> 16, varidx & 0xFFFF
|
||||
|
||||
varData = self._varData
|
||||
scalars = [self._getScalar(ri) for ri in varData[major].VarRegionIndex]
|
||||
|
||||
deltas = varData[major].Item[minor]
|
||||
@staticmethod
|
||||
def interpolateFromDeltasAndScalars(deltas, scalars):
|
||||
delta = 0.
|
||||
for d,s in zip(deltas, scalars):
|
||||
if not s: continue
|
||||
delta += d * s
|
||||
return delta
|
||||
|
||||
def __getitem__(self, varidx):
|
||||
major, minor = varidx >> 16, varidx & 0xFFFF
|
||||
varData = self._varData
|
||||
scalars = [self._getScalar(ri) for ri in varData[major].VarRegionIndex]
|
||||
deltas = varData[major].Item[minor]
|
||||
return self.interpolateFromDeltasAndScalars(deltas, scalars)
|
||||
|
||||
def interpolateFromDeltas(self, varDataIndex, deltas):
|
||||
varData = self._varData
|
||||
scalars = [self._getScalar(ri) for ri in
|
||||
varData[varDataIndex].VarRegionIndex]
|
||||
return self.interpolateFromDeltasAndScalars(deltas, scalars)
|
||||
|
||||
|
||||
#
|
||||
# Optimizations
|
||||
|
BIN
Tests/varLib/data/TestCFF2VF.otf
Normal file
BIN
Tests/varLib/data/TestCFF2VF.otf
Normal file
Binary file not shown.
107
Tests/varLib/data/test_results/InterpolateTestCFF2VF.ttx
Normal file
107
Tests/varLib/data/test_results/InterpolateTestCFF2VF.ttx
Normal file
@ -0,0 +1,107 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ttFont sfntVersion="OTTO" ttLibVersion="3.32">
|
||||
|
||||
<hmtx>
|
||||
<mtx name=".notdef" width="600" lsb="84"/>
|
||||
<mtx name="A" width="600" lsb="50"/>
|
||||
<mtx name="T" width="600" lsb="50"/>
|
||||
<mtx name="dollar" width="600" lsb="102"/>
|
||||
<mtx name="glyph00003" width="600" lsb="102"/>
|
||||
</hmtx>
|
||||
|
||||
<CFF2>
|
||||
<major value="2"/>
|
||||
<minor value="0"/>
|
||||
<CFFFont name="CFF2Font">
|
||||
<FontMatrix value="0.001 0 0 0.001 0 0"/>
|
||||
<FDArray>
|
||||
<FontDict index="0">
|
||||
<Private>
|
||||
<BlueValues value="-12 0 478 490 570 582 640 652 660 672 722 734"/>
|
||||
<OtherBlues value="-234 -222"/>
|
||||
<BlueScale value="0.0625"/>
|
||||
<BlueShift value="7"/>
|
||||
<BlueFuzz value="0"/>
|
||||
<StdHW value="28"/>
|
||||
<StdVW value="34"/>
|
||||
</Private>
|
||||
</FontDict>
|
||||
</FDArray>
|
||||
<CharStrings>
|
||||
<CharString name=".notdef">
|
||||
84 hmoveto
|
||||
432 660 -432 hlineto
|
||||
48 -628 rmoveto
|
||||
102 176 64 106 rlineto
|
||||
4 hlineto
|
||||
62 -106 100 -176 rlineto
|
||||
-342 42 rmoveto
|
||||
536 vlineto
|
||||
154 -270 rlineto
|
||||
22 26 rmoveto
|
||||
-56 92 -94 168 rlineto
|
||||
302 hlineto
|
||||
-94 -168 -54 -92 rlineto
|
||||
22 -26 rmoveto
|
||||
152 270 rlineto
|
||||
-536 vlineto
|
||||
</CharString>
|
||||
<CharString name="A">
|
||||
50 hmoveto
|
||||
32 hlineto
|
||||
140 396 28 80 24 68 24 82 rlinecurve
|
||||
4 hlineto
|
||||
24 -82 24 -68 28 -80 138 -396 rcurveline
|
||||
34 hlineto
|
||||
-236 660 rlineto
|
||||
-28 hlineto
|
||||
-134 -424 rmoveto
|
||||
293 28 -293 hlineto
|
||||
</CharString>
|
||||
<CharString name="T">
|
||||
284 hmoveto
|
||||
32 632 234 28 -500 -28 234 hlineto
|
||||
</CharString>
|
||||
<CharString name="dollar">
|
||||
311 34 rmoveto
|
||||
103 88 56 94 hvcurveto
|
||||
184 -338 -32 142 vvcurveto
|
||||
68 57 44 85 76 34 -24 -38 44 vhcurveto
|
||||
20 20 rlineto
|
||||
38 -41 -45 32 -85 hhcurveto
|
||||
-99 -78 -54 -88 hvcurveto
|
||||
-166 338 28 -156 vvcurveto
|
||||
-70 -56 -50 -103 -85 -66 38 34 -40 vhcurveto
|
||||
-18 -22 45 -38 73 -40 91 0 rlinecurve
|
||||
-18 566 rmoveto
|
||||
30 hlineto
|
||||
50 0 50 50 vvcurveto
|
||||
-30 hlineto
|
||||
-50 0 -50 -50 vvcurveto
|
||||
-562 vmoveto
|
||||
-148 30 148 vlineto
|
||||
</CharString>
|
||||
<CharString name="glyph00003">
|
||||
311 34 rmoveto
|
||||
103 88 56 94 hvcurveto
|
||||
184 -338 -32 142 vvcurveto
|
||||
68 57 44 85 76 34 -24 -38 44 vhcurveto
|
||||
20 20 rlineto
|
||||
38 -41 -45 32 -85 hhcurveto
|
||||
-99 -78 -54 -88 hvcurveto
|
||||
-166 338 28 -156 vvcurveto
|
||||
-70 -56 -50 -103 -85 -66 38 34 -40 vhcurveto
|
||||
-18 -22 rlineto
|
||||
-38 45 73 -40 91 hhcurveto
|
||||
-70 -146 rmoveto
|
||||
158 860 -30 4 -158 -860 rlineto
|
||||
</CharString>
|
||||
</CharStrings>
|
||||
</CFFFont>
|
||||
|
||||
<GlobalSubrs>
|
||||
<!-- The 'index' attribute is only for humans; it is ignored when parsed. -->
|
||||
</GlobalSubrs>
|
||||
</CFF2>
|
||||
|
||||
</ttFont>
|
@ -3,6 +3,7 @@ from fontTools.misc.py23 import *
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.varLib import build
|
||||
from fontTools.varLib.mutator import main as mutator
|
||||
from fontTools.varLib.mutator import instantiateVariableFont as make_instance
|
||||
import difflib
|
||||
import os
|
||||
import shutil
|
||||
@ -160,6 +161,18 @@ class MutatorTest(unittest.TestCase):
|
||||
expected_ttx_path = self.get_test_output(varfont_name + '-instance.ttx')
|
||||
self.expect_ttx(instfont, expected_ttx_path, tables)
|
||||
|
||||
def test_varlib_mutator_CFF2(self):
|
||||
|
||||
otf_vf_path = self.get_test_input('TestCFF2VF.otf')
|
||||
expected_ttx_name = 'InterpolateTestCFF2VF'
|
||||
tables = ["hmtx", "CFF2"]
|
||||
loc = {'wght':float(200)}
|
||||
|
||||
varfont = TTFont(otf_vf_path)
|
||||
new_font = make_instance(varfont, loc)
|
||||
expected_ttx_path = self.get_test_output(expected_ttx_name + '.ttx')
|
||||
self.expect_ttx(new_font, expected_ttx_path, tables)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(unittest.main())
|
||||
|
Loading…
x
Reference in New Issue
Block a user