Spaces:
Sleeping
Sleeping
| """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) | |
| def visit(visitor, obj, attr, value): | |
| setattr(obj, attr, visitor.scale(value)) | |
| def visit(visitor, obj, attr, metrics): | |
| for g in metrics: | |
| advance, lsb = metrics[g] | |
| metrics[g] = visitor.scale(advance), visitor.scale(lsb) | |
| def visit(visitor, obj, attr, VOriginRecords): | |
| for g in VOriginRecords: | |
| VOriginRecords[g] = visitor.scale(VOriginRecords[g]) | |
| 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) | |
| 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]) | |
| 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() | |
| 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 | |
| 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 | |
| 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 | |
| def visit(visitor, record): | |
| oldPaint = record.Paint | |
| scale = otTables.Paint() | |
| _setup_scale_paint(scale, visitor.scaleFactor) | |
| scale.Paint = oldPaint | |
| record.Paint = scale | |
| return True | |
| 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()) | |