ajout fichiers manquant
This commit is contained in:
461
venv/Lib/site-packages/fontTools/varLib/mutator.py
Normal file
461
venv/Lib/site-packages/fontTools/varLib/mutator.py
Normal file
@@ -0,0 +1,461 @@
|
||||
"""
|
||||
Instantiate a variation font. Run, eg:
|
||||
|
||||
$ fonttools varLib.mutator ./NotoSansArabic-VF.ttf wght=140 wdth=85
|
||||
"""
|
||||
from fontTools.misc.fixedTools import floatToFixedToFloat, floatToFixed
|
||||
from fontTools.misc.roundTools import otRound
|
||||
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, flagOverlapSimple, OVERLAP_COMPOUND
|
||||
from fontTools.varLib.models import (
|
||||
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
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
log = logging.getLogger("fontTools.varlib.mutator")
|
||||
|
||||
# map 'wdth' axis (1..200) to OS/2.usWidthClass (1..9), rounding to closest
|
||||
OS2_WIDTH_CLASS_VALUES = {}
|
||||
percents = [50.0, 62.5, 75.0, 87.5, 100.0, 112.5, 125.0, 150.0, 200.0]
|
||||
for i, (prev, curr) in enumerate(zip(percents[:-1], percents[1:]), start=1):
|
||||
half = (prev + curr) / 2
|
||||
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
|
||||
# e.g replace blend op args with regular args,
|
||||
# and use and discard vsindex op.
|
||||
charstring = charstrings[gname]
|
||||
new_program = []
|
||||
vsindex = 0
|
||||
last_i = 0
|
||||
for i, token in enumerate(charstring.program):
|
||||
if token == 'vsindex':
|
||||
vsindex = charstring.program[i - 1]
|
||||
if last_i != 0:
|
||||
new_program.extend(charstring.program[last_i:i - 1])
|
||||
last_i = i + 1
|
||||
elif token == 'blend':
|
||||
num_regions = charstring.getNumRegions(vsindex)
|
||||
numMasters = 1 + num_regions
|
||||
num_args = charstring.program[i - 1]
|
||||
# The program list starting at program[i] 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 = otRound(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, overlap=True):
|
||||
""" Generate a static instance from a variable TTFont and a dictionary
|
||||
defining the desired location along the variable font's axes.
|
||||
The location values must be specified as user-space coordinates, e.g.:
|
||||
|
||||
{'wght': 400, 'wdth': 100}
|
||||
|
||||
By default, a new TTFont object is returned. If ``inplace`` is True, the
|
||||
input varfont is modified and reduced to a static font.
|
||||
|
||||
When the overlap parameter is defined as True,
|
||||
OVERLAP_SIMPLE and OVERLAP_COMPOUND bits are set to 1. See
|
||||
https://docs.microsoft.com/en-us/typography/opentype/spec/glyf
|
||||
"""
|
||||
if not inplace:
|
||||
# make a copy to leave input varfont unmodified
|
||||
stream = BytesIO()
|
||||
varfont.save(stream)
|
||||
stream.seek(0)
|
||||
varfont = TTFont(stream)
|
||||
|
||||
fvar = varfont['fvar']
|
||||
axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes}
|
||||
loc = normalizeLocation(location, axes)
|
||||
if 'avar' in varfont:
|
||||
maps = varfont['avar'].segments
|
||||
loc = {k: piecewiseLinearMap(v, maps[k]) for k,v in loc.items()}
|
||||
# Quantize to F2Dot14, to avoid surprise interpolations.
|
||||
loc = {k:floatToFixedToFloat(v, 14) for k,v in loc.items()}
|
||||
# Location is normalized now
|
||||
log.info("Normalized location: %s", loc)
|
||||
|
||||
if 'gvar' in varfont:
|
||||
log.info("Mutating glyf/gvar tables")
|
||||
gvar = varfont['gvar']
|
||||
glyf = varfont['glyf']
|
||||
hMetrics = varfont['hmtx'].metrics
|
||||
vMetrics = getattr(varfont.get('vmtx'), 'metrics', None)
|
||||
# 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, _ = glyf._getCoordinatesAndControls(glyphname, hMetrics, vMetrics)
|
||||
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, g = glyf._getCoordinatesAndControls(glyphname, hMetrics, vMetrics)
|
||||
delta = iup_delta(delta, origCoords, g.endPts)
|
||||
coordinates += GlyphCoordinates(delta) * scalar
|
||||
glyf._setCoordinates(glyphname, coordinates, hMetrics, vMetrics)
|
||||
else:
|
||||
glyf = None
|
||||
|
||||
if 'cvar' in varfont:
|
||||
log.info("Mutating cvt/cvar tables")
|
||||
cvar = varfont['cvar']
|
||||
cvt = varfont['cvt ']
|
||||
deltas = {}
|
||||
for var in cvar.variations:
|
||||
scalar = supportScalar(loc, var.axes)
|
||||
if not scalar: continue
|
||||
for i, c in enumerate(var.coordinates):
|
||||
if c is not None:
|
||||
deltas[i] = deltas.get(i, 0) + scalar * c
|
||||
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()
|
||||
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
|
||||
varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, loc)
|
||||
records = mvar.ValueRecord
|
||||
for rec in records:
|
||||
mvarTag = rec.ValueTag
|
||||
if mvarTag not in MVAR_ENTRIES:
|
||||
continue
|
||||
tableTag, itemName = MVAR_ENTRIES[mvarTag]
|
||||
delta = otRound(varStoreInstancer[rec.VarIdx])
|
||||
if not delta:
|
||||
continue
|
||||
setattr(varfont[tableTag], itemName,
|
||||
getattr(varfont[tableTag], itemName) + delta)
|
||||
|
||||
log.info("Mutating FeatureVariations")
|
||||
for tableTag in 'GSUB','GPOS':
|
||||
if not tableTag in varfont:
|
||||
continue
|
||||
table = varfont[tableTag].table
|
||||
if not getattr(table, 'FeatureVariations', None):
|
||||
continue
|
||||
variations = table.FeatureVariations
|
||||
for record in variations.FeatureVariationRecord:
|
||||
applies = True
|
||||
for condition in record.ConditionSet.ConditionTable:
|
||||
if condition.Format == 1:
|
||||
axisIdx = condition.AxisIndex
|
||||
axisTag = fvar.axes[axisIdx].axisTag
|
||||
Min = condition.FilterRangeMinValue
|
||||
Max = condition.FilterRangeMaxValue
|
||||
v = loc[axisTag]
|
||||
if not (Min <= v <= Max):
|
||||
applies = False
|
||||
else:
|
||||
applies = False
|
||||
if not applies:
|
||||
break
|
||||
|
||||
if applies:
|
||||
assert record.FeatureTableSubstitution.Version == 0x00010000
|
||||
for rec in record.FeatureTableSubstitution.SubstitutionRecord:
|
||||
table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = rec.Feature
|
||||
break
|
||||
del table.FeatureVariations
|
||||
|
||||
if 'GDEF' in varfont and varfont['GDEF'].table.Version >= 0x00010003:
|
||||
log.info("Mutating GDEF/GPOS/GSUB tables")
|
||||
gdef = varfont['GDEF'].table
|
||||
instancer = VarStoreInstancer(gdef.VarStore, fvar.axes, loc)
|
||||
|
||||
merger = MutatorMerger(varfont, instancer)
|
||||
merger.mergeTables(varfont, [varfont], ['GDEF', 'GPOS'])
|
||||
|
||||
# Downgrade GDEF.
|
||||
del gdef.VarStore
|
||||
gdef.Version = 0x00010002
|
||||
if gdef.MarkGlyphSetsDef is None:
|
||||
del gdef.MarkGlyphSetsDef
|
||||
gdef.Version = 0x00010000
|
||||
|
||||
if not (gdef.LigCaretList or
|
||||
gdef.MarkAttachClassDef or
|
||||
gdef.GlyphClassDef or
|
||||
gdef.AttachList or
|
||||
(gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)):
|
||||
del varfont['GDEF']
|
||||
|
||||
addidef = False
|
||||
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 overlap:
|
||||
for glyph_name in glyf.keys():
|
||||
glyph = glyf[glyph_name]
|
||||
# Set OVERLAP_COMPOUND bit for compound glyphs
|
||||
if glyph.isComposite():
|
||||
glyph.components[0].flags |= OVERLAP_COMPOUND
|
||||
# Set OVERLAP_SIMPLE bit for simple glyphs
|
||||
elif glyph.numberOfContours > 0:
|
||||
glyph.flags[0] |= flagOverlapSimple
|
||||
if addidef:
|
||||
log.info("Adding IDEF to fpgm table for GETVARIATION opcode")
|
||||
asm = []
|
||||
if 'fpgm' in varfont:
|
||||
fpgm = varfont['fpgm']
|
||||
asm = fpgm.program.getAssembly()
|
||||
else:
|
||||
fpgm = newTable('fpgm')
|
||||
fpgm.program = ttProgram.Program()
|
||||
varfont['fpgm'] = fpgm
|
||||
asm.append("PUSHB[000] 145")
|
||||
asm.append("IDEF[ ]")
|
||||
args = [str(len(loc))]
|
||||
for a in fvar.axes:
|
||||
args.append(str(floatToFixed(loc[a.axisTag], 14)))
|
||||
asm.append("NPUSHW[ ] " + ' '.join(args))
|
||||
asm.append("ENDF[ ]")
|
||||
fpgm.program.fromAssembly(asm)
|
||||
|
||||
# Change maxp attributes as IDEF is added
|
||||
if 'maxp' in varfont:
|
||||
maxp = varfont['maxp']
|
||||
setattr(maxp, "maxInstructionDefs", 1 + getattr(maxp, "maxInstructionDefs", 0))
|
||||
setattr(maxp, "maxStackElements", max(len(loc), getattr(maxp, "maxStackElements", 0)))
|
||||
|
||||
if 'name' in varfont:
|
||||
log.info("Pruning name table")
|
||||
exclude = {a.axisNameID for a in fvar.axes}
|
||||
for i in fvar.instances:
|
||||
exclude.add(i.subfamilyNameID)
|
||||
exclude.add(i.postscriptNameID)
|
||||
if 'ltag' in varfont:
|
||||
# Drop the whole 'ltag' table if all its language tags are referenced by
|
||||
# name records to be pruned.
|
||||
# TODO: prune unused ltag tags and re-enumerate langIDs accordingly
|
||||
excludedUnicodeLangIDs = [
|
||||
n.langID for n in varfont['name'].names
|
||||
if n.nameID in exclude and n.platformID == 0 and n.langID != 0xFFFF
|
||||
]
|
||||
if set(excludedUnicodeLangIDs) == set(range(len((varfont['ltag'].tags)))):
|
||||
del varfont['ltag']
|
||||
varfont['name'].names[:] = [
|
||||
n for n in varfont['name'].names
|
||||
if n.nameID not in exclude
|
||||
]
|
||||
|
||||
if "wght" in location and "OS/2" in varfont:
|
||||
varfont["OS/2"].usWeightClass = otRound(
|
||||
max(1, min(location["wght"], 1000))
|
||||
)
|
||||
if "wdth" in location:
|
||||
wdth = location["wdth"]
|
||||
for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()):
|
||||
if wdth < percent:
|
||||
varfont["OS/2"].usWidthClass = widthClass
|
||||
break
|
||||
else:
|
||||
varfont["OS/2"].usWidthClass = 9
|
||||
if "slnt" in location and "post" in varfont:
|
||||
varfont["post"].italicAngle = max(-90, min(location["slnt"], 90))
|
||||
|
||||
log.info("Removing variable tables")
|
||||
for tag in ('avar','cvar','fvar','gvar','HVAR','MVAR','VVAR','STAT'):
|
||||
if tag in varfont:
|
||||
del varfont[tag]
|
||||
|
||||
return varfont
|
||||
|
||||
|
||||
def main(args=None):
|
||||
"""Instantiate a variation font"""
|
||||
from fontTools import configLogger
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
"fonttools varLib.mutator", description="Instantiate a variable font")
|
||||
parser.add_argument(
|
||||
"input", metavar="INPUT.ttf", help="Input variable TTF file.")
|
||||
parser.add_argument(
|
||||
"locargs", metavar="AXIS=LOC", nargs="*",
|
||||
help="List of space separated locations. A location consist in "
|
||||
"the name of a variation axis, followed by '=' and a number. E.g.: "
|
||||
" wght=700 wdth=80. The default is the location of the base master.")
|
||||
parser.add_argument(
|
||||
"-o", "--output", metavar="OUTPUT.ttf", default=None,
|
||||
help="Output instance TTF file (default: INPUT-instance.ttf).")
|
||||
logging_group = parser.add_mutually_exclusive_group(required=False)
|
||||
logging_group.add_argument(
|
||||
"-v", "--verbose", action="store_true", help="Run more verbosely.")
|
||||
logging_group.add_argument(
|
||||
"-q", "--quiet", action="store_true", help="Turn verbosity off.")
|
||||
parser.add_argument(
|
||||
"--no-overlap",
|
||||
dest="overlap",
|
||||
action="store_false",
|
||||
help="Don't set OVERLAP_SIMPLE/OVERLAP_COMPOUND glyf flags."
|
||||
)
|
||||
options = parser.parse_args(args)
|
||||
|
||||
varfilename = options.input
|
||||
outfile = (
|
||||
os.path.splitext(varfilename)[0] + '-instance.ttf'
|
||||
if not options.output else options.output)
|
||||
configLogger(level=(
|
||||
"DEBUG" if options.verbose else
|
||||
"ERROR" if options.quiet else
|
||||
"INFO"))
|
||||
|
||||
loc = {}
|
||||
for arg in options.locargs:
|
||||
try:
|
||||
tag, val = arg.split('=')
|
||||
assert len(tag) <= 4
|
||||
loc[tag.ljust(4)] = float(val)
|
||||
except (ValueError, AssertionError):
|
||||
parser.error("invalid location argument format: %r" % arg)
|
||||
log.info("Location: %s", loc)
|
||||
|
||||
log.info("Loading variable font")
|
||||
varfont = TTFont(varfilename)
|
||||
|
||||
instantiateVariableFont(varfont, loc, inplace=True, overlap=options.overlap)
|
||||
|
||||
log.info("Saving instance font %s", outfile)
|
||||
varfont.save(outfile)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
if len(sys.argv) > 1:
|
||||
sys.exit(main())
|
||||
import doctest
|
||||
sys.exit(doctest.testmod().failed)
|
||||
Reference in New Issue
Block a user