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