• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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