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