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