| """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 |
| 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"), |
| ), # 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 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): |
| # VarComposites are a pain to handle :-( |
| 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 |
| |
| # VarComposite glyph |
| |
| 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 |
| |
| # Phantom points |
| 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) |
| |
| # 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()) |