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