Spaces:
Running
Running
"""Change the units-per-EM of a font. | |
AAT and Graphite tables are not supported. CFF/CFF2 fonts | |
are de-subroutinized.""" | |
from fontTools.ttLib.ttVisitor import TTVisitor | |
import fontTools.ttLib as ttLib | |
import fontTools.ttLib.tables.otBase as otBase | |
import fontTools.ttLib.tables.otTables as otTables | |
from fontTools.cffLib import VarStoreData | |
import fontTools.cffLib.specializer as cffSpecializer | |
from fontTools.varLib import builder # for VarData.calculateNumShorts | |
from fontTools.varLib.multiVarStore import OnlineMultiVarStoreBuilder | |
from fontTools.misc.vector import Vector | |
from fontTools.misc.fixedTools import otRound | |
from fontTools.misc.iterTools import batched | |
__all__ = ["scale_upem", "ScalerVisitor"] | |
class ScalerVisitor(TTVisitor): | |
def __init__(self, scaleFactor): | |
self.scaleFactor = scaleFactor | |
def scale(self, v): | |
return otRound(v * self.scaleFactor) | |
def visit(visitor, obj, attr, value): | |
setattr(obj, attr, visitor.scale(value)) | |
def visit(visitor, obj, attr, metrics): | |
for g in metrics: | |
advance, lsb = metrics[g] | |
metrics[g] = visitor.scale(advance), visitor.scale(lsb) | |
def visit(visitor, obj, attr, VOriginRecords): | |
for g in VOriginRecords: | |
VOriginRecords[g] = visitor.scale(VOriginRecords[g]) | |
def visit(visitor, obj, attr, glyphs): | |
for g in glyphs.values(): | |
for attr in ("xMin", "xMax", "yMin", "yMax"): | |
v = getattr(g, attr, None) | |
if v is not None: | |
setattr(g, attr, visitor.scale(v)) | |
if g.isComposite(): | |
for component in g.components: | |
component.x = visitor.scale(component.x) | |
component.y = visitor.scale(component.y) | |
continue | |
if hasattr(g, "coordinates"): | |
coordinates = g.coordinates | |
for i, (x, y) in enumerate(coordinates): | |
coordinates[i] = visitor.scale(x), visitor.scale(y) | |
def visit(visitor, obj, attr, variations): | |
glyfTable = visitor.font["glyf"] | |
for glyphName, varlist in variations.items(): | |
glyph = glyfTable[glyphName] | |
for var in varlist: | |
coordinates = var.coordinates | |
for i, xy in enumerate(coordinates): | |
if xy is None: | |
continue | |
coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1]) | |
def visit(visitor, obj, attr, varc): | |
# VarComposite variations are a pain | |
fvar = visitor.font["fvar"] | |
fvarAxes = [a.axisTag for a in fvar.axes] | |
store = varc.MultiVarStore | |
storeBuilder = OnlineMultiVarStoreBuilder(fvarAxes) | |
for g in varc.VarCompositeGlyphs.VarCompositeGlyph: | |
for component in g.components: | |
t = component.transform | |
t.translateX = visitor.scale(t.translateX) | |
t.translateY = visitor.scale(t.translateY) | |
t.tCenterX = visitor.scale(t.tCenterX) | |
t.tCenterY = visitor.scale(t.tCenterY) | |
if component.axisValuesVarIndex != otTables.NO_VARIATION_INDEX: | |
varIdx = component.axisValuesVarIndex | |
# TODO Move this code duplicated below to MultiVarStore.__getitem__, | |
# or a getDeltasAndSupports(). | |
if varIdx != otTables.NO_VARIATION_INDEX: | |
major = varIdx >> 16 | |
minor = varIdx & 0xFFFF | |
varData = store.MultiVarData[major] | |
vec = varData.Item[minor] | |
storeBuilder.setSupports(store.get_supports(major, fvar.axes)) | |
if vec: | |
m = len(vec) // varData.VarRegionCount | |
vec = list(batched(vec, m)) | |
vec = [Vector(v) for v in vec] | |
component.axisValuesVarIndex = storeBuilder.storeDeltas(vec) | |
else: | |
component.axisValuesVarIndex = otTables.NO_VARIATION_INDEX | |
if component.transformVarIndex != otTables.NO_VARIATION_INDEX: | |
varIdx = component.transformVarIndex | |
if varIdx != otTables.NO_VARIATION_INDEX: | |
major = varIdx >> 16 | |
minor = varIdx & 0xFFFF | |
vec = varData.Item[varIdx & 0xFFFF] | |
major = varIdx >> 16 | |
minor = varIdx & 0xFFFF | |
varData = store.MultiVarData[major] | |
vec = varData.Item[minor] | |
storeBuilder.setSupports(store.get_supports(major, fvar.axes)) | |
if vec: | |
m = len(vec) // varData.VarRegionCount | |
flags = component.flags | |
vec = list(batched(vec, m)) | |
newVec = [] | |
for v in vec: | |
v = list(v) | |
i = 0 | |
## Scale translate & tCenter | |
if flags & otTables.VarComponentFlags.HAVE_TRANSLATE_X: | |
v[i] = visitor.scale(v[i]) | |
i += 1 | |
if flags & otTables.VarComponentFlags.HAVE_TRANSLATE_Y: | |
v[i] = visitor.scale(v[i]) | |
i += 1 | |
if flags & otTables.VarComponentFlags.HAVE_ROTATION: | |
i += 1 | |
if flags & otTables.VarComponentFlags.HAVE_SCALE_X: | |
i += 1 | |
if flags & otTables.VarComponentFlags.HAVE_SCALE_Y: | |
i += 1 | |
if flags & otTables.VarComponentFlags.HAVE_SKEW_X: | |
i += 1 | |
if flags & otTables.VarComponentFlags.HAVE_SKEW_Y: | |
i += 1 | |
if flags & otTables.VarComponentFlags.HAVE_TCENTER_X: | |
v[i] = visitor.scale(v[i]) | |
i += 1 | |
if flags & otTables.VarComponentFlags.HAVE_TCENTER_Y: | |
v[i] = visitor.scale(v[i]) | |
i += 1 | |
newVec.append(Vector(v)) | |
vec = newVec | |
component.transformVarIndex = storeBuilder.storeDeltas(vec) | |
else: | |
component.transformVarIndex = otTables.NO_VARIATION_INDEX | |
varc.MultiVarStore = storeBuilder.finish() | |
def visit(visitor, obj, attr, kernTables): | |
for table in kernTables: | |
kernTable = table.kernTable | |
for k in kernTable.keys(): | |
kernTable[k] = visitor.scale(kernTable[k]) | |
def _cff_scale(visitor, args): | |
for i, arg in enumerate(args): | |
if not isinstance(arg, list): | |
if not isinstance(arg, bytes): | |
args[i] = visitor.scale(arg) | |
else: | |
num_blends = arg[-1] | |
_cff_scale(visitor, arg) | |
arg[-1] = num_blends | |
def visit(visitor, obj, attr, cff): | |
cff.desubroutinize() | |
topDict = cff.topDictIndex[0] | |
varStore = getattr(topDict, "VarStore", None) | |
getNumRegions = varStore.getNumRegions if varStore is not None else None | |
privates = set() | |
for fontname in cff.keys(): | |
font = cff[fontname] | |
cs = font.CharStrings | |
for g in font.charset: | |
c, _ = cs.getItemAndSelector(g) | |
privates.add(c.private) | |
commands = cffSpecializer.programToCommands( | |
c.program, getNumRegions=getNumRegions | |
) | |
for op, args in commands: | |
if op == "vsindex": | |
continue | |
_cff_scale(visitor, args) | |
c.program[:] = cffSpecializer.commandsToProgram(commands) | |
# Annoying business of scaling numbers that do not matter whatsoever | |
for attr in ( | |
"UnderlinePosition", | |
"UnderlineThickness", | |
"FontBBox", | |
"StrokeWidth", | |
): | |
value = getattr(topDict, attr, None) | |
if value is None: | |
continue | |
if isinstance(value, list): | |
_cff_scale(visitor, value) | |
else: | |
setattr(topDict, attr, visitor.scale(value)) | |
for i in range(6): | |
topDict.FontMatrix[i] /= visitor.scaleFactor | |
for private in privates: | |
for attr in ( | |
"BlueValues", | |
"OtherBlues", | |
"FamilyBlues", | |
"FamilyOtherBlues", | |
# "BlueScale", | |
# "BlueShift", | |
# "BlueFuzz", | |
"StdHW", | |
"StdVW", | |
"StemSnapH", | |
"StemSnapV", | |
"defaultWidthX", | |
"nominalWidthX", | |
): | |
value = getattr(private, attr, None) | |
if value is None: | |
continue | |
if isinstance(value, list): | |
_cff_scale(visitor, value) | |
else: | |
setattr(private, attr, visitor.scale(value)) | |
# ItemVariationStore | |
def visit(visitor, varData): | |
for item in varData.Item: | |
for i, v in enumerate(item): | |
item[i] = visitor.scale(v) | |
varData.calculateNumShorts() | |
# COLRv1 | |
def _setup_scale_paint(paint, scale): | |
if -2 <= scale <= 2 - (1 >> 14): | |
paint.Format = otTables.PaintFormat.PaintScaleUniform | |
paint.scale = scale | |
return | |
transform = otTables.Affine2x3() | |
transform.populateDefaults() | |
transform.xy = transform.yx = transform.dx = transform.dy = 0 | |
transform.xx = transform.yy = scale | |
paint.Format = otTables.PaintFormat.PaintTransform | |
paint.Transform = transform | |
def visit(visitor, record): | |
oldPaint = record.Paint | |
scale = otTables.Paint() | |
_setup_scale_paint(scale, visitor.scaleFactor) | |
scale.Paint = oldPaint | |
record.Paint = scale | |
return True | |
def visit(visitor, paint): | |
if paint.Format != otTables.PaintFormat.PaintGlyph: | |
return True | |
newPaint = otTables.Paint() | |
newPaint.Format = paint.Format | |
newPaint.Paint = paint.Paint | |
newPaint.Glyph = paint.Glyph | |
del paint.Paint | |
del paint.Glyph | |
_setup_scale_paint(paint, 1 / visitor.scaleFactor) | |
paint.Paint = newPaint | |
visitor.visit(newPaint.Paint) | |
return False | |
def scale_upem(font, new_upem): | |
"""Change the units-per-EM of font to the new value.""" | |
upem = font["head"].unitsPerEm | |
visitor = ScalerVisitor(new_upem / upem) | |
visitor.visit(font) | |
def main(args=None): | |
"""Change the units-per-EM of fonts""" | |
if args is None: | |
import sys | |
args = sys.argv[1:] | |
from fontTools.ttLib import TTFont | |
from fontTools.misc.cliTools import makeOutputFileName | |
import argparse | |
parser = argparse.ArgumentParser( | |
"fonttools ttLib.scaleUpem", description="Change the units-per-EM of fonts" | |
) | |
parser.add_argument("font", metavar="font", help="Font file.") | |
parser.add_argument( | |
"new_upem", metavar="new-upem", help="New units-per-EM integer value." | |
) | |
parser.add_argument( | |
"--output-file", metavar="path", default=None, help="Output file." | |
) | |
options = parser.parse_args(args) | |
font = TTFont(options.font) | |
new_upem = int(options.new_upem) | |
output_file = ( | |
options.output_file | |
if options.output_file is not None | |
else makeOutputFileName(options.font, overWrite=True, suffix="-scaled") | |
) | |
scale_upem(font, new_upem) | |
print("Writing %s" % output_file) | |
font.save(output_file) | |
if __name__ == "__main__": | |
import sys | |
sys.exit(main()) | |