|
"""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 |
|
from fontTools.misc.fixedTools import otRound |
|
from fontTools.ttLib.tables._g_l_y_f import VarComponentFlags |
|
|
|
|
|
__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"), |
|
), |
|
(otTables.Anchor, ("XCoordinate", "YCoordinate")), |
|
(otTables.CaretValue, ("Coordinate")), |
|
(otTables.BaseCoord, ("Coordinate")), |
|
(otTables.MathValueRecord, ("Value")), |
|
(otTables.ClipBox, ("xMin", "yMin", "xMax", "yMax")), |
|
) |
|
) |
|
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 g.isVarComposite(): |
|
for component in g.components: |
|
for attr in ("translateX", "translateY", "tCenterX", "tCenterY"): |
|
v = getattr(component.transform, attr) |
|
setattr(component.transform, attr, visitor.scale(v)) |
|
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] |
|
isVarComposite = glyph.isVarComposite() |
|
for var in varlist: |
|
coordinates = var.coordinates |
|
|
|
if not isVarComposite: |
|
for i, xy in enumerate(coordinates): |
|
if xy is None: |
|
continue |
|
coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1]) |
|
continue |
|
|
|
|
|
|
|
i = 0 |
|
for component in glyph.components: |
|
if component.flags & VarComponentFlags.AXES_HAVE_VARIATION: |
|
i += len(component.location) |
|
if component.flags & ( |
|
VarComponentFlags.HAVE_TRANSLATE_X |
|
| VarComponentFlags.HAVE_TRANSLATE_Y |
|
): |
|
xy = coordinates[i] |
|
coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1]) |
|
i += 1 |
|
if component.flags & VarComponentFlags.HAVE_ROTATION: |
|
i += 1 |
|
if component.flags & ( |
|
VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y |
|
): |
|
i += 1 |
|
if component.flags & ( |
|
VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y |
|
): |
|
i += 1 |
|
if component.flags & ( |
|
VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y |
|
): |
|
xy = coordinates[i] |
|
coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1]) |
|
i += 1 |
|
|
|
|
|
assert i + 4 == len(coordinates) |
|
for i in range(i, len(coordinates)): |
|
xy = coordinates[i] |
|
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) |
|
|
|
|
|
|
|
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", |
|
|
|
|
|
|
|
"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)) |
|
|
|
|
|
|
|
|
|
|
|
@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() |
|
|
|
|
|
|
|
|
|
|
|
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()) |
|
|