xref: /aosp_15_r20/external/fonttools/Lib/fontTools/ttLib/scaleUpem.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
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