| | """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()) |
| |
|