diff options
Diffstat (limited to 'Lib/fontTools/ttLib/tables/_n_a_m_e.py')
-rw-r--r-- | Lib/fontTools/ttLib/tables/_n_a_m_e.py | 160 |
1 files changed, 126 insertions, 34 deletions
diff --git a/Lib/fontTools/ttLib/tables/_n_a_m_e.py b/Lib/fontTools/ttLib/tables/_n_a_m_e.py index 488c4ea5..206469de 100644 --- a/Lib/fontTools/ttLib/tables/_n_a_m_e.py +++ b/Lib/fontTools/ttLib/tables/_n_a_m_e.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function, division, absolute_import -from __future__ import unicode_literals -from fontTools.misc.py23 import * +from fontTools.misc.py23 import bytechr, byteord, bytesjoin, strjoin, tobytes, tostr from fontTools.misc import sstruct from fontTools.misc.textTools import safeEval from fontTools.misc.encodingTools import getEncoding @@ -135,7 +133,7 @@ class table__n_a_m_e(DefaultTable.DefaultTable): """ if not hasattr(self, 'names'): self.names = [] - if not isinstance(string, unicode): + if not isinstance(string, str): if isinstance(string, bytes): log.warning( "name string is bytes, ensure it's correctly encoded: %r", string) @@ -149,6 +147,31 @@ class table__n_a_m_e(DefaultTable.DefaultTable): else: self.names.append(makeName(string, nameID, platformID, platEncID, langID)) + def removeNames(self, nameID=None, platformID=None, platEncID=None, langID=None): + """Remove any name records identified by the given combination of 'nameID', + 'platformID', 'platEncID' and 'langID'. + """ + args = { + argName: argValue + for argName, argValue in ( + ("nameID", nameID), + ("platformID", platformID), + ("platEncID", platEncID), + ("langID", langID), + ) + if argValue is not None + } + if not args: + # no arguments, nothing to do + return + self.names = [ + rec for rec in self.names + if any( + argValue != getattr(rec, argName) + for argName, argValue in args.items() + ) + ] + def _findUnusedNameID(self, minNameID=256): """Finds an unused name id. @@ -161,8 +184,65 @@ 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, minNameID=0): + """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. + + The returned name ID will not be less than the 'minNameID' + argument. + """ + # 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: + try: + key = (name.toUnicode(), name.platformID, + name.platEncID, name.langID) + except UnicodeDecodeError: + continue + if key in reqNameSet and name.nameID >= minNameID: + 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): + windows=True, mac=True, minNameID=0): """Add a multilingual name, returning its name ID 'names' is a dictionary with the name in multiple languages, @@ -176,14 +256,23 @@ 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. + + If the 'nameID' argument is None, the created nameID will not + be less than the 'minNameID' argument. """ if not hasattr(self, 'names'): self.names = [] if nameID is None: + # Reuse nameID if possible + nameID = self.findMultilingualName( + names, windows=windows, mac=mac, minNameID=minNameID) + if nameID is not None: + return nameID nameID = self._findUnusedNameID() # TODO: Should minimize BCP 47 language codes. # https://github.com/fonttools/fonttools/issues/930 @@ -221,10 +310,9 @@ class table__n_a_m_e(DefaultTable.DefaultTable): "'platforms' must contain at least one (platformID, platEncID, langID) tuple" if not hasattr(self, 'names'): self.names = [] - if not isinstance(string, unicode): + if not isinstance(string, str): raise TypeError( - "expected %s, found %s: %r" % ( - unicode.__name__, type(string).__name__,string )) + "expected str, found %s: %r" % (type(string).__name__, string)) nameID = self._findUnusedNameID(minNameID + 1) for platformID, platEncID, langID in platforms: self.names.append(makeName(string, nameID, platformID, platEncID, langID)) @@ -352,7 +440,7 @@ class NameRecord(object): encoding = self.getEncoding() string = self.string - if encoding == 'utf_16_be' and len(string) % 2 == 1: + if isinstance(string, bytes) and encoding == 'utf_16_be' and len(string) % 2 == 1: # Recover badly encoded UTF-16 strings that have an odd number of bytes: # - If the last byte is zero, drop it. Otherwise, # - If all the odd bytes are zero and all the even bytes are ASCII, @@ -368,7 +456,7 @@ class NameRecord(object): elif byteord(string[0]) == 0 and all(isascii(byteord(b)) for b in string[1:]): string = bytesjoin(b'\0'+bytechr(byteord(b)) for b in string[1:]) - string = tounicode(string, encoding=encoding, errors=errors) + string = tostr(string, encoding=encoding, errors=errors) # If decoded strings still looks like UTF-16BE, it suggests a double-encoding. # Fix it up. @@ -392,13 +480,7 @@ class NameRecord(object): """ return tobytes(self.string, encoding=self.getEncoding(), errors=errors) - def toStr(self, errors='strict'): - if str == bytes: - # python 2 - return self.toBytes(errors) - else: - # python 3 - return self.toUnicode(errors) + toStr = toUnicode def toXML(self, writer, ttFont): try: @@ -442,22 +524,32 @@ class NameRecord(object): if type(self) != type(other): return NotImplemented - # implemented so that list.sort() sorts according to the spec. - selfTuple = ( - getattr(self, "platformID", None), - getattr(self, "platEncID", None), - getattr(self, "langID", None), - getattr(self, "nameID", None), - getattr(self, "string", None), - ) - otherTuple = ( - getattr(other, "platformID", None), - getattr(other, "platEncID", None), - getattr(other, "langID", None), - getattr(other, "nameID", None), - getattr(other, "string", None), - ) - return selfTuple < otherTuple + try: + # implemented so that list.sort() sorts according to the spec. + selfTuple = ( + self.platformID, + self.platEncID, + self.langID, + self.nameID, + self.toBytes(), + ) + otherTuple = ( + other.platformID, + other.platEncID, + other.langID, + other.nameID, + other.toBytes(), + ) + return selfTuple < otherTuple + except (UnicodeEncodeError, AttributeError): + # This can only happen for + # 1) an object that is not a NameRecord, or + # 2) an unlikely incomplete NameRecord object which has not been + # fully populated, or + # 3) when all IDs are identical but the strings can't be encoded + # for their platform encoding. + # In all cases it is best to return NotImplemented. + return NotImplemented def __repr__(self): return "<NameRecord NameID=%d; PlatformID=%d; LanguageID=%d>" % ( |