"""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) @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("VARC"), "table") 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() @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())