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