In favor of separate VARC table. ttGlyphSet does NOT yet know how to draw VARC table though. The 9 failing tests are all VarComposite-related and need to be updated with VARC equivalents eventually when we add VARC support to subsetter and instancer.
345 lines
9.9 KiB
Python
345 lines
9.9 KiB
Python
"""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.misc.fixedTools import otRound
|
|
|
|
|
|
__all__ = ["scale_upem", "ScalerVisitor"]
|
|
|
|
|
|
class ScalerVisitor(TTVisitor):
|
|
def __init__(self, scaleFactor):
|
|
self.scaleFactor = scaleFactor
|
|
|
|
def scale(self, v):
|
|
return otRound(v * self.scaleFactor)
|
|
|
|
|
|
@ScalerVisitor.register_attrs(
|
|
(
|
|
(ttLib.getTableClass("head"), ("unitsPerEm", "xMin", "yMin", "xMax", "yMax")),
|
|
(ttLib.getTableClass("post"), ("underlinePosition", "underlineThickness")),
|
|
(ttLib.getTableClass("VORG"), ("defaultVertOriginY")),
|
|
(
|
|
ttLib.getTableClass("hhea"),
|
|
(
|
|
"ascent",
|
|
"descent",
|
|
"lineGap",
|
|
"advanceWidthMax",
|
|
"minLeftSideBearing",
|
|
"minRightSideBearing",
|
|
"xMaxExtent",
|
|
"caretOffset",
|
|
),
|
|
),
|
|
(
|
|
ttLib.getTableClass("vhea"),
|
|
(
|
|
"ascent",
|
|
"descent",
|
|
"lineGap",
|
|
"advanceHeightMax",
|
|
"minTopSideBearing",
|
|
"minBottomSideBearing",
|
|
"yMaxExtent",
|
|
"caretOffset",
|
|
),
|
|
),
|
|
(
|
|
ttLib.getTableClass("OS/2"),
|
|
(
|
|
"xAvgCharWidth",
|
|
"ySubscriptXSize",
|
|
"ySubscriptYSize",
|
|
"ySubscriptXOffset",
|
|
"ySubscriptYOffset",
|
|
"ySuperscriptXSize",
|
|
"ySuperscriptYSize",
|
|
"ySuperscriptXOffset",
|
|
"ySuperscriptYOffset",
|
|
"yStrikeoutSize",
|
|
"yStrikeoutPosition",
|
|
"sTypoAscender",
|
|
"sTypoDescender",
|
|
"sTypoLineGap",
|
|
"usWinAscent",
|
|
"usWinDescent",
|
|
"sxHeight",
|
|
"sCapHeight",
|
|
),
|
|
),
|
|
(
|
|
otTables.ValueRecord,
|
|
("XAdvance", "YAdvance", "XPlacement", "YPlacement"),
|
|
), # GPOS
|
|
(otTables.Anchor, ("XCoordinate", "YCoordinate")), # GPOS
|
|
(otTables.CaretValue, ("Coordinate")), # GDEF
|
|
(otTables.BaseCoord, ("Coordinate")), # BASE
|
|
(otTables.MathValueRecord, ("Value")), # MATH
|
|
(otTables.ClipBox, ("xMin", "yMin", "xMax", "yMax")), # COLR
|
|
)
|
|
)
|
|
def visit(visitor, obj, attr, value):
|
|
setattr(obj, attr, visitor.scale(value))
|
|
|
|
|
|
@ScalerVisitor.register_attr(
|
|
(ttLib.getTableClass("hmtx"), ttLib.getTableClass("vmtx")), "metrics"
|
|
)
|
|
def visit(visitor, obj, attr, metrics):
|
|
for g in metrics:
|
|
advance, lsb = metrics[g]
|
|
metrics[g] = visitor.scale(advance), visitor.scale(lsb)
|
|
|
|
|
|
@ScalerVisitor.register_attr(ttLib.getTableClass("VMTX"), "VOriginRecords")
|
|
def visit(visitor, obj, attr, VOriginRecords):
|
|
for g in VOriginRecords:
|
|
VOriginRecords[g] = visitor.scale(VOriginRecords[g])
|
|
|
|
|
|
@ScalerVisitor.register_attr(ttLib.getTableClass("glyf"), "glyphs")
|
|
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)
|
|
|
|
|
|
@ScalerVisitor.register_attr(ttLib.getTableClass("gvar"), "variations")
|
|
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])
|
|
|
|
|
|
@ScalerVisitor.register_attr(ttLib.getTableClass("kern"), "kernTables")
|
|
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
|
|
|
|
|
|
@ScalerVisitor.register_attr(
|
|
(ttLib.getTableClass("CFF "), ttLib.getTableClass("CFF2")), "cff"
|
|
)
|
|
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
|
|
|
|
|
|
@ScalerVisitor.register(otTables.VarData)
|
|
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
|
|
|
|
|
|
@ScalerVisitor.register(otTables.BaseGlyphPaintRecord)
|
|
def visit(visitor, record):
|
|
oldPaint = record.Paint
|
|
|
|
scale = otTables.Paint()
|
|
_setup_scale_paint(scale, visitor.scaleFactor)
|
|
scale.Paint = oldPaint
|
|
|
|
record.Paint = scale
|
|
|
|
return True
|
|
|
|
|
|
@ScalerVisitor.register(otTables.Paint)
|
|
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())
|