1# -*- coding: utf-8 -*-
2from __future__ import print_function, absolute_import, division, unicode_literals
3from fontTools.ttLib import TTFont
4from fontTools.misc.py23 import basestring, unichr, byteord
5from ufo2ft.outlineCompiler import OutlineTTFCompiler, OutlineOTFCompiler
6from ufo2ft.fontInfoData import intListToNum
7from fontTools.ttLib.tables._g_l_y_f import USE_MY_METRICS
8from ufo2ft.constants import USE_PRODUCTION_NAMES, GLYPHS_DONT_USE_PRODUCTION_NAMES
9from ufo2ft import compileTTF
10import os
11import logging
12import pytest
13
14
15def getpath(filename):
16    dirname = os.path.dirname(__file__)
17    return os.path.join(dirname, "data", filename)
18
19
20@pytest.fixture
21def testufo(FontClass):
22    font = FontClass(getpath("TestFont.ufo"))
23    del font.lib["public.postscriptNames"]
24    return font
25
26
27@pytest.fixture
28def use_my_metrics_ufo(FontClass):
29    return FontClass(getpath("UseMyMetrics.ufo"))
30
31
32@pytest.fixture
33def emptyufo(FontClass):
34    font = FontClass()
35    font.info.unitsPerEm = 1000
36    font.info.familyName = "Test Font"
37    font.info.styleName = "Regular"
38    font.info.ascender = 750
39    font.info.descender = -250
40    font.info.xHeight = 500
41    font.info.capHeight = 750
42    return font
43
44
45class OutlineTTFCompilerTest(object):
46    def test_setupTable_gasp(self, testufo):
47        compiler = OutlineTTFCompiler(testufo)
48        compiler.otf = TTFont()
49        compiler.setupTable_gasp()
50        assert "gasp" in compiler.otf
51        assert compiler.otf["gasp"].gaspRange == {7: 10, 65535: 15}
52
53    def test_compile_with_gasp(self, testufo):
54        compiler = OutlineTTFCompiler(testufo)
55        compiler.compile()
56        assert "gasp" in compiler.otf
57        assert compiler.otf["gasp"].gaspRange == {7: 10, 65535: 15}
58
59    def test_compile_without_gasp(self, testufo):
60        testufo.info.openTypeGaspRangeRecords = None
61        compiler = OutlineTTFCompiler(testufo)
62        compiler.compile()
63        assert "gasp" not in compiler.otf
64
65    def test_compile_empty_gasp(self, testufo):
66        # ignore empty gasp
67        testufo.info.openTypeGaspRangeRecords = []
68        compiler = OutlineTTFCompiler(testufo)
69        compiler.compile()
70        assert "gasp" not in compiler.otf
71
72    def test_makeGlyphsBoundingBoxes(self, testufo):
73        # the call to 'makeGlyphsBoundingBoxes' happen in the __init__ method
74        compiler = OutlineTTFCompiler(testufo)
75        assert compiler.glyphBoundingBoxes[".notdef"] == (50, 0, 450, 750)
76        # no outline data
77        assert compiler.glyphBoundingBoxes["space"] is None
78        # float coordinates are rounded, so is the bbox
79        assert compiler.glyphBoundingBoxes["d"] == (90, 77, 211, 197)
80
81    def test_autoUseMyMetrics(self, use_my_metrics_ufo):
82        compiler = OutlineTTFCompiler(use_my_metrics_ufo)
83        ttf = compiler.compile()
84        # the first component in the 'Iacute' composite glyph ('acute')
85        # does _not_ have the USE_MY_METRICS flag
86        assert not (ttf["glyf"]["Iacute"].components[0].flags & USE_MY_METRICS)
87        # the second component in the 'Iacute' composite glyph ('I')
88        # has the USE_MY_METRICS flag set
89        assert ttf["glyf"]["Iacute"].components[1].flags & USE_MY_METRICS
90        # none of the 'I' components of the 'romanthree' glyph has
91        # the USE_MY_METRICS flag set, because the composite glyph has a
92        # different width
93        for component in ttf["glyf"]["romanthree"].components:
94            assert not (component.flags & USE_MY_METRICS)
95
96    def test_autoUseMyMetrics_None(self, use_my_metrics_ufo):
97        compiler = OutlineTTFCompiler(use_my_metrics_ufo)
98        # setting 'autoUseMyMetrics' attribute to None disables the feature
99        compiler.autoUseMyMetrics = None
100        ttf = compiler.compile()
101        assert not (ttf["glyf"]["Iacute"].components[1].flags & USE_MY_METRICS)
102
103    def test_importTTX(self, testufo):
104        compiler = OutlineTTFCompiler(testufo)
105        otf = compiler.otf = TTFont()
106        compiler.importTTX()
107        assert "CUST" in otf
108        assert otf["CUST"].data == b"\x00\x01\xbe\xef"
109        assert otf.sfntVersion == "\x00\x01\x00\x00"
110
111    def test_no_contour_glyphs(self, testufo):
112        for glyph in testufo:
113            glyph.clearContours()
114        compiler = OutlineTTFCompiler(testufo)
115        compiler.compile()
116        assert compiler.otf["hhea"].advanceWidthMax == 600
117        assert compiler.otf["hhea"].minLeftSideBearing == 0
118        assert compiler.otf["hhea"].minRightSideBearing == 0
119        assert compiler.otf["hhea"].xMaxExtent == 0
120
121    def test_os2_no_widths(self, testufo):
122        for glyph in testufo:
123            glyph.width = 0
124        compiler = OutlineTTFCompiler(testufo)
125        compiler.compile()
126        assert compiler.otf["OS/2"].xAvgCharWidth == 0
127
128    def test_missing_component(self, emptyufo):
129        ufo = emptyufo
130        a = ufo.newGlyph("a")
131        pen = a.getPen()
132        pen.moveTo((0, 0))
133        pen.lineTo((100, 0))
134        pen.lineTo((100, 100))
135        pen.lineTo((0, 100))
136        pen.closePath()
137
138        # a mixed contour/component glyph, which is decomposed by the
139        # TTGlyphPen; one of the components does not exist thus should
140        # be dropped
141        b = ufo.newGlyph("b")
142        pen = b.getPen()
143        pen.moveTo((0, 200))
144        pen.lineTo((100, 200))
145        pen.lineTo((50, 300))
146        pen.closePath()
147        pen.addComponent("a", (1, 0, 0, 1, 0, 0))
148        pen.addComponent("c", (1, 0, 0, 1, 0, 0))  # missing
149
150        d = ufo.newGlyph("d")
151        pen = d.getPen()
152        pen.addComponent("c", (1, 0, 0, 1, 0, 0))  # missing
153
154        e = ufo.newGlyph("e")
155        pen = e.getPen()
156        pen.addComponent("a", (1, 0, 0, 1, 0, 0))
157        pen.addComponent("c", (1, 0, 0, 1, 0, 0))  # missing
158
159        compiler = OutlineTTFCompiler(ufo)
160        ttFont = compiler.compile()
161        glyf = ttFont["glyf"]
162
163        assert glyf["a"].numberOfContours == 1
164        assert glyf["b"].numberOfContours == 2
165        assert glyf["d"].numberOfContours == 0
166        assert glyf["e"].numberOfContours == -1  # composite glyph
167        assert len(glyf["e"].components) == 1
168
169
170class OutlineOTFCompilerTest(object):
171    def test_setupTable_CFF_all_blues_defined(self, testufo):
172        testufo.info.postscriptBlueFuzz = 2
173        testufo.info.postscriptBlueShift = 8
174        testufo.info.postscriptBlueScale = 0.049736
175        testufo.info.postscriptForceBold = False
176        testufo.info.postscriptBlueValues = [-12, 0, 486, 498, 712, 724]
177        testufo.info.postscriptOtherBlues = [-217, -205]
178        testufo.info.postscriptFamilyBlues = [-12, 0, 486, 498, 712, 724]
179        testufo.info.postscriptFamilyOtherBlues = [-217, -205]
180
181        compiler = OutlineOTFCompiler(testufo)
182        compiler.otf = TTFont(sfntVersion="OTTO")
183
184        compiler.setupTable_CFF()
185
186        cff = compiler.otf["CFF "].cff
187        private = cff[list(cff.keys())[0]].Private
188
189        assert private.BlueFuzz == 2
190        assert private.BlueShift == 8
191        assert private.BlueScale == 0.049736
192        assert private.ForceBold == 0
193        assert private.BlueValues == [-12, 0, 486, 498, 712, 724]
194        assert private.OtherBlues == [-217, -205]
195        assert private.FamilyBlues == [-12, 0, 486, 498, 712, 724]
196        assert private.FamilyOtherBlues == [-217, -205]
197
198    def test_setupTable_CFF_no_blues_defined(self, testufo):
199        # no blue values defined
200        testufo.info.postscriptBlueValues = []
201        testufo.info.postscriptOtherBlues = []
202        testufo.info.postscriptFamilyBlues = []
203        testufo.info.postscriptFamilyOtherBlues = []
204        # the following attributes have no effect
205        testufo.info.postscriptBlueFuzz = 2
206        testufo.info.postscriptBlueShift = 8
207        testufo.info.postscriptBlueScale = 0.049736
208        testufo.info.postscriptForceBold = False
209
210        compiler = OutlineOTFCompiler(testufo)
211        compiler.otf = TTFont(sfntVersion="OTTO")
212
213        compiler.setupTable_CFF()
214
215        cff = compiler.otf["CFF "].cff
216        private = cff[list(cff.keys())[0]].Private
217
218        # expect default values as defined in fontTools' cffLib.py
219        assert private.BlueFuzz == 1
220        assert private.BlueShift == 7
221        assert private.BlueScale == 0.039625
222        assert private.ForceBold == 0
223        # CFF PrivateDict has no blues attributes
224        assert not hasattr(private, "BlueValues")
225        assert not hasattr(private, "OtherBlues")
226        assert not hasattr(private, "FamilyBlues")
227        assert not hasattr(private, "FamilyOtherBlues")
228
229    def test_setupTable_CFF_some_blues_defined(self, testufo):
230        testufo.info.postscriptBlueFuzz = 2
231        testufo.info.postscriptForceBold = True
232        testufo.info.postscriptBlueValues = []
233        testufo.info.postscriptOtherBlues = [-217, -205]
234        testufo.info.postscriptFamilyBlues = []
235        testufo.info.postscriptFamilyOtherBlues = []
236
237        compiler = OutlineOTFCompiler(testufo)
238        compiler.otf = TTFont(sfntVersion="OTTO")
239
240        compiler.setupTable_CFF()
241
242        cff = compiler.otf["CFF "].cff
243        private = cff[list(cff.keys())[0]].Private
244
245        assert private.BlueFuzz == 2
246        assert private.BlueShift == 7  # default
247        assert private.BlueScale == 0.039625  # default
248        assert private.ForceBold is True
249        assert not hasattr(private, "BlueValues")
250        assert private.OtherBlues == [-217, -205]
251        assert not hasattr(private, "FamilyBlues")
252        assert not hasattr(private, "FamilyOtherBlues")
253
254    @staticmethod
255    def get_charstring_program(ttFont, glyphName):
256        cff = ttFont["CFF "].cff
257        charstrings = cff[list(cff.keys())[0]].CharStrings
258        c, _ = charstrings.getItemAndSelector(glyphName)
259        c.decompile()
260        return c.program
261
262    def assertProgramEqual(self, expected, actual):
263        assert len(expected) == len(actual)
264        for exp_token, act_token in zip(expected, actual):
265            if isinstance(exp_token, basestring):
266                assert exp_token == act_token
267            else:
268                assert not isinstance(act_token, basestring)
269                assert exp_token == pytest.approx(act_token)
270
271    def test_setupTable_CFF_round_all(self, testufo):
272        # by default all floats are rounded to integer
273        compiler = OutlineOTFCompiler(testufo)
274        otf = compiler.otf = TTFont(sfntVersion="OTTO")
275
276        compiler.setupTable_CFF()
277        # glyph 'd' in TestFont.ufo contains float coordinates
278        program = self.get_charstring_program(otf, "d")
279
280        self.assertProgramEqual(
281            program,
282            [
283                -26,
284                151,
285                197,
286                "rmoveto",
287                -34,
288                -27,
289                -27,
290                -33,
291                -33,
292                27,
293                -27,
294                34,
295                33,
296                27,
297                27,
298                33,
299                33,
300                -27,
301                27,
302                -33,
303                "hvcurveto",
304                "endchar",
305            ],
306        )
307
308    def test_setupTable_CFF_round_none(self, testufo):
309        # roundTolerance=0 means 'don't round, keep all floats'
310        compiler = OutlineOTFCompiler(testufo, roundTolerance=0)
311        otf = compiler.otf = TTFont(sfntVersion="OTTO")
312
313        compiler.setupTable_CFF()
314        program = self.get_charstring_program(otf, "d")
315
316        self.assertProgramEqual(
317            program,
318            [
319                -26,
320                150.66,
321                197.32,
322                "rmoveto",
323                -33.66,
324                -26.67,
325                -26.99,
326                -33.33,
327                -33.33,
328                26.67,
329                -26.66,
330                33.66,
331                33.33,
332                26.66,
333                26.66,
334                33.33,
335                33.33,
336                -26.66,
337                26.99,
338                -33.33,
339                "hvcurveto",
340                "endchar",
341            ],
342        )
343
344    def test_setupTable_CFF_round_some(self, testufo):
345        # only floats 'close enough' are rounded to integer
346        compiler = OutlineOTFCompiler(testufo, roundTolerance=0.34)
347        otf = compiler.otf = TTFont(sfntVersion="OTTO")
348
349        compiler.setupTable_CFF()
350        program = self.get_charstring_program(otf, "d")
351
352        self.assertProgramEqual(
353            program,
354            [
355                -26,
356                150.66,
357                197,
358                "rmoveto",
359                -33.66,
360                -27,
361                -27,
362                -33,
363                -33,
364                27,
365                -27,
366                33.66,
367                33.34,
368                26.65,
369                27,
370                33,
371                33,
372                -26.65,
373                27,
374                -33.34,
375                "hvcurveto",
376                "endchar",
377            ],
378        )
379
380    def test_setupTable_CFF_optimize(self, testufo):
381        compiler = OutlineOTFCompiler(testufo, optimizeCFF=True)
382        otf = compiler.otf = TTFont(sfntVersion="OTTO")
383
384        compiler.setupTable_CFF()
385        program = self.get_charstring_program(otf, "a")
386
387        self.assertProgramEqual(
388            program,
389            [
390                -12,
391                66,
392                'hmoveto',
393                256,
394                'hlineto',
395                -128,
396                510,
397                'rlineto',
398                'endchar'
399            ]
400        )
401
402    def test_setupTable_CFF_no_optimize(self, testufo):
403        compiler = OutlineOTFCompiler(testufo, optimizeCFF=False)
404        otf = compiler.otf = TTFont(sfntVersion="OTTO")
405
406        compiler.setupTable_CFF()
407        program = self.get_charstring_program(otf, "a")
408
409        self.assertProgramEqual(
410            program,
411            [
412                -12,
413                66,
414                0,
415                'rmoveto',
416                256,
417                0,
418                'rlineto',
419                -128,
420                510,
421                'rlineto',
422                'endchar'
423            ],
424        )
425
426    def test_makeGlyphsBoundingBoxes(self, testufo):
427        # the call to 'makeGlyphsBoundingBoxes' happen in the __init__ method
428        compiler = OutlineOTFCompiler(testufo)
429        # with default roundTolerance, all coordinates and hence the bounding
430        # box values are rounded with otRound()
431        assert compiler.glyphBoundingBoxes["d"] == (90, 77, 211, 197)
432
433    def test_makeGlyphsBoundingBoxes_floats(self, testufo):
434        # specifying a custom roundTolerance affects which coordinates are
435        # rounded; in this case, the top-most Y coordinate stays a float
436        # (197.32), hence the bbox.yMax (198) is rounded using math.ceiling()
437        compiler = OutlineOTFCompiler(testufo, roundTolerance=0.1)
438        assert compiler.glyphBoundingBoxes["d"] == (90, 77, 211, 198)
439
440    def test_importTTX(self, testufo):
441        compiler = OutlineOTFCompiler(testufo)
442        otf = compiler.otf = TTFont(sfntVersion="OTTO")
443        compiler.importTTX()
444        assert "CUST" in otf
445        assert otf["CUST"].data == b"\x00\x01\xbe\xef"
446        assert otf.sfntVersion == "OTTO"
447
448    def test_no_contour_glyphs(self, testufo):
449        for glyph in testufo:
450            glyph.clearContours()
451        compiler = OutlineOTFCompiler(testufo)
452        compiler.compile()
453        assert compiler.otf["hhea"].advanceWidthMax == 600
454        assert compiler.otf["hhea"].minLeftSideBearing == 0
455        assert compiler.otf["hhea"].minRightSideBearing == 0
456        assert compiler.otf["hhea"].xMaxExtent == 0
457
458    def test_optimized_default_and_nominal_widths(self, FontClass):
459        ufo = FontClass()
460        ufo.info.unitsPerEm = 1000
461        for glyphName, width in (
462            (".notdef", 500),
463            ("space", 250),
464            ("a", 388),
465            ("b", 410),
466            ("c", 374),
467            ("d", 374),
468            ("e", 388),
469            ("f", 410),
470            ("g", 388),
471            ("h", 410),
472            ("i", 600),
473            ("j", 600),
474            ("k", 600),
475            ("l", 600),
476        ):
477            glyph = ufo.newGlyph(glyphName)
478            glyph.width = width
479
480        compiler = OutlineOTFCompiler(ufo)
481        compiler.otf = TTFont(sfntVersion="OTTO")
482
483        compiler.setupTable_hmtx()
484        compiler.setupTable_CFF()
485
486        cff = compiler.otf["CFF "].cff
487        topDict = cff[list(cff.keys())[0]]
488        private = topDict.Private
489
490        assert private.defaultWidthX == 600
491        assert private.nominalWidthX == 303
492
493        charStrings = topDict.CharStrings
494        # the following have width == defaultWidthX, so it's omitted
495        for g in ("i", "j", "k", "l"):
496            assert charStrings.getItemAndSelector(g)[0].program == ["endchar"]
497        # 'space' has width 250, so the width encoded in its charstring is:
498        # 250 - nominalWidthX
499        assert charStrings.getItemAndSelector("space")[0].program == [-53, "endchar"]
500
501
502class GlyphOrderTest(object):
503    def test_compile_original_glyph_order(self, testufo):
504        DEFAULT_ORDER = [
505            ".notdef",
506            "space",
507            "a",
508            "b",
509            "c",
510            "d",
511            "e",
512            "f",
513            "g",
514            "h",
515            "i",
516            "j",
517            "k",
518            "l",
519        ]
520        compiler = OutlineTTFCompiler(testufo)
521        compiler.compile()
522        assert compiler.otf.getGlyphOrder() == DEFAULT_ORDER
523
524    def test_compile_tweaked_glyph_order(self, testufo):
525        NEW_ORDER = [
526            ".notdef",
527            "space",
528            "b",
529            "a",
530            "c",
531            "d",
532            "e",
533            "f",
534            "g",
535            "h",
536            "i",
537            "j",
538            "k",
539            "l",
540        ]
541        testufo.lib["public.glyphOrder"] = NEW_ORDER
542        compiler = OutlineTTFCompiler(testufo)
543        compiler.compile()
544        assert compiler.otf.getGlyphOrder() == NEW_ORDER
545
546    def test_compile_strange_glyph_order(self, testufo):
547        """Move space and .notdef to end of glyph ids
548        ufo2ft always puts .notdef first.
549        """
550        NEW_ORDER = ["b", "a", "c", "d", "space", ".notdef"]
551        EXPECTED_ORDER = [
552            ".notdef",
553            "b",
554            "a",
555            "c",
556            "d",
557            "space",
558            "e",
559            "f",
560            "g",
561            "h",
562            "i",
563            "j",
564            "k",
565            "l",
566        ]
567        testufo.lib["public.glyphOrder"] = NEW_ORDER
568        compiler = OutlineTTFCompiler(testufo)
569        compiler.compile()
570        assert compiler.otf.getGlyphOrder() == EXPECTED_ORDER
571
572
573class NamesTest(object):
574    @pytest.mark.parametrize(
575        "prod_names_key, prod_names_value",
576        [(USE_PRODUCTION_NAMES, False), (GLYPHS_DONT_USE_PRODUCTION_NAMES, True)],
577        ids=["useProductionNames", "Don't use Production Names"],
578    )
579    def test_compile_without_production_names(
580        self, testufo, prod_names_key, prod_names_value
581    ):
582        expected = [
583            ".notdef",
584            "space",
585            "a",
586            "b",
587            "c",
588            "d",
589            "e",
590            "f",
591            "g",
592            "h",
593            "i",
594            "j",
595            "k",
596            "l",
597        ]
598
599        result = compileTTF(testufo, useProductionNames=False)
600        assert result.getGlyphOrder() == expected
601
602        testufo.lib[prod_names_key] = prod_names_value
603        result = compileTTF(testufo)
604        assert result.getGlyphOrder() == expected
605
606    def test_compile_with_production_names(self, testufo):
607        original = [
608            ".notdef",
609            "space",
610            "a",
611            "b",
612            "c",
613            "d",
614            "e",
615            "f",
616            "g",
617            "h",
618            "i",
619            "j",
620            "k",
621            "l",
622        ]
623        modified = [
624            ".notdef",
625            "uni0020",
626            "uni0061",
627            "uni0062",
628            "uni0063",
629            "uni0064",
630            "uni0065",
631            "uni0066",
632            "uni0067",
633            "uni0068",
634            "uni0069",
635            "uni006A",
636            "uni006B",
637            "uni006C",
638        ]
639
640        result = compileTTF(testufo)
641        assert result.getGlyphOrder() == original
642
643        result = compileTTF(testufo, useProductionNames=True)
644        assert result.getGlyphOrder() == modified
645
646        testufo.lib[USE_PRODUCTION_NAMES] = True
647        result = compileTTF(testufo)
648        assert result.getGlyphOrder() == modified
649
650    def test_postprocess_production_names_no_notdef(self, testufo):
651        import ufo2ft
652
653        del testufo[".notdef"]
654        assert ".notdef" not in testufo
655        result = compileTTF(testufo, useProductionNames=False)
656        assert ".notdef" in result.getGlyphOrder()
657
658        pp = ufo2ft.postProcessor.PostProcessor(result, testufo, glyphSet=None)
659        try:
660            f = pp.process(useProductionNames=True)
661        except Exception as e:
662            pytest.xfail("Unexpected exception: " + str(e))
663        assert ".notdef" in f.getGlyphOrder()
664
665    CUSTOM_POSTSCRIPT_NAMES = {
666        ".notdef": ".notdef",
667        "space": "foo",
668        "a": "bar",
669        "b": "baz",
670        "c": "meh",
671        "d": "doh",
672        "e": "bim",
673        "f": "bum",
674        "g": "bam",
675        "h": "bib",
676        "i": "bob",
677        "j": "bub",
678        "k": "kkk",
679        "l": "lll",
680    }
681
682    @pytest.mark.parametrize("use_production_names", [None, True])
683    def test_compile_with_custom_postscript_names(self, testufo, use_production_names):
684        testufo.lib["public.postscriptNames"] = self.CUSTOM_POSTSCRIPT_NAMES
685        result = compileTTF(testufo, useProductionNames=use_production_names)
686        assert sorted(result.getGlyphOrder()) == sorted(
687            self.CUSTOM_POSTSCRIPT_NAMES.values()
688        )
689
690    @pytest.mark.parametrize("use_production_names", [None, True])
691    def test_compile_with_custom_postscript_names_notdef_preserved(
692        self, testufo, use_production_names
693    ):
694        custom_names = dict(self.CUSTOM_POSTSCRIPT_NAMES)
695        del custom_names[".notdef"]
696        testufo.lib["public.postscriptNames"] = custom_names
697        result = compileTTF(testufo, useProductionNames=use_production_names)
698        assert result.getGlyphOrder() == [
699            ".notdef",
700            "foo",
701            "bar",
702            "baz",
703            "meh",
704            "doh",
705            "bim",
706            "bum",
707            "bam",
708            "bib",
709            "bob",
710            "bub",
711            "kkk",
712            "lll",
713        ]
714
715    def test_warn_name_exceeds_max_length(self, testufo, caplog):
716        long_name = 64 * "a"
717        testufo.newGlyph(long_name)
718
719        with caplog.at_level(logging.WARNING, logger="ufo2ft.postProcessor"):
720            result = compileTTF(testufo, useProductionNames=True)
721
722        assert "length exceeds 63 characters" in caplog.text
723        assert long_name in result.getGlyphOrder()
724
725    def test_duplicate_glyph_names(self, testufo):
726        order = ["ab", "ab.1", "a-b", "a/b", "ba"]
727        testufo.lib["public.glyphOrder"] = order
728        testufo.lib["public.postscriptNames"] = {"ba": "ab"}
729        for name in order:
730            if name not in testufo:
731                testufo.newGlyph(name)
732
733        result = compileTTF(testufo, useProductionNames=True).getGlyphOrder()
734
735        assert result[1] == "ab"
736        assert result[2] == "ab.1"
737        assert result[3] == "ab.2"
738        assert result[4] == "ab.3"
739        assert result[5] == "ab.4"
740
741    def test_too_long_production_name(self, testufo):
742        name = "_".join(("a",) * 16)
743        testufo.newGlyph(name)
744
745        result = compileTTF(testufo, useProductionNames=True).getGlyphOrder()
746
747        # the production name uniXXXX would exceed the max length so the
748        # original name is used
749        assert name in result
750
751
752ASCII = [unichr(c) for c in range(0x20, 0x7E)]
753
754
755@pytest.mark.parametrize(
756    "unicodes, expected",
757    [
758        [ASCII + ["Þ"], {0}],  # Latin 1
759        [ASCII + ["Ľ"], {1}],  # Latin 2: Eastern Europe
760        [ASCII + ["Ľ", "┤"], {1, 58}],  # Latin 2
761        [["Б"], {2}],  # Cyrillic
762        [["Б", "Ѕ", "┤"], {2, 57}],  # IBM Cyrillic
763        [["Б", "╜", "┤"], {2, 49}],  # MS-DOS Russian
764        [["Ά"], {3}],  # Greek
765        [["Ά", "½", "┤"], {3, 48}],  # IBM Greek
766        [["Ά", "√", "┤"], {3, 60}],  # Greek, former 437 G
767        [ASCII + ["İ"], {4}],  # Turkish
768        [ASCII + ["İ", "┤"], {4, 56}],  # IBM turkish
769        [["א"], {5}],  # Hebrew
770        [["א", "√", "┤"], {5, 53}],  # Hebrew
771        [["ر"], {6}],  # Arabic
772        [["ر", "√"], {6, 51}],  # Arabic
773        [["ر", "√", "┤"], {6, 51, 61}],  # Arabic; ASMO 708
774        [ASCII + ["ŗ"], {7}],  # Windows Baltic
775        [ASCII + ["ŗ", "┤"], {7, 59}],  # MS-DOS Baltic
776        [ASCII + ["₫"], {8}],  # Vietnamese
777        [["ๅ"], {16}],  # Thai
778        [["エ"], {17}],  # JIS/Japan
779        [["ㄅ"], {18}],  # Chinese: Simplified chars
780        [["ㄱ"], {19}],  # Korean wansung
781        [["央"], {20}],  # Chinese: Traditional chars
782        [["곴"], {21}],  # Korean Johab
783        [ASCII + ["♥"], {30}],  # OEM Character Set
784        [ASCII + ["þ", "┤"], {54}],  # MS-DOS Icelandic
785        [ASCII + ["╚"], {62, 63}],  # WE/Latin 1
786        [ASCII + ["┤", "√", "Å"], {50}],  # MS-DOS Nordic
787        [ASCII + ["┤", "√", "é"], {52}],  # MS-DOS Canadian French
788        [ASCII + ["┤", "√", "õ"], {55}],  # MS-DOS Portuguese
789        [ASCII + ["‰", "∑"], {29}],  # Macintosh Character Set (US Roman)
790        [[" ", "0", "1", "2", "අ"], {0}],  # always fallback to Latin 1
791    ],
792)
793def test_calcCodePageRanges(emptyufo, unicodes, expected):
794    font = emptyufo
795    for i, c in enumerate(unicodes):
796        font.newGlyph("glyph%d" % i).unicode = byteord(c)
797
798    compiler = OutlineOTFCompiler(font)
799    compiler.compile()
800
801    assert compiler.otf["OS/2"].ulCodePageRange1 == intListToNum(
802        expected, start=0, length=32
803    )
804    assert compiler.otf["OS/2"].ulCodePageRange2 == intListToNum(
805        expected, start=32, length=32
806    )
807
808
809if __name__ == "__main__":
810    import sys
811
812    sys.exit(pytest.main(sys.argv))
813