scaleUpem.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. """Change the units-per-EM of a font.
  2. AAT and Graphite tables are not supported. CFF/CFF2 fonts
  3. are de-subroutinized."""
  4. from fontTools.ttLib.ttVisitor import TTVisitor
  5. import fontTools.ttLib as ttLib
  6. import fontTools.ttLib.tables.otBase as otBase
  7. import fontTools.ttLib.tables.otTables as otTables
  8. from fontTools.cffLib import VarStoreData
  9. import fontTools.cffLib.specializer as cffSpecializer
  10. from fontTools.varLib import builder # for VarData.calculateNumShorts
  11. from fontTools.misc.fixedTools import otRound
  12. from fontTools.ttLib.tables._g_l_y_f import VarComponentFlags
  13. __all__ = ["scale_upem", "ScalerVisitor"]
  14. class ScalerVisitor(TTVisitor):
  15. def __init__(self, scaleFactor):
  16. self.scaleFactor = scaleFactor
  17. def scale(self, v):
  18. return otRound(v * self.scaleFactor)
  19. @ScalerVisitor.register_attrs(
  20. (
  21. (ttLib.getTableClass("head"), ("unitsPerEm", "xMin", "yMin", "xMax", "yMax")),
  22. (ttLib.getTableClass("post"), ("underlinePosition", "underlineThickness")),
  23. (ttLib.getTableClass("VORG"), ("defaultVertOriginY")),
  24. (
  25. ttLib.getTableClass("hhea"),
  26. (
  27. "ascent",
  28. "descent",
  29. "lineGap",
  30. "advanceWidthMax",
  31. "minLeftSideBearing",
  32. "minRightSideBearing",
  33. "xMaxExtent",
  34. "caretOffset",
  35. ),
  36. ),
  37. (
  38. ttLib.getTableClass("vhea"),
  39. (
  40. "ascent",
  41. "descent",
  42. "lineGap",
  43. "advanceHeightMax",
  44. "minTopSideBearing",
  45. "minBottomSideBearing",
  46. "yMaxExtent",
  47. "caretOffset",
  48. ),
  49. ),
  50. (
  51. ttLib.getTableClass("OS/2"),
  52. (
  53. "xAvgCharWidth",
  54. "ySubscriptXSize",
  55. "ySubscriptYSize",
  56. "ySubscriptXOffset",
  57. "ySubscriptYOffset",
  58. "ySuperscriptXSize",
  59. "ySuperscriptYSize",
  60. "ySuperscriptXOffset",
  61. "ySuperscriptYOffset",
  62. "yStrikeoutSize",
  63. "yStrikeoutPosition",
  64. "sTypoAscender",
  65. "sTypoDescender",
  66. "sTypoLineGap",
  67. "usWinAscent",
  68. "usWinDescent",
  69. "sxHeight",
  70. "sCapHeight",
  71. ),
  72. ),
  73. (
  74. otTables.ValueRecord,
  75. ("XAdvance", "YAdvance", "XPlacement", "YPlacement"),
  76. ), # GPOS
  77. (otTables.Anchor, ("XCoordinate", "YCoordinate")), # GPOS
  78. (otTables.CaretValue, ("Coordinate")), # GDEF
  79. (otTables.BaseCoord, ("Coordinate")), # BASE
  80. (otTables.MathValueRecord, ("Value")), # MATH
  81. (otTables.ClipBox, ("xMin", "yMin", "xMax", "yMax")), # COLR
  82. )
  83. )
  84. def visit(visitor, obj, attr, value):
  85. setattr(obj, attr, visitor.scale(value))
  86. @ScalerVisitor.register_attr(
  87. (ttLib.getTableClass("hmtx"), ttLib.getTableClass("vmtx")), "metrics"
  88. )
  89. def visit(visitor, obj, attr, metrics):
  90. for g in metrics:
  91. advance, lsb = metrics[g]
  92. metrics[g] = visitor.scale(advance), visitor.scale(lsb)
  93. @ScalerVisitor.register_attr(ttLib.getTableClass("VMTX"), "VOriginRecords")
  94. def visit(visitor, obj, attr, VOriginRecords):
  95. for g in VOriginRecords:
  96. VOriginRecords[g] = visitor.scale(VOriginRecords[g])
  97. @ScalerVisitor.register_attr(ttLib.getTableClass("glyf"), "glyphs")
  98. def visit(visitor, obj, attr, glyphs):
  99. for g in glyphs.values():
  100. for attr in ("xMin", "xMax", "yMin", "yMax"):
  101. v = getattr(g, attr, None)
  102. if v is not None:
  103. setattr(g, attr, visitor.scale(v))
  104. if g.isComposite():
  105. for component in g.components:
  106. component.x = visitor.scale(component.x)
  107. component.y = visitor.scale(component.y)
  108. continue
  109. if g.isVarComposite():
  110. for component in g.components:
  111. for attr in ("translateX", "translateY", "tCenterX", "tCenterY"):
  112. v = getattr(component.transform, attr)
  113. setattr(component.transform, attr, visitor.scale(v))
  114. continue
  115. if hasattr(g, "coordinates"):
  116. coordinates = g.coordinates
  117. for i, (x, y) in enumerate(coordinates):
  118. coordinates[i] = visitor.scale(x), visitor.scale(y)
  119. @ScalerVisitor.register_attr(ttLib.getTableClass("gvar"), "variations")
  120. def visit(visitor, obj, attr, variations):
  121. # VarComposites are a pain to handle :-(
  122. glyfTable = visitor.font["glyf"]
  123. for glyphName, varlist in variations.items():
  124. glyph = glyfTable[glyphName]
  125. isVarComposite = glyph.isVarComposite()
  126. for var in varlist:
  127. coordinates = var.coordinates
  128. if not isVarComposite:
  129. for i, xy in enumerate(coordinates):
  130. if xy is None:
  131. continue
  132. coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1])
  133. continue
  134. # VarComposite glyph
  135. i = 0
  136. for component in glyph.components:
  137. if component.flags & VarComponentFlags.AXES_HAVE_VARIATION:
  138. i += len(component.location)
  139. if component.flags & (
  140. VarComponentFlags.HAVE_TRANSLATE_X
  141. | VarComponentFlags.HAVE_TRANSLATE_Y
  142. ):
  143. xy = coordinates[i]
  144. coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1])
  145. i += 1
  146. if component.flags & VarComponentFlags.HAVE_ROTATION:
  147. i += 1
  148. if component.flags & (
  149. VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y
  150. ):
  151. i += 1
  152. if component.flags & (
  153. VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y
  154. ):
  155. i += 1
  156. if component.flags & (
  157. VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y
  158. ):
  159. xy = coordinates[i]
  160. coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1])
  161. i += 1
  162. # Phantom points
  163. assert i + 4 == len(coordinates)
  164. for i in range(i, len(coordinates)):
  165. xy = coordinates[i]
  166. coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1])
  167. @ScalerVisitor.register_attr(ttLib.getTableClass("kern"), "kernTables")
  168. def visit(visitor, obj, attr, kernTables):
  169. for table in kernTables:
  170. kernTable = table.kernTable
  171. for k in kernTable.keys():
  172. kernTable[k] = visitor.scale(kernTable[k])
  173. def _cff_scale(visitor, args):
  174. for i, arg in enumerate(args):
  175. if not isinstance(arg, list):
  176. if not isinstance(arg, bytes):
  177. args[i] = visitor.scale(arg)
  178. else:
  179. num_blends = arg[-1]
  180. _cff_scale(visitor, arg)
  181. arg[-1] = num_blends
  182. @ScalerVisitor.register_attr(
  183. (ttLib.getTableClass("CFF "), ttLib.getTableClass("CFF2")), "cff"
  184. )
  185. def visit(visitor, obj, attr, cff):
  186. cff.desubroutinize()
  187. topDict = cff.topDictIndex[0]
  188. varStore = getattr(topDict, "VarStore", None)
  189. getNumRegions = varStore.getNumRegions if varStore is not None else None
  190. privates = set()
  191. for fontname in cff.keys():
  192. font = cff[fontname]
  193. cs = font.CharStrings
  194. for g in font.charset:
  195. c, _ = cs.getItemAndSelector(g)
  196. privates.add(c.private)
  197. commands = cffSpecializer.programToCommands(
  198. c.program, getNumRegions=getNumRegions
  199. )
  200. for op, args in commands:
  201. if op == "vsindex":
  202. continue
  203. _cff_scale(visitor, args)
  204. c.program[:] = cffSpecializer.commandsToProgram(commands)
  205. # Annoying business of scaling numbers that do not matter whatsoever
  206. for attr in (
  207. "UnderlinePosition",
  208. "UnderlineThickness",
  209. "FontBBox",
  210. "StrokeWidth",
  211. ):
  212. value = getattr(topDict, attr, None)
  213. if value is None:
  214. continue
  215. if isinstance(value, list):
  216. _cff_scale(visitor, value)
  217. else:
  218. setattr(topDict, attr, visitor.scale(value))
  219. for i in range(6):
  220. topDict.FontMatrix[i] /= visitor.scaleFactor
  221. for private in privates:
  222. for attr in (
  223. "BlueValues",
  224. "OtherBlues",
  225. "FamilyBlues",
  226. "FamilyOtherBlues",
  227. # "BlueScale",
  228. # "BlueShift",
  229. # "BlueFuzz",
  230. "StdHW",
  231. "StdVW",
  232. "StemSnapH",
  233. "StemSnapV",
  234. "defaultWidthX",
  235. "nominalWidthX",
  236. ):
  237. value = getattr(private, attr, None)
  238. if value is None:
  239. continue
  240. if isinstance(value, list):
  241. _cff_scale(visitor, value)
  242. else:
  243. setattr(private, attr, visitor.scale(value))
  244. # ItemVariationStore
  245. @ScalerVisitor.register(otTables.VarData)
  246. def visit(visitor, varData):
  247. for item in varData.Item:
  248. for i, v in enumerate(item):
  249. item[i] = visitor.scale(v)
  250. varData.calculateNumShorts()
  251. # COLRv1
  252. def _setup_scale_paint(paint, scale):
  253. if -2 <= scale <= 2 - (1 >> 14):
  254. paint.Format = otTables.PaintFormat.PaintScaleUniform
  255. paint.scale = scale
  256. return
  257. transform = otTables.Affine2x3()
  258. transform.populateDefaults()
  259. transform.xy = transform.yx = transform.dx = transform.dy = 0
  260. transform.xx = transform.yy = scale
  261. paint.Format = otTables.PaintFormat.PaintTransform
  262. paint.Transform = transform
  263. @ScalerVisitor.register(otTables.BaseGlyphPaintRecord)
  264. def visit(visitor, record):
  265. oldPaint = record.Paint
  266. scale = otTables.Paint()
  267. _setup_scale_paint(scale, visitor.scaleFactor)
  268. scale.Paint = oldPaint
  269. record.Paint = scale
  270. return True
  271. @ScalerVisitor.register(otTables.Paint)
  272. def visit(visitor, paint):
  273. if paint.Format != otTables.PaintFormat.PaintGlyph:
  274. return True
  275. newPaint = otTables.Paint()
  276. newPaint.Format = paint.Format
  277. newPaint.Paint = paint.Paint
  278. newPaint.Glyph = paint.Glyph
  279. del paint.Paint
  280. del paint.Glyph
  281. _setup_scale_paint(paint, 1 / visitor.scaleFactor)
  282. paint.Paint = newPaint
  283. visitor.visit(newPaint.Paint)
  284. return False
  285. def scale_upem(font, new_upem):
  286. """Change the units-per-EM of font to the new value."""
  287. upem = font["head"].unitsPerEm
  288. visitor = ScalerVisitor(new_upem / upem)
  289. visitor.visit(font)
  290. def main(args=None):
  291. """Change the units-per-EM of fonts"""
  292. if args is None:
  293. import sys
  294. args = sys.argv[1:]
  295. from fontTools.ttLib import TTFont
  296. from fontTools.misc.cliTools import makeOutputFileName
  297. import argparse
  298. parser = argparse.ArgumentParser(
  299. "fonttools ttLib.scaleUpem", description="Change the units-per-EM of fonts"
  300. )
  301. parser.add_argument("font", metavar="font", help="Font file.")
  302. parser.add_argument(
  303. "new_upem", metavar="new-upem", help="New units-per-EM integer value."
  304. )
  305. parser.add_argument(
  306. "--output-file", metavar="path", default=None, help="Output file."
  307. )
  308. options = parser.parse_args(args)
  309. font = TTFont(options.font)
  310. new_upem = int(options.new_upem)
  311. output_file = (
  312. options.output_file
  313. if options.output_file is not None
  314. else makeOutputFileName(options.font, overWrite=True, suffix="-scaled")
  315. )
  316. scale_upem(font, new_upem)
  317. print("Writing %s" % output_file)
  318. font.save(output_file)
  319. if __name__ == "__main__":
  320. import sys
  321. sys.exit(main())