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