xref: /aosp_15_r20/external/fonttools/Tests/merge/merge_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughesimport io
2*e1fe3e4aSElliott Hughesimport itertools
3*e1fe3e4aSElliott Hughesfrom fontTools import ttLib
4*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables._g_l_y_f import Glyph
5*e1fe3e4aSElliott Hughesfrom fontTools.fontBuilder import FontBuilder
6*e1fe3e4aSElliott Hughesfrom fontTools.merge import Merger, main as merge_main
7*e1fe3e4aSElliott Hughesimport difflib
8*e1fe3e4aSElliott Hughesimport os
9*e1fe3e4aSElliott Hughesimport re
10*e1fe3e4aSElliott Hughesimport shutil
11*e1fe3e4aSElliott Hughesimport sys
12*e1fe3e4aSElliott Hughesimport tempfile
13*e1fe3e4aSElliott Hughesimport unittest
14*e1fe3e4aSElliott Hughesimport pathlib
15*e1fe3e4aSElliott Hughesimport pytest
16*e1fe3e4aSElliott Hughes
17*e1fe3e4aSElliott Hughes
18*e1fe3e4aSElliott Hughesclass MergeIntegrationTest(unittest.TestCase):
19*e1fe3e4aSElliott Hughes    def setUp(self):
20*e1fe3e4aSElliott Hughes        self.tempdir = None
21*e1fe3e4aSElliott Hughes        self.num_tempfiles = 0
22*e1fe3e4aSElliott Hughes
23*e1fe3e4aSElliott Hughes    def tearDown(self):
24*e1fe3e4aSElliott Hughes        if self.tempdir:
25*e1fe3e4aSElliott Hughes            shutil.rmtree(self.tempdir)
26*e1fe3e4aSElliott Hughes
27*e1fe3e4aSElliott Hughes    @staticmethod
28*e1fe3e4aSElliott Hughes    def getpath(testfile):
29*e1fe3e4aSElliott Hughes        path, _ = os.path.split(__file__)
30*e1fe3e4aSElliott Hughes        return os.path.join(path, "data", testfile)
31*e1fe3e4aSElliott Hughes
32*e1fe3e4aSElliott Hughes    def temp_path(self, suffix):
33*e1fe3e4aSElliott Hughes        if not self.tempdir:
34*e1fe3e4aSElliott Hughes            self.tempdir = tempfile.mkdtemp()
35*e1fe3e4aSElliott Hughes        self.num_tempfiles += 1
36*e1fe3e4aSElliott Hughes        return os.path.join(self.tempdir, "tmp%d%s" % (self.num_tempfiles, suffix))
37*e1fe3e4aSElliott Hughes
38*e1fe3e4aSElliott Hughes    IGNORED_LINES_RE = re.compile(
39*e1fe3e4aSElliott Hughes        "^(<ttFont |    <(checkSumAdjustment|created|modified) ).*"
40*e1fe3e4aSElliott Hughes    )
41*e1fe3e4aSElliott Hughes
42*e1fe3e4aSElliott Hughes    def read_ttx(self, path):
43*e1fe3e4aSElliott Hughes        lines = []
44*e1fe3e4aSElliott Hughes        with open(path, "r", encoding="utf-8") as ttx:
45*e1fe3e4aSElliott Hughes            for line in ttx.readlines():
46*e1fe3e4aSElliott Hughes                # Elide lines with data that often change.
47*e1fe3e4aSElliott Hughes                if self.IGNORED_LINES_RE.match(line):
48*e1fe3e4aSElliott Hughes                    lines.append("\n")
49*e1fe3e4aSElliott Hughes                else:
50*e1fe3e4aSElliott Hughes                    lines.append(line.rstrip() + "\n")
51*e1fe3e4aSElliott Hughes        return lines
52*e1fe3e4aSElliott Hughes
53*e1fe3e4aSElliott Hughes    def expect_ttx(self, font, expected_ttx, tables=None):
54*e1fe3e4aSElliott Hughes        path = self.temp_path(suffix=".ttx")
55*e1fe3e4aSElliott Hughes        font.saveXML(path, tables=tables)
56*e1fe3e4aSElliott Hughes        actual = self.read_ttx(path)
57*e1fe3e4aSElliott Hughes        expected = self.read_ttx(expected_ttx)
58*e1fe3e4aSElliott Hughes        if actual != expected:
59*e1fe3e4aSElliott Hughes            for line in difflib.unified_diff(
60*e1fe3e4aSElliott Hughes                expected, actual, fromfile=expected_ttx, tofile=path
61*e1fe3e4aSElliott Hughes            ):
62*e1fe3e4aSElliott Hughes                sys.stdout.write(line)
63*e1fe3e4aSElliott Hughes            self.fail("TTX output is different from expected")
64*e1fe3e4aSElliott Hughes
65*e1fe3e4aSElliott Hughes    def compile_font(self, path, suffix):
66*e1fe3e4aSElliott Hughes        savepath = self.temp_path(suffix=suffix)
67*e1fe3e4aSElliott Hughes        font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
68*e1fe3e4aSElliott Hughes        font.importXML(path)
69*e1fe3e4aSElliott Hughes        font.save(savepath, reorderTables=None)
70*e1fe3e4aSElliott Hughes        return font, savepath
71*e1fe3e4aSElliott Hughes
72*e1fe3e4aSElliott Hughes    # -----
73*e1fe3e4aSElliott Hughes    # Tests
74*e1fe3e4aSElliott Hughes    # -----
75*e1fe3e4aSElliott Hughes
76*e1fe3e4aSElliott Hughes    def test_merge_cff(self):
77*e1fe3e4aSElliott Hughes        _, fontpath1 = self.compile_font(self.getpath("CFFFont1.ttx"), ".otf")
78*e1fe3e4aSElliott Hughes        _, fontpath2 = self.compile_font(self.getpath("CFFFont2.ttx"), ".otf")
79*e1fe3e4aSElliott Hughes        mergedpath = self.temp_path(".otf")
80*e1fe3e4aSElliott Hughes        merge_main([fontpath1, fontpath2, "--output-file=%s" % mergedpath])
81*e1fe3e4aSElliott Hughes        mergedfont = ttLib.TTFont(mergedpath)
82*e1fe3e4aSElliott Hughes        self.expect_ttx(mergedfont, self.getpath("CFFFont_expected.ttx"))
83*e1fe3e4aSElliott Hughes
84*e1fe3e4aSElliott Hughes
85*e1fe3e4aSElliott Hughesclass gaspMergeUnitTest(unittest.TestCase):
86*e1fe3e4aSElliott Hughes    def setUp(self):
87*e1fe3e4aSElliott Hughes        self.merger = Merger()
88*e1fe3e4aSElliott Hughes
89*e1fe3e4aSElliott Hughes        self.table1 = ttLib.newTable("gasp")
90*e1fe3e4aSElliott Hughes        self.table1.version = 1
91*e1fe3e4aSElliott Hughes        self.table1.gaspRange = {
92*e1fe3e4aSElliott Hughes            0x8: 0xA,
93*e1fe3e4aSElliott Hughes            0x10: 0x5,
94*e1fe3e4aSElliott Hughes        }
95*e1fe3e4aSElliott Hughes
96*e1fe3e4aSElliott Hughes        self.table2 = ttLib.newTable("gasp")
97*e1fe3e4aSElliott Hughes        self.table2.version = 1
98*e1fe3e4aSElliott Hughes        self.table2.gaspRange = {
99*e1fe3e4aSElliott Hughes            0x6: 0xB,
100*e1fe3e4aSElliott Hughes            0xFF: 0x4,
101*e1fe3e4aSElliott Hughes        }
102*e1fe3e4aSElliott Hughes
103*e1fe3e4aSElliott Hughes        self.result = ttLib.newTable("gasp")
104*e1fe3e4aSElliott Hughes
105*e1fe3e4aSElliott Hughes    def test_gasp_merge_basic(self):
106*e1fe3e4aSElliott Hughes        result = self.result.merge(self.merger, [self.table1, self.table2])
107*e1fe3e4aSElliott Hughes        self.assertEqual(result, self.table1)
108*e1fe3e4aSElliott Hughes
109*e1fe3e4aSElliott Hughes        result = self.result.merge(self.merger, [self.table2, self.table1])
110*e1fe3e4aSElliott Hughes        self.assertEqual(result, self.table2)
111*e1fe3e4aSElliott Hughes
112*e1fe3e4aSElliott Hughes    def test_gasp_merge_notImplemented(self):
113*e1fe3e4aSElliott Hughes        result = self.result.merge(self.merger, [NotImplemented, self.table1])
114*e1fe3e4aSElliott Hughes        self.assertEqual(result, NotImplemented)
115*e1fe3e4aSElliott Hughes
116*e1fe3e4aSElliott Hughes        result = self.result.merge(self.merger, [self.table1, NotImplemented])
117*e1fe3e4aSElliott Hughes        self.assertEqual(result, self.table1)
118*e1fe3e4aSElliott Hughes
119*e1fe3e4aSElliott Hughes
120*e1fe3e4aSElliott Hughesclass CmapMergeUnitTest(unittest.TestCase):
121*e1fe3e4aSElliott Hughes    def setUp(self):
122*e1fe3e4aSElliott Hughes        self.merger = Merger()
123*e1fe3e4aSElliott Hughes        self.table1 = ttLib.newTable("cmap")
124*e1fe3e4aSElliott Hughes        self.table2 = ttLib.newTable("cmap")
125*e1fe3e4aSElliott Hughes        self.mergedTable = ttLib.newTable("cmap")
126*e1fe3e4aSElliott Hughes        pass
127*e1fe3e4aSElliott Hughes
128*e1fe3e4aSElliott Hughes    def tearDown(self):
129*e1fe3e4aSElliott Hughes        pass
130*e1fe3e4aSElliott Hughes
131*e1fe3e4aSElliott Hughes    def makeSubtable(self, format, platformID, platEncID, cmap):
132*e1fe3e4aSElliott Hughes        module = ttLib.getTableModule("cmap")
133*e1fe3e4aSElliott Hughes        subtable = module.cmap_classes[format](format)
134*e1fe3e4aSElliott Hughes        (subtable.platformID, subtable.platEncID, subtable.language, subtable.cmap) = (
135*e1fe3e4aSElliott Hughes            platformID,
136*e1fe3e4aSElliott Hughes            platEncID,
137*e1fe3e4aSElliott Hughes            0,
138*e1fe3e4aSElliott Hughes            cmap,
139*e1fe3e4aSElliott Hughes        )
140*e1fe3e4aSElliott Hughes        return subtable
141*e1fe3e4aSElliott Hughes
142*e1fe3e4aSElliott Hughes    # 4-3-1 table merged with 12-3-10 table with no dupes with codepoints outside BMP
143*e1fe3e4aSElliott Hughes    def test_cmap_merge_no_dupes(self):
144*e1fe3e4aSElliott Hughes        table1 = self.table1
145*e1fe3e4aSElliott Hughes        table2 = self.table2
146*e1fe3e4aSElliott Hughes        mergedTable = self.mergedTable
147*e1fe3e4aSElliott Hughes
148*e1fe3e4aSElliott Hughes        cmap1 = {0x2603: "SNOWMAN"}
149*e1fe3e4aSElliott Hughes        table1.tables = [self.makeSubtable(4, 3, 1, cmap1)]
150*e1fe3e4aSElliott Hughes
151*e1fe3e4aSElliott Hughes        cmap2 = {0x26C4: "SNOWMAN WITHOUT SNOW"}
152*e1fe3e4aSElliott Hughes        cmap2Extended = {0x1F93C: "WRESTLERS"}
153*e1fe3e4aSElliott Hughes        cmap2Extended.update(cmap2)
154*e1fe3e4aSElliott Hughes        table2.tables = [
155*e1fe3e4aSElliott Hughes            self.makeSubtable(4, 3, 1, cmap2),
156*e1fe3e4aSElliott Hughes            self.makeSubtable(12, 3, 10, cmap2Extended),
157*e1fe3e4aSElliott Hughes        ]
158*e1fe3e4aSElliott Hughes
159*e1fe3e4aSElliott Hughes        self.merger.alternateGlyphsPerFont = [{}, {}]
160*e1fe3e4aSElliott Hughes        mergedTable.merge(self.merger, [table1, table2])
161*e1fe3e4aSElliott Hughes
162*e1fe3e4aSElliott Hughes        expectedCmap = cmap2.copy()
163*e1fe3e4aSElliott Hughes        expectedCmap.update(cmap1)
164*e1fe3e4aSElliott Hughes        expectedCmapExtended = cmap2Extended.copy()
165*e1fe3e4aSElliott Hughes        expectedCmapExtended.update(cmap1)
166*e1fe3e4aSElliott Hughes        self.assertEqual(mergedTable.numSubTables, 2)
167*e1fe3e4aSElliott Hughes        self.assertEqual(
168*e1fe3e4aSElliott Hughes            [
169*e1fe3e4aSElliott Hughes                (table.format, table.platformID, table.platEncID, table.language)
170*e1fe3e4aSElliott Hughes                for table in mergedTable.tables
171*e1fe3e4aSElliott Hughes            ],
172*e1fe3e4aSElliott Hughes            [(4, 3, 1, 0), (12, 3, 10, 0)],
173*e1fe3e4aSElliott Hughes        )
174*e1fe3e4aSElliott Hughes        self.assertEqual(mergedTable.tables[0].cmap, expectedCmap)
175*e1fe3e4aSElliott Hughes        self.assertEqual(mergedTable.tables[1].cmap, expectedCmapExtended)
176*e1fe3e4aSElliott Hughes
177*e1fe3e4aSElliott Hughes    # Tests Issue #322
178*e1fe3e4aSElliott Hughes    def test_cmap_merge_three_dupes(self):
179*e1fe3e4aSElliott Hughes        table1 = self.table1
180*e1fe3e4aSElliott Hughes        table2 = self.table2
181*e1fe3e4aSElliott Hughes        mergedTable = self.mergedTable
182*e1fe3e4aSElliott Hughes
183*e1fe3e4aSElliott Hughes        cmap1 = {0x20: "space#0", 0xA0: "space#0"}
184*e1fe3e4aSElliott Hughes        table1.tables = [self.makeSubtable(4, 3, 1, cmap1)]
185*e1fe3e4aSElliott Hughes        cmap2 = {0x20: "space#1", 0xA0: "uni00A0#1"}
186*e1fe3e4aSElliott Hughes        table2.tables = [self.makeSubtable(4, 3, 1, cmap2)]
187*e1fe3e4aSElliott Hughes
188*e1fe3e4aSElliott Hughes        self.merger.duplicateGlyphsPerFont = [{}, {}]
189*e1fe3e4aSElliott Hughes        mergedTable.merge(self.merger, [table1, table2])
190*e1fe3e4aSElliott Hughes
191*e1fe3e4aSElliott Hughes        expectedCmap = cmap1.copy()
192*e1fe3e4aSElliott Hughes        self.assertEqual(mergedTable.numSubTables, 1)
193*e1fe3e4aSElliott Hughes        table = mergedTable.tables[0]
194*e1fe3e4aSElliott Hughes        self.assertEqual(
195*e1fe3e4aSElliott Hughes            (table.format, table.platformID, table.platEncID, table.language),
196*e1fe3e4aSElliott Hughes            (4, 3, 1, 0),
197*e1fe3e4aSElliott Hughes        )
198*e1fe3e4aSElliott Hughes        self.assertEqual(table.cmap, expectedCmap)
199*e1fe3e4aSElliott Hughes        self.assertEqual(
200*e1fe3e4aSElliott Hughes            self.merger.duplicateGlyphsPerFont, [{}, {"space#0": "space#1"}]
201*e1fe3e4aSElliott Hughes        )
202*e1fe3e4aSElliott Hughes
203*e1fe3e4aSElliott Hughes
204*e1fe3e4aSElliott Hughesdef _compile(ttFont):
205*e1fe3e4aSElliott Hughes    buf = io.BytesIO()
206*e1fe3e4aSElliott Hughes    ttFont.save(buf)
207*e1fe3e4aSElliott Hughes    buf.seek(0)
208*e1fe3e4aSElliott Hughes    return buf
209*e1fe3e4aSElliott Hughes
210*e1fe3e4aSElliott Hughes
211*e1fe3e4aSElliott Hughesdef _make_fontfile_with_OS2(*, version, **kwargs):
212*e1fe3e4aSElliott Hughes    upem = 1000
213*e1fe3e4aSElliott Hughes    glyphOrder = [".notdef", "a"]
214*e1fe3e4aSElliott Hughes    cmap = {0x61: "a"}
215*e1fe3e4aSElliott Hughes    glyphs = {gn: Glyph() for gn in glyphOrder}
216*e1fe3e4aSElliott Hughes    hmtx = {gn: (500, 0) for gn in glyphOrder}
217*e1fe3e4aSElliott Hughes    names = {"familyName": "TestOS2", "styleName": "Regular"}
218*e1fe3e4aSElliott Hughes
219*e1fe3e4aSElliott Hughes    fb = FontBuilder(unitsPerEm=upem)
220*e1fe3e4aSElliott Hughes    fb.setupGlyphOrder(glyphOrder)
221*e1fe3e4aSElliott Hughes    fb.setupCharacterMap(cmap)
222*e1fe3e4aSElliott Hughes    fb.setupGlyf(glyphs)
223*e1fe3e4aSElliott Hughes    fb.setupHorizontalMetrics(hmtx)
224*e1fe3e4aSElliott Hughes    fb.setupHorizontalHeader()
225*e1fe3e4aSElliott Hughes    fb.setupNameTable(names)
226*e1fe3e4aSElliott Hughes    fb.setupOS2(version=version, **kwargs)
227*e1fe3e4aSElliott Hughes
228*e1fe3e4aSElliott Hughes    return _compile(fb.font)
229*e1fe3e4aSElliott Hughes
230*e1fe3e4aSElliott Hughes
231*e1fe3e4aSElliott Hughesdef _merge_and_recompile(fontfiles, options=None):
232*e1fe3e4aSElliott Hughes    merger = Merger(options)
233*e1fe3e4aSElliott Hughes    merged = merger.merge(fontfiles)
234*e1fe3e4aSElliott Hughes    buf = _compile(merged)
235*e1fe3e4aSElliott Hughes    return ttLib.TTFont(buf)
236*e1fe3e4aSElliott Hughes
237*e1fe3e4aSElliott Hughes
238*e1fe3e4aSElliott Hughes@pytest.mark.parametrize("v1, v2", list(itertools.permutations(range(5 + 1), 2)))
239*e1fe3e4aSElliott Hughesdef test_merge_OS2_mixed_versions(v1, v2):
240*e1fe3e4aSElliott Hughes    # https://github.com/fonttools/fonttools/issues/1865
241*e1fe3e4aSElliott Hughes    fontfiles = [
242*e1fe3e4aSElliott Hughes        _make_fontfile_with_OS2(version=v1),
243*e1fe3e4aSElliott Hughes        _make_fontfile_with_OS2(version=v2),
244*e1fe3e4aSElliott Hughes    ]
245*e1fe3e4aSElliott Hughes    merged = _merge_and_recompile(fontfiles)
246*e1fe3e4aSElliott Hughes    assert merged["OS/2"].version == max(v1, v2)
247*e1fe3e4aSElliott Hughes
248*e1fe3e4aSElliott Hughes
249*e1fe3e4aSElliott Hughesif __name__ == "__main__":
250*e1fe3e4aSElliott Hughes    import sys
251*e1fe3e4aSElliott Hughes
252*e1fe3e4aSElliott Hughes    sys.exit(unittest.main())
253