From 4febf38be2dd3fbc169f8a76b48f566880a78b83 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Thu, 7 May 2020 11:06:51 +0200 Subject: [PATCH] [ttLib.name] Add nameTable.findMultilingualName() method (#1921) * add nameTable.findMultilingualName(), to find an existing multilingual name * Make addMultilingualName() reuse nameIDs if possible when asking for a new nameID, by calling findMultilingualName() --- Lib/fontTools/ttLib/tables/_n_a_m_e.py | 58 +++++++++++++++++++++++++- Tests/ttLib/tables/_n_a_m_e_test.py | 42 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/ttLib/tables/_n_a_m_e.py b/Lib/fontTools/ttLib/tables/_n_a_m_e.py index e9ff2151d..ec5d07eeb 100644 --- a/Lib/fontTools/ttLib/tables/_n_a_m_e.py +++ b/Lib/fontTools/ttLib/tables/_n_a_m_e.py @@ -184,6 +184,57 @@ class table__n_a_m_e(DefaultTable.DefaultTable): raise ValueError("nameID must be less than 32768") return nameID + def findMultilingualName(self, names, windows=True, mac=True): + """Return the name ID of an existing multilingual name that + matches the 'names' dictionary, or None if not found. + + 'names' is a dictionary with the name in multiple languages, + such as {'en': 'Pale', 'de': 'Blaß', 'de-CH': 'Blass'}. + The keys can be arbitrary IETF BCP 47 language codes; + the values are Unicode strings. + + If 'windows' is True, the returned name ID is guaranteed + exist for all requested languages for platformID=3 and + platEncID=1. + If 'mac' is True, the returned name ID is guaranteed to exist + for all requested languages for platformID=1 and platEncID=0. + """ + # Gather the set of requested + # (string, platformID, platEncID, langID) + # tuples + reqNameSet = set() + for lang, name in sorted(names.items()): + if windows: + windowsName = _makeWindowsName(name, None, lang) + if windowsName is not None: + reqNameSet.add((windowsName.string, + windowsName.platformID, + windowsName.platEncID, + windowsName.langID)) + if mac: + macName = _makeMacName(name, None, lang) + if macName is not None: + reqNameSet.add((macName.string, + macName.platformID, + macName.platEncID, + macName.langID)) + + # Collect matching name IDs + matchingNames = dict() + for name in self.names: + key = (name.string, name.platformID, + name.platEncID, name.langID) + if key in reqNameSet: + nameSet = matchingNames.setdefault(name.nameID, set()) + nameSet.add(key) + + # Return the first name ID that defines all requested strings + for nameID, nameSet in sorted(matchingNames.items()): + if nameSet == reqNameSet: + return nameID + + return None # not found + def addMultilingualName(self, names, ttFont=None, nameID=None, windows=True, mac=True): """Add a multilingual name, returning its name ID @@ -199,7 +250,8 @@ class table__n_a_m_e(DefaultTable.DefaultTable): names that otherwise cannot get encoded at all. 'nameID' is the name ID to be used, or None to let the library - pick an unused name ID. + find an existing set of name records that match, or pick an + unused name ID. If 'windows' is True, a platformID=3 name record will be added. If 'mac' is True, a platformID=1 name record will be added. @@ -207,6 +259,10 @@ class table__n_a_m_e(DefaultTable.DefaultTable): if not hasattr(self, 'names'): self.names = [] if nameID is None: + # Reuse nameID if possible + nameID = self.findMultilingualName(names, windows=windows, mac=mac) + if nameID is not None: + return nameID nameID = self._findUnusedNameID() # TODO: Should minimize BCP 47 language codes. # https://github.com/fonttools/fonttools/issues/930 diff --git a/Tests/ttLib/tables/_n_a_m_e_test.py b/Tests/ttLib/tables/_n_a_m_e_test.py index d770a5231..5f5c965ce 100644 --- a/Tests/ttLib/tables/_n_a_m_e_test.py +++ b/Tests/ttLib/tables/_n_a_m_e_test.py @@ -144,6 +144,48 @@ class NameTableTest(unittest.TestCase): rec2 = table.getName(2, 1, 0, 0) self.assertEqual(str(rec2), "Regular") + @staticmethod + def _get_test_names(): + names = { + "en": "Width", + "de-CH": "Breite", + "gsw-LI": "Bräiti", + } + namesSubSet = names.copy() + del namesSubSet["gsw-LI"] + namesSuperSet = names.copy() + namesSuperSet["nl"] = "Breedte" + return names, namesSubSet, namesSuperSet + + def test_findMultilingualName(self): + table = table__n_a_m_e() + names, namesSubSet, namesSuperSet = self._get_test_names() + nameID = table.addMultilingualName(names) + assert nameID is not None + self.assertEqual(nameID, table.findMultilingualName(names)) + self.assertEqual(nameID, table.findMultilingualName(namesSubSet)) + self.assertEqual(None, table.findMultilingualName(namesSuperSet)) + + def test_addMultilingualNameReuse(self): + table = table__n_a_m_e() + names, namesSubSet, namesSuperSet = self._get_test_names() + nameID = table.addMultilingualName(names) + assert nameID is not None + self.assertEqual(nameID, table.addMultilingualName(names)) + self.assertEqual(nameID, table.addMultilingualName(namesSubSet)) + self.assertNotEqual(None, table.addMultilingualName(namesSuperSet)) + + def test_findMultilingualNameNoMac(self): + table = table__n_a_m_e() + names, namesSubSet, namesSuperSet = self._get_test_names() + nameID = table.addMultilingualName(names, mac=False) + assert nameID is not None + self.assertEqual(nameID, table.findMultilingualName(names, mac=False)) + self.assertEqual(None, table.findMultilingualName(names)) + self.assertEqual(nameID, table.findMultilingualName(namesSubSet, mac=False)) + self.assertEqual(None, table.findMultilingualName(namesSubSet)) + self.assertEqual(None, table.findMultilingualName(namesSuperSet)) + def test_addMultilingualName(self): # Microsoft Windows has language codes for “English” (en) # and for “Standard German as used in Switzerland” (de-CH).