scaleUpem.py 14 KB

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