253 lines
7.7 KiB
Python
253 lines
7.7 KiB
Python
import io
|
|
import itertools
|
|
from fontTools import ttLib
|
|
from fontTools.ttLib.tables._g_l_y_f import Glyph
|
|
from fontTools.fontBuilder import FontBuilder
|
|
from fontTools.merge import Merger, main as merge_main
|
|
import difflib
|
|
import os
|
|
import re
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
import pathlib
|
|
import pytest
|
|
|
|
|
|
class MergeIntegrationTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tempdir = None
|
|
self.num_tempfiles = 0
|
|
|
|
def tearDown(self):
|
|
if self.tempdir:
|
|
shutil.rmtree(self.tempdir)
|
|
|
|
@staticmethod
|
|
def getpath(testfile):
|
|
path, _ = os.path.split(__file__)
|
|
return os.path.join(path, "data", testfile)
|
|
|
|
def temp_path(self, suffix):
|
|
if not self.tempdir:
|
|
self.tempdir = tempfile.mkdtemp()
|
|
self.num_tempfiles += 1
|
|
return os.path.join(self.tempdir, "tmp%d%s" % (self.num_tempfiles, suffix))
|
|
|
|
IGNORED_LINES_RE = re.compile(
|
|
"^(<ttFont | <(checkSumAdjustment|created|modified) ).*"
|
|
)
|
|
|
|
def read_ttx(self, path):
|
|
lines = []
|
|
with open(path, "r", encoding="utf-8") as ttx:
|
|
for line in ttx.readlines():
|
|
# Elide lines with data that often change.
|
|
if self.IGNORED_LINES_RE.match(line):
|
|
lines.append("\n")
|
|
else:
|
|
lines.append(line.rstrip() + "\n")
|
|
return lines
|
|
|
|
def expect_ttx(self, font, expected_ttx, tables=None):
|
|
path = self.temp_path(suffix=".ttx")
|
|
font.saveXML(path, tables=tables)
|
|
actual = self.read_ttx(path)
|
|
expected = self.read_ttx(expected_ttx)
|
|
if actual != expected:
|
|
for line in difflib.unified_diff(
|
|
expected, actual, fromfile=expected_ttx, tofile=path
|
|
):
|
|
sys.stdout.write(line)
|
|
self.fail("TTX output is different from expected")
|
|
|
|
def compile_font(self, path, suffix):
|
|
savepath = self.temp_path(suffix=suffix)
|
|
font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
|
|
font.importXML(path)
|
|
font.save(savepath, reorderTables=None)
|
|
return font, savepath
|
|
|
|
# -----
|
|
# Tests
|
|
# -----
|
|
|
|
def test_merge_cff(self):
|
|
_, fontpath1 = self.compile_font(self.getpath("CFFFont1.ttx"), ".otf")
|
|
_, fontpath2 = self.compile_font(self.getpath("CFFFont2.ttx"), ".otf")
|
|
mergedpath = self.temp_path(".otf")
|
|
merge_main([fontpath1, fontpath2, "--output-file=%s" % mergedpath])
|
|
mergedfont = ttLib.TTFont(mergedpath)
|
|
self.expect_ttx(mergedfont, self.getpath("CFFFont_expected.ttx"))
|
|
|
|
|
|
class gaspMergeUnitTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.merger = Merger()
|
|
|
|
self.table1 = ttLib.newTable("gasp")
|
|
self.table1.version = 1
|
|
self.table1.gaspRange = {
|
|
0x8: 0xA,
|
|
0x10: 0x5,
|
|
}
|
|
|
|
self.table2 = ttLib.newTable("gasp")
|
|
self.table2.version = 1
|
|
self.table2.gaspRange = {
|
|
0x6: 0xB,
|
|
0xFF: 0x4,
|
|
}
|
|
|
|
self.result = ttLib.newTable("gasp")
|
|
|
|
def test_gasp_merge_basic(self):
|
|
result = self.result.merge(self.merger, [self.table1, self.table2])
|
|
self.assertEqual(result, self.table1)
|
|
|
|
result = self.result.merge(self.merger, [self.table2, self.table1])
|
|
self.assertEqual(result, self.table2)
|
|
|
|
def test_gasp_merge_notImplemented(self):
|
|
result = self.result.merge(self.merger, [NotImplemented, self.table1])
|
|
self.assertEqual(result, NotImplemented)
|
|
|
|
result = self.result.merge(self.merger, [self.table1, NotImplemented])
|
|
self.assertEqual(result, self.table1)
|
|
|
|
|
|
class CmapMergeUnitTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.merger = Merger()
|
|
self.table1 = ttLib.newTable("cmap")
|
|
self.table2 = ttLib.newTable("cmap")
|
|
self.mergedTable = ttLib.newTable("cmap")
|
|
pass
|
|
|
|
def tearDown(self):
|
|
pass
|
|
|
|
def makeSubtable(self, format, platformID, platEncID, cmap):
|
|
module = ttLib.getTableModule("cmap")
|
|
subtable = module.cmap_classes[format](format)
|
|
(subtable.platformID, subtable.platEncID, subtable.language, subtable.cmap) = (
|
|
platformID,
|
|
platEncID,
|
|
0,
|
|
cmap,
|
|
)
|
|
return subtable
|
|
|
|
# 4-3-1 table merged with 12-3-10 table with no dupes with codepoints outside BMP
|
|
def test_cmap_merge_no_dupes(self):
|
|
table1 = self.table1
|
|
table2 = self.table2
|
|
mergedTable = self.mergedTable
|
|
|
|
cmap1 = {0x2603: "SNOWMAN"}
|
|
table1.tables = [self.makeSubtable(4, 3, 1, cmap1)]
|
|
|
|
cmap2 = {0x26C4: "SNOWMAN WITHOUT SNOW"}
|
|
cmap2Extended = {0x1F93C: "WRESTLERS"}
|
|
cmap2Extended.update(cmap2)
|
|
table2.tables = [
|
|
self.makeSubtable(4, 3, 1, cmap2),
|
|
self.makeSubtable(12, 3, 10, cmap2Extended),
|
|
]
|
|
|
|
self.merger.alternateGlyphsPerFont = [{}, {}]
|
|
mergedTable.merge(self.merger, [table1, table2])
|
|
|
|
expectedCmap = cmap2.copy()
|
|
expectedCmap.update(cmap1)
|
|
expectedCmapExtended = cmap2Extended.copy()
|
|
expectedCmapExtended.update(cmap1)
|
|
self.assertEqual(mergedTable.numSubTables, 2)
|
|
self.assertEqual(
|
|
[
|
|
(table.format, table.platformID, table.platEncID, table.language)
|
|
for table in mergedTable.tables
|
|
],
|
|
[(4, 3, 1, 0), (12, 3, 10, 0)],
|
|
)
|
|
self.assertEqual(mergedTable.tables[0].cmap, expectedCmap)
|
|
self.assertEqual(mergedTable.tables[1].cmap, expectedCmapExtended)
|
|
|
|
# Tests Issue #322
|
|
def test_cmap_merge_three_dupes(self):
|
|
table1 = self.table1
|
|
table2 = self.table2
|
|
mergedTable = self.mergedTable
|
|
|
|
cmap1 = {0x20: "space#0", 0xA0: "space#0"}
|
|
table1.tables = [self.makeSubtable(4, 3, 1, cmap1)]
|
|
cmap2 = {0x20: "space#1", 0xA0: "uni00A0#1"}
|
|
table2.tables = [self.makeSubtable(4, 3, 1, cmap2)]
|
|
|
|
self.merger.duplicateGlyphsPerFont = [{}, {}]
|
|
mergedTable.merge(self.merger, [table1, table2])
|
|
|
|
expectedCmap = cmap1.copy()
|
|
self.assertEqual(mergedTable.numSubTables, 1)
|
|
table = mergedTable.tables[0]
|
|
self.assertEqual(
|
|
(table.format, table.platformID, table.platEncID, table.language),
|
|
(4, 3, 1, 0),
|
|
)
|
|
self.assertEqual(table.cmap, expectedCmap)
|
|
self.assertEqual(
|
|
self.merger.duplicateGlyphsPerFont, [{}, {"space#0": "space#1"}]
|
|
)
|
|
|
|
|
|
def _compile(ttFont):
|
|
buf = io.BytesIO()
|
|
ttFont.save(buf)
|
|
buf.seek(0)
|
|
return buf
|
|
|
|
|
|
def _make_fontfile_with_OS2(*, version, **kwargs):
|
|
upem = 1000
|
|
glyphOrder = [".notdef", "a"]
|
|
cmap = {0x61: "a"}
|
|
glyphs = {gn: Glyph() for gn in glyphOrder}
|
|
hmtx = {gn: (500, 0) for gn in glyphOrder}
|
|
names = {"familyName": "TestOS2", "styleName": "Regular"}
|
|
|
|
fb = FontBuilder(unitsPerEm=upem)
|
|
fb.setupGlyphOrder(glyphOrder)
|
|
fb.setupCharacterMap(cmap)
|
|
fb.setupGlyf(glyphs)
|
|
fb.setupHorizontalMetrics(hmtx)
|
|
fb.setupHorizontalHeader()
|
|
fb.setupNameTable(names)
|
|
fb.setupOS2(version=version, **kwargs)
|
|
|
|
return _compile(fb.font)
|
|
|
|
|
|
def _merge_and_recompile(fontfiles, options=None):
|
|
merger = Merger(options)
|
|
merged = merger.merge(fontfiles)
|
|
buf = _compile(merged)
|
|
return ttLib.TTFont(buf)
|
|
|
|
|
|
@pytest.mark.parametrize("v1, v2", list(itertools.permutations(range(5 + 1), 2)))
|
|
def test_merge_OS2_mixed_versions(v1, v2):
|
|
# https://github.com/fonttools/fonttools/issues/1865
|
|
fontfiles = [
|
|
_make_fontfile_with_OS2(version=v1),
|
|
_make_fontfile_with_OS2(version=v2),
|
|
]
|
|
merged = _merge_and_recompile(fontfiles)
|
|
assert merged["OS/2"].version == max(v1, v2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(unittest.main())
|