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