_n_a_m_e.py 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228
  1. # -*- coding: utf-8 -*-
  2. from fontTools.misc import sstruct
  3. from fontTools.misc.textTools import (
  4. bytechr,
  5. byteord,
  6. bytesjoin,
  7. strjoin,
  8. tobytes,
  9. tostr,
  10. safeEval,
  11. )
  12. from fontTools.misc.encodingTools import getEncoding
  13. from fontTools.ttLib import newTable
  14. from fontTools.ttLib.ttVisitor import TTVisitor
  15. from fontTools import ttLib
  16. import fontTools.ttLib.tables.otTables as otTables
  17. from fontTools.ttLib.tables import C_P_A_L_
  18. from . import DefaultTable
  19. import struct
  20. import logging
  21. log = logging.getLogger(__name__)
  22. nameRecordFormat = """
  23. > # big endian
  24. platformID: H
  25. platEncID: H
  26. langID: H
  27. nameID: H
  28. length: H
  29. offset: H
  30. """
  31. nameRecordSize = sstruct.calcsize(nameRecordFormat)
  32. class table__n_a_m_e(DefaultTable.DefaultTable):
  33. dependencies = ["ltag"]
  34. def decompile(self, data, ttFont):
  35. format, n, stringOffset = struct.unpack(b">HHH", data[:6])
  36. expectedStringOffset = 6 + n * nameRecordSize
  37. if stringOffset != expectedStringOffset:
  38. log.error(
  39. "'name' table stringOffset incorrect. Expected: %s; Actual: %s",
  40. expectedStringOffset,
  41. stringOffset,
  42. )
  43. stringData = data[stringOffset:]
  44. data = data[6:]
  45. self.names = []
  46. for i in range(n):
  47. if len(data) < 12:
  48. log.error("skipping malformed name record #%d", i)
  49. continue
  50. name, data = sstruct.unpack2(nameRecordFormat, data, NameRecord())
  51. name.string = stringData[name.offset : name.offset + name.length]
  52. if name.offset + name.length > len(stringData):
  53. log.error("skipping malformed name record #%d", i)
  54. continue
  55. assert len(name.string) == name.length
  56. # if (name.platEncID, name.platformID) in ((0, 0), (1, 3)):
  57. # if len(name.string) % 2:
  58. # print "2-byte string doesn't have even length!"
  59. # print name.__dict__
  60. del name.offset, name.length
  61. self.names.append(name)
  62. def compile(self, ttFont):
  63. if not hasattr(self, "names"):
  64. # only happens when there are NO name table entries read
  65. # from the TTX file
  66. self.names = []
  67. names = self.names
  68. names.sort() # sort according to the spec; see NameRecord.__lt__()
  69. stringData = b""
  70. format = 0
  71. n = len(names)
  72. stringOffset = 6 + n * sstruct.calcsize(nameRecordFormat)
  73. data = struct.pack(b">HHH", format, n, stringOffset)
  74. lastoffset = 0
  75. done = {} # remember the data so we can reuse the "pointers"
  76. for name in names:
  77. string = name.toBytes()
  78. if string in done:
  79. name.offset, name.length = done[string]
  80. else:
  81. name.offset, name.length = done[string] = len(stringData), len(string)
  82. stringData = bytesjoin([stringData, string])
  83. data = data + sstruct.pack(nameRecordFormat, name)
  84. return data + stringData
  85. def toXML(self, writer, ttFont):
  86. for name in self.names:
  87. name.toXML(writer, ttFont)
  88. def fromXML(self, name, attrs, content, ttFont):
  89. if name != "namerecord":
  90. return # ignore unknown tags
  91. if not hasattr(self, "names"):
  92. self.names = []
  93. name = NameRecord()
  94. self.names.append(name)
  95. name.fromXML(name, attrs, content, ttFont)
  96. def getName(self, nameID, platformID, platEncID, langID=None):
  97. for namerecord in self.names:
  98. if (
  99. namerecord.nameID == nameID
  100. and namerecord.platformID == platformID
  101. and namerecord.platEncID == platEncID
  102. ):
  103. if langID is None or namerecord.langID == langID:
  104. return namerecord
  105. return None # not found
  106. def getDebugName(self, nameID):
  107. englishName = someName = None
  108. for name in self.names:
  109. if name.nameID != nameID:
  110. continue
  111. try:
  112. unistr = name.toUnicode()
  113. except UnicodeDecodeError:
  114. continue
  115. someName = unistr
  116. if (name.platformID, name.langID) in ((1, 0), (3, 0x409)):
  117. englishName = unistr
  118. break
  119. if englishName:
  120. return englishName
  121. elif someName:
  122. return someName
  123. else:
  124. return None
  125. def getFirstDebugName(self, nameIDs):
  126. for nameID in nameIDs:
  127. name = self.getDebugName(nameID)
  128. if name is not None:
  129. return name
  130. return None
  131. def getBestFamilyName(self):
  132. # 21 = WWS Family Name
  133. # 16 = Typographic Family Name
  134. # 1 = Family Name
  135. return self.getFirstDebugName((21, 16, 1))
  136. def getBestSubFamilyName(self):
  137. # 22 = WWS SubFamily Name
  138. # 17 = Typographic SubFamily Name
  139. # 2 = SubFamily Name
  140. return self.getFirstDebugName((22, 17, 2))
  141. def getBestFullName(self):
  142. # 4 = Full Name
  143. # 6 = PostScript Name
  144. for nameIDs in ((21, 22), (16, 17), (1, 2), (4,), (6,)):
  145. if len(nameIDs) == 2:
  146. name_fam = self.getDebugName(nameIDs[0])
  147. name_subfam = self.getDebugName(nameIDs[1])
  148. if None in [name_fam, name_subfam]:
  149. continue # if any is None, skip
  150. name = f"{name_fam} {name_subfam}"
  151. if name_subfam.lower() == "regular":
  152. name = f"{name_fam}"
  153. return name
  154. else:
  155. name = self.getDebugName(nameIDs[0])
  156. if name is not None:
  157. return name
  158. return None
  159. def setName(self, string, nameID, platformID, platEncID, langID):
  160. """Set the 'string' for the name record identified by 'nameID', 'platformID',
  161. 'platEncID' and 'langID'. If a record with that nameID doesn't exist, create it
  162. and append to the name table.
  163. 'string' can be of type `str` (`unicode` in PY2) or `bytes`. In the latter case,
  164. it is assumed to be already encoded with the correct plaform-specific encoding
  165. identified by the (platformID, platEncID, langID) triplet. A warning is issued
  166. to prevent unexpected results.
  167. """
  168. if not hasattr(self, "names"):
  169. self.names = []
  170. if not isinstance(string, str):
  171. if isinstance(string, bytes):
  172. log.warning(
  173. "name string is bytes, ensure it's correctly encoded: %r", string
  174. )
  175. else:
  176. raise TypeError(
  177. "expected unicode or bytes, found %s: %r"
  178. % (type(string).__name__, string)
  179. )
  180. namerecord = self.getName(nameID, platformID, platEncID, langID)
  181. if namerecord:
  182. namerecord.string = string
  183. else:
  184. self.names.append(makeName(string, nameID, platformID, platEncID, langID))
  185. def removeNames(self, nameID=None, platformID=None, platEncID=None, langID=None):
  186. """Remove any name records identified by the given combination of 'nameID',
  187. 'platformID', 'platEncID' and 'langID'.
  188. """
  189. args = {
  190. argName: argValue
  191. for argName, argValue in (
  192. ("nameID", nameID),
  193. ("platformID", platformID),
  194. ("platEncID", platEncID),
  195. ("langID", langID),
  196. )
  197. if argValue is not None
  198. }
  199. if not args:
  200. # no arguments, nothing to do
  201. return
  202. self.names = [
  203. rec
  204. for rec in self.names
  205. if any(
  206. argValue != getattr(rec, argName) for argName, argValue in args.items()
  207. )
  208. ]
  209. @staticmethod
  210. def removeUnusedNames(ttFont):
  211. """Remove any name records which are not in NameID range 0-255 and not utilized
  212. within the font itself."""
  213. visitor = NameRecordVisitor()
  214. visitor.visit(ttFont)
  215. toDelete = set()
  216. for record in ttFont["name"].names:
  217. # Name IDs 26 to 255, inclusive, are reserved for future standard names.
  218. # https://learn.microsoft.com/en-us/typography/opentype/spec/name#name-ids
  219. if record.nameID < 256:
  220. continue
  221. if record.nameID not in visitor.seen:
  222. toDelete.add(record.nameID)
  223. for nameID in toDelete:
  224. ttFont["name"].removeNames(nameID)
  225. return toDelete
  226. def _findUnusedNameID(self, minNameID=256):
  227. """Finds an unused name id.
  228. The nameID is assigned in the range between 'minNameID' and 32767 (inclusive),
  229. following the last nameID in the name table.
  230. """
  231. names = getattr(self, "names", [])
  232. nameID = 1 + max([n.nameID for n in names] + [minNameID - 1])
  233. if nameID > 32767:
  234. raise ValueError("nameID must be less than 32768")
  235. return nameID
  236. def findMultilingualName(
  237. self, names, windows=True, mac=True, minNameID=0, ttFont=None
  238. ):
  239. """Return the name ID of an existing multilingual name that
  240. matches the 'names' dictionary, or None if not found.
  241. 'names' is a dictionary with the name in multiple languages,
  242. such as {'en': 'Pale', 'de': 'Blaß', 'de-CH': 'Blass'}.
  243. The keys can be arbitrary IETF BCP 47 language codes;
  244. the values are Unicode strings.
  245. If 'windows' is True, the returned name ID is guaranteed
  246. exist for all requested languages for platformID=3 and
  247. platEncID=1.
  248. If 'mac' is True, the returned name ID is guaranteed to exist
  249. for all requested languages for platformID=1 and platEncID=0.
  250. The returned name ID will not be less than the 'minNameID'
  251. argument.
  252. """
  253. # Gather the set of requested
  254. # (string, platformID, platEncID, langID)
  255. # tuples
  256. reqNameSet = set()
  257. for lang, name in sorted(names.items()):
  258. if windows:
  259. windowsName = _makeWindowsName(name, None, lang)
  260. if windowsName is not None:
  261. reqNameSet.add(
  262. (
  263. windowsName.string,
  264. windowsName.platformID,
  265. windowsName.platEncID,
  266. windowsName.langID,
  267. )
  268. )
  269. if mac:
  270. macName = _makeMacName(name, None, lang, ttFont)
  271. if macName is not None:
  272. reqNameSet.add(
  273. (
  274. macName.string,
  275. macName.platformID,
  276. macName.platEncID,
  277. macName.langID,
  278. )
  279. )
  280. # Collect matching name IDs
  281. matchingNames = dict()
  282. for name in self.names:
  283. try:
  284. key = (name.toUnicode(), name.platformID, name.platEncID, name.langID)
  285. except UnicodeDecodeError:
  286. continue
  287. if key in reqNameSet and name.nameID >= minNameID:
  288. nameSet = matchingNames.setdefault(name.nameID, set())
  289. nameSet.add(key)
  290. # Return the first name ID that defines all requested strings
  291. for nameID, nameSet in sorted(matchingNames.items()):
  292. if nameSet == reqNameSet:
  293. return nameID
  294. return None # not found
  295. def addMultilingualName(
  296. self, names, ttFont=None, nameID=None, windows=True, mac=True, minNameID=0
  297. ):
  298. """Add a multilingual name, returning its name ID
  299. 'names' is a dictionary with the name in multiple languages,
  300. such as {'en': 'Pale', 'de': 'Blaß', 'de-CH': 'Blass'}.
  301. The keys can be arbitrary IETF BCP 47 language codes;
  302. the values are Unicode strings.
  303. 'ttFont' is the TTFont to which the names are added, or None.
  304. If present, the font's 'ltag' table can get populated
  305. to store exotic language codes, which allows encoding
  306. names that otherwise cannot get encoded at all.
  307. 'nameID' is the name ID to be used, or None to let the library
  308. find an existing set of name records that match, or pick an
  309. unused name ID.
  310. If 'windows' is True, a platformID=3 name record will be added.
  311. If 'mac' is True, a platformID=1 name record will be added.
  312. If the 'nameID' argument is None, the created nameID will not
  313. be less than the 'minNameID' argument.
  314. """
  315. if not hasattr(self, "names"):
  316. self.names = []
  317. if nameID is None:
  318. # Reuse nameID if possible
  319. nameID = self.findMultilingualName(
  320. names, windows=windows, mac=mac, minNameID=minNameID, ttFont=ttFont
  321. )
  322. if nameID is not None:
  323. return nameID
  324. nameID = self._findUnusedNameID()
  325. # TODO: Should minimize BCP 47 language codes.
  326. # https://github.com/fonttools/fonttools/issues/930
  327. for lang, name in sorted(names.items()):
  328. if windows:
  329. windowsName = _makeWindowsName(name, nameID, lang)
  330. if windowsName is not None:
  331. self.names.append(windowsName)
  332. else:
  333. # We cannot not make a Windows name: make sure we add a
  334. # Mac name as a fallback. This can happen for exotic
  335. # BCP47 language tags that have no Windows language code.
  336. mac = True
  337. if mac:
  338. macName = _makeMacName(name, nameID, lang, ttFont)
  339. if macName is not None:
  340. self.names.append(macName)
  341. return nameID
  342. def addName(self, string, platforms=((1, 0, 0), (3, 1, 0x409)), minNameID=255):
  343. """Add a new name record containing 'string' for each (platformID, platEncID,
  344. langID) tuple specified in the 'platforms' list.
  345. The nameID is assigned in the range between 'minNameID'+1 and 32767 (inclusive),
  346. following the last nameID in the name table.
  347. If no 'platforms' are specified, two English name records are added, one for the
  348. Macintosh (platformID=0), and one for the Windows platform (3).
  349. The 'string' must be a Unicode string, so it can be encoded with different,
  350. platform-specific encodings.
  351. Return the new nameID.
  352. """
  353. assert (
  354. len(platforms) > 0
  355. ), "'platforms' must contain at least one (platformID, platEncID, langID) tuple"
  356. if not hasattr(self, "names"):
  357. self.names = []
  358. if not isinstance(string, str):
  359. raise TypeError(
  360. "expected str, found %s: %r" % (type(string).__name__, string)
  361. )
  362. nameID = self._findUnusedNameID(minNameID + 1)
  363. for platformID, platEncID, langID in platforms:
  364. self.names.append(makeName(string, nameID, platformID, platEncID, langID))
  365. return nameID
  366. def makeName(string, nameID, platformID, platEncID, langID):
  367. name = NameRecord()
  368. name.string, name.nameID, name.platformID, name.platEncID, name.langID = (
  369. string,
  370. nameID,
  371. platformID,
  372. platEncID,
  373. langID,
  374. )
  375. return name
  376. def _makeWindowsName(name, nameID, language):
  377. """Create a NameRecord for the Microsoft Windows platform
  378. 'language' is an arbitrary IETF BCP 47 language identifier such
  379. as 'en', 'de-CH', 'de-AT-1901', or 'fa-Latn'. If Microsoft Windows
  380. does not support the desired language, the result will be None.
  381. Future versions of fonttools might return a NameRecord for the
  382. OpenType 'name' table format 1, but this is not implemented yet.
  383. """
  384. langID = _WINDOWS_LANGUAGE_CODES.get(language.lower())
  385. if langID is not None:
  386. return makeName(name, nameID, 3, 1, langID)
  387. else:
  388. log.warning(
  389. "cannot add Windows name in language %s "
  390. "because fonttools does not yet support "
  391. "name table format 1" % language
  392. )
  393. return None
  394. def _makeMacName(name, nameID, language, font=None):
  395. """Create a NameRecord for Apple platforms
  396. 'language' is an arbitrary IETF BCP 47 language identifier such
  397. as 'en', 'de-CH', 'de-AT-1901', or 'fa-Latn'. When possible, we
  398. create a Macintosh NameRecord that is understood by old applications
  399. (platform ID 1 and an old-style Macintosh language enum). If this
  400. is not possible, we create a Unicode NameRecord (platform ID 0)
  401. whose language points to the font’s 'ltag' table. The latter
  402. can encode any string in any language, but legacy applications
  403. might not recognize the format (in which case they will ignore
  404. those names).
  405. 'font' should be the TTFont for which you want to create a name.
  406. If 'font' is None, we only return NameRecords for legacy Macintosh;
  407. in that case, the result will be None for names that need to
  408. be encoded with an 'ltag' table.
  409. See the section “The language identifier” in Apple’s specification:
  410. https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html
  411. """
  412. macLang = _MAC_LANGUAGE_CODES.get(language.lower())
  413. macScript = _MAC_LANGUAGE_TO_SCRIPT.get(macLang)
  414. if macLang is not None and macScript is not None:
  415. encoding = getEncoding(1, macScript, macLang, default="ascii")
  416. # Check if we can actually encode this name. If we can't,
  417. # for example because we have no support for the legacy
  418. # encoding, or because the name string contains Unicode
  419. # characters that the legacy encoding cannot represent,
  420. # we fall back to encoding the name in Unicode and put
  421. # the language tag into the ltag table.
  422. try:
  423. _ = tobytes(name, encoding, errors="strict")
  424. return makeName(name, nameID, 1, macScript, macLang)
  425. except UnicodeEncodeError:
  426. pass
  427. if font is not None:
  428. ltag = font.tables.get("ltag")
  429. if ltag is None:
  430. ltag = font["ltag"] = newTable("ltag")
  431. # 0 = Unicode; 4 = “Unicode 2.0 or later semantics (non-BMP characters allowed)”
  432. # “The preferred platform-specific code for Unicode would be 3 or 4.”
  433. # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html
  434. return makeName(name, nameID, 0, 4, ltag.addTag(language))
  435. else:
  436. log.warning(
  437. "cannot store language %s into 'ltag' table "
  438. "without having access to the TTFont object" % language
  439. )
  440. return None
  441. class NameRecord(object):
  442. def getEncoding(self, default="ascii"):
  443. """Returns the Python encoding name for this name entry based on its platformID,
  444. platEncID, and langID. If encoding for these values is not known, by default
  445. 'ascii' is returned. That can be overriden by passing a value to the default
  446. argument.
  447. """
  448. return getEncoding(self.platformID, self.platEncID, self.langID, default)
  449. def encodingIsUnicodeCompatible(self):
  450. return self.getEncoding(None) in ["utf_16_be", "ucs2be", "ascii", "latin1"]
  451. def __str__(self):
  452. return self.toStr(errors="backslashreplace")
  453. def isUnicode(self):
  454. return self.platformID == 0 or (
  455. self.platformID == 3 and self.platEncID in [0, 1, 10]
  456. )
  457. def toUnicode(self, errors="strict"):
  458. """
  459. If self.string is a Unicode string, return it; otherwise try decoding the
  460. bytes in self.string to a Unicode string using the encoding of this
  461. entry as returned by self.getEncoding(); Note that self.getEncoding()
  462. returns 'ascii' if the encoding is unknown to the library.
  463. Certain heuristics are performed to recover data from bytes that are
  464. ill-formed in the chosen encoding, or that otherwise look misencoded
  465. (mostly around bad UTF-16BE encoded bytes, or bytes that look like UTF-16BE
  466. but marked otherwise). If the bytes are ill-formed and the heuristics fail,
  467. the error is handled according to the errors parameter to this function, which is
  468. passed to the underlying decode() function; by default it throws a
  469. UnicodeDecodeError exception.
  470. Note: The mentioned heuristics mean that roundtripping a font to XML and back
  471. to binary might recover some misencoded data whereas just loading the font
  472. and saving it back will not change them.
  473. """
  474. def isascii(b):
  475. return (b >= 0x20 and b <= 0x7E) or b in [0x09, 0x0A, 0x0D]
  476. encoding = self.getEncoding()
  477. string = self.string
  478. if (
  479. isinstance(string, bytes)
  480. and encoding == "utf_16_be"
  481. and len(string) % 2 == 1
  482. ):
  483. # Recover badly encoded UTF-16 strings that have an odd number of bytes:
  484. # - If the last byte is zero, drop it. Otherwise,
  485. # - If all the odd bytes are zero and all the even bytes are ASCII,
  486. # prepend one zero byte. Otherwise,
  487. # - If first byte is zero and all other bytes are ASCII, insert zero
  488. # bytes between consecutive ASCII bytes.
  489. #
  490. # (Yes, I've seen all of these in the wild... sigh)
  491. if byteord(string[-1]) == 0:
  492. string = string[:-1]
  493. elif all(
  494. byteord(b) == 0 if i % 2 else isascii(byteord(b))
  495. for i, b in enumerate(string)
  496. ):
  497. string = b"\0" + string
  498. elif byteord(string[0]) == 0 and all(
  499. isascii(byteord(b)) for b in string[1:]
  500. ):
  501. string = bytesjoin(b"\0" + bytechr(byteord(b)) for b in string[1:])
  502. string = tostr(string, encoding=encoding, errors=errors)
  503. # If decoded strings still looks like UTF-16BE, it suggests a double-encoding.
  504. # Fix it up.
  505. if all(
  506. ord(c) == 0 if i % 2 == 0 else isascii(ord(c)) for i, c in enumerate(string)
  507. ):
  508. # If string claims to be Mac encoding, but looks like UTF-16BE with ASCII text,
  509. # narrow it down.
  510. string = "".join(c for c in string[1::2])
  511. return string
  512. def toBytes(self, errors="strict"):
  513. """If self.string is a bytes object, return it; otherwise try encoding
  514. the Unicode string in self.string to bytes using the encoding of this
  515. entry as returned by self.getEncoding(); Note that self.getEncoding()
  516. returns 'ascii' if the encoding is unknown to the library.
  517. If the Unicode string cannot be encoded to bytes in the chosen encoding,
  518. the error is handled according to the errors parameter to this function,
  519. which is passed to the underlying encode() function; by default it throws a
  520. UnicodeEncodeError exception.
  521. """
  522. return tobytes(self.string, encoding=self.getEncoding(), errors=errors)
  523. toStr = toUnicode
  524. def toXML(self, writer, ttFont):
  525. try:
  526. unistr = self.toUnicode()
  527. except UnicodeDecodeError:
  528. unistr = None
  529. attrs = [
  530. ("nameID", self.nameID),
  531. ("platformID", self.platformID),
  532. ("platEncID", self.platEncID),
  533. ("langID", hex(self.langID)),
  534. ]
  535. if unistr is None or not self.encodingIsUnicodeCompatible():
  536. attrs.append(("unicode", unistr is not None))
  537. writer.begintag("namerecord", attrs)
  538. writer.newline()
  539. if unistr is not None:
  540. writer.write(unistr)
  541. else:
  542. writer.write8bit(self.string)
  543. writer.newline()
  544. writer.endtag("namerecord")
  545. writer.newline()
  546. def fromXML(self, name, attrs, content, ttFont):
  547. self.nameID = safeEval(attrs["nameID"])
  548. self.platformID = safeEval(attrs["platformID"])
  549. self.platEncID = safeEval(attrs["platEncID"])
  550. self.langID = safeEval(attrs["langID"])
  551. s = strjoin(content).strip()
  552. encoding = self.getEncoding()
  553. if self.encodingIsUnicodeCompatible() or safeEval(
  554. attrs.get("unicode", "False")
  555. ):
  556. self.string = s.encode(encoding)
  557. else:
  558. # This is the inverse of write8bit...
  559. self.string = s.encode("latin1")
  560. def __lt__(self, other):
  561. if type(self) != type(other):
  562. return NotImplemented
  563. try:
  564. selfTuple = (
  565. self.platformID,
  566. self.platEncID,
  567. self.langID,
  568. self.nameID,
  569. )
  570. otherTuple = (
  571. other.platformID,
  572. other.platEncID,
  573. other.langID,
  574. other.nameID,
  575. )
  576. except AttributeError:
  577. # This can only happen for
  578. # 1) an object that is not a NameRecord, or
  579. # 2) an unlikely incomplete NameRecord object which has not been
  580. # fully populated
  581. return NotImplemented
  582. try:
  583. # Include the actual NameRecord string in the comparison tuples
  584. selfTuple = selfTuple + (self.toBytes(),)
  585. otherTuple = otherTuple + (other.toBytes(),)
  586. except UnicodeEncodeError as e:
  587. # toBytes caused an encoding error in either of the two, so content
  588. # to sorting based on IDs only
  589. log.error("NameRecord sorting failed to encode: %s" % e)
  590. # Implemented so that list.sort() sorts according to the spec by using
  591. # the order of the tuple items and their comparison
  592. return selfTuple < otherTuple
  593. def __repr__(self):
  594. return "<NameRecord NameID=%d; PlatformID=%d; LanguageID=%d>" % (
  595. self.nameID,
  596. self.platformID,
  597. self.langID,
  598. )
  599. # Windows language ID → IETF BCP-47 language tag
  600. #
  601. # While Microsoft indicates a region/country for all its language
  602. # IDs, we follow Unicode practice by omitting “most likely subtags”
  603. # as per Unicode CLDR. For example, English is simply “en” and not
  604. # “en-Latn” because according to Unicode, the default script
  605. # for English is Latin.
  606. #
  607. # http://www.unicode.org/cldr/charts/latest/supplemental/likely_subtags.html
  608. # http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
  609. _WINDOWS_LANGUAGES = {
  610. 0x0436: "af",
  611. 0x041C: "sq",
  612. 0x0484: "gsw",
  613. 0x045E: "am",
  614. 0x1401: "ar-DZ",
  615. 0x3C01: "ar-BH",
  616. 0x0C01: "ar",
  617. 0x0801: "ar-IQ",
  618. 0x2C01: "ar-JO",
  619. 0x3401: "ar-KW",
  620. 0x3001: "ar-LB",
  621. 0x1001: "ar-LY",
  622. 0x1801: "ary",
  623. 0x2001: "ar-OM",
  624. 0x4001: "ar-QA",
  625. 0x0401: "ar-SA",
  626. 0x2801: "ar-SY",
  627. 0x1C01: "aeb",
  628. 0x3801: "ar-AE",
  629. 0x2401: "ar-YE",
  630. 0x042B: "hy",
  631. 0x044D: "as",
  632. 0x082C: "az-Cyrl",
  633. 0x042C: "az",
  634. 0x046D: "ba",
  635. 0x042D: "eu",
  636. 0x0423: "be",
  637. 0x0845: "bn",
  638. 0x0445: "bn-IN",
  639. 0x201A: "bs-Cyrl",
  640. 0x141A: "bs",
  641. 0x047E: "br",
  642. 0x0402: "bg",
  643. 0x0403: "ca",
  644. 0x0C04: "zh-HK",
  645. 0x1404: "zh-MO",
  646. 0x0804: "zh",
  647. 0x1004: "zh-SG",
  648. 0x0404: "zh-TW",
  649. 0x0483: "co",
  650. 0x041A: "hr",
  651. 0x101A: "hr-BA",
  652. 0x0405: "cs",
  653. 0x0406: "da",
  654. 0x048C: "prs",
  655. 0x0465: "dv",
  656. 0x0813: "nl-BE",
  657. 0x0413: "nl",
  658. 0x0C09: "en-AU",
  659. 0x2809: "en-BZ",
  660. 0x1009: "en-CA",
  661. 0x2409: "en-029",
  662. 0x4009: "en-IN",
  663. 0x1809: "en-IE",
  664. 0x2009: "en-JM",
  665. 0x4409: "en-MY",
  666. 0x1409: "en-NZ",
  667. 0x3409: "en-PH",
  668. 0x4809: "en-SG",
  669. 0x1C09: "en-ZA",
  670. 0x2C09: "en-TT",
  671. 0x0809: "en-GB",
  672. 0x0409: "en",
  673. 0x3009: "en-ZW",
  674. 0x0425: "et",
  675. 0x0438: "fo",
  676. 0x0464: "fil",
  677. 0x040B: "fi",
  678. 0x080C: "fr-BE",
  679. 0x0C0C: "fr-CA",
  680. 0x040C: "fr",
  681. 0x140C: "fr-LU",
  682. 0x180C: "fr-MC",
  683. 0x100C: "fr-CH",
  684. 0x0462: "fy",
  685. 0x0456: "gl",
  686. 0x0437: "ka",
  687. 0x0C07: "de-AT",
  688. 0x0407: "de",
  689. 0x1407: "de-LI",
  690. 0x1007: "de-LU",
  691. 0x0807: "de-CH",
  692. 0x0408: "el",
  693. 0x046F: "kl",
  694. 0x0447: "gu",
  695. 0x0468: "ha",
  696. 0x040D: "he",
  697. 0x0439: "hi",
  698. 0x040E: "hu",
  699. 0x040F: "is",
  700. 0x0470: "ig",
  701. 0x0421: "id",
  702. 0x045D: "iu",
  703. 0x085D: "iu-Latn",
  704. 0x083C: "ga",
  705. 0x0434: "xh",
  706. 0x0435: "zu",
  707. 0x0410: "it",
  708. 0x0810: "it-CH",
  709. 0x0411: "ja",
  710. 0x044B: "kn",
  711. 0x043F: "kk",
  712. 0x0453: "km",
  713. 0x0486: "quc",
  714. 0x0487: "rw",
  715. 0x0441: "sw",
  716. 0x0457: "kok",
  717. 0x0412: "ko",
  718. 0x0440: "ky",
  719. 0x0454: "lo",
  720. 0x0426: "lv",
  721. 0x0427: "lt",
  722. 0x082E: "dsb",
  723. 0x046E: "lb",
  724. 0x042F: "mk",
  725. 0x083E: "ms-BN",
  726. 0x043E: "ms",
  727. 0x044C: "ml",
  728. 0x043A: "mt",
  729. 0x0481: "mi",
  730. 0x047A: "arn",
  731. 0x044E: "mr",
  732. 0x047C: "moh",
  733. 0x0450: "mn",
  734. 0x0850: "mn-CN",
  735. 0x0461: "ne",
  736. 0x0414: "nb",
  737. 0x0814: "nn",
  738. 0x0482: "oc",
  739. 0x0448: "or",
  740. 0x0463: "ps",
  741. 0x0415: "pl",
  742. 0x0416: "pt",
  743. 0x0816: "pt-PT",
  744. 0x0446: "pa",
  745. 0x046B: "qu-BO",
  746. 0x086B: "qu-EC",
  747. 0x0C6B: "qu",
  748. 0x0418: "ro",
  749. 0x0417: "rm",
  750. 0x0419: "ru",
  751. 0x243B: "smn",
  752. 0x103B: "smj-NO",
  753. 0x143B: "smj",
  754. 0x0C3B: "se-FI",
  755. 0x043B: "se",
  756. 0x083B: "se-SE",
  757. 0x203B: "sms",
  758. 0x183B: "sma-NO",
  759. 0x1C3B: "sms",
  760. 0x044F: "sa",
  761. 0x1C1A: "sr-Cyrl-BA",
  762. 0x0C1A: "sr",
  763. 0x181A: "sr-Latn-BA",
  764. 0x081A: "sr-Latn",
  765. 0x046C: "nso",
  766. 0x0432: "tn",
  767. 0x045B: "si",
  768. 0x041B: "sk",
  769. 0x0424: "sl",
  770. 0x2C0A: "es-AR",
  771. 0x400A: "es-BO",
  772. 0x340A: "es-CL",
  773. 0x240A: "es-CO",
  774. 0x140A: "es-CR",
  775. 0x1C0A: "es-DO",
  776. 0x300A: "es-EC",
  777. 0x440A: "es-SV",
  778. 0x100A: "es-GT",
  779. 0x480A: "es-HN",
  780. 0x080A: "es-MX",
  781. 0x4C0A: "es-NI",
  782. 0x180A: "es-PA",
  783. 0x3C0A: "es-PY",
  784. 0x280A: "es-PE",
  785. 0x500A: "es-PR",
  786. # Microsoft has defined two different language codes for
  787. # “Spanish with modern sorting” and “Spanish with traditional
  788. # sorting”. This makes sense for collation APIs, and it would be
  789. # possible to express this in BCP 47 language tags via Unicode
  790. # extensions (eg., “es-u-co-trad” is “Spanish with traditional
  791. # sorting”). However, for storing names in fonts, this distinction
  792. # does not make sense, so we use “es” in both cases.
  793. 0x0C0A: "es",
  794. 0x040A: "es",
  795. 0x540A: "es-US",
  796. 0x380A: "es-UY",
  797. 0x200A: "es-VE",
  798. 0x081D: "sv-FI",
  799. 0x041D: "sv",
  800. 0x045A: "syr",
  801. 0x0428: "tg",
  802. 0x085F: "tzm",
  803. 0x0449: "ta",
  804. 0x0444: "tt",
  805. 0x044A: "te",
  806. 0x041E: "th",
  807. 0x0451: "bo",
  808. 0x041F: "tr",
  809. 0x0442: "tk",
  810. 0x0480: "ug",
  811. 0x0422: "uk",
  812. 0x042E: "hsb",
  813. 0x0420: "ur",
  814. 0x0843: "uz-Cyrl",
  815. 0x0443: "uz",
  816. 0x042A: "vi",
  817. 0x0452: "cy",
  818. 0x0488: "wo",
  819. 0x0485: "sah",
  820. 0x0478: "ii",
  821. 0x046A: "yo",
  822. }
  823. _MAC_LANGUAGES = {
  824. 0: "en",
  825. 1: "fr",
  826. 2: "de",
  827. 3: "it",
  828. 4: "nl",
  829. 5: "sv",
  830. 6: "es",
  831. 7: "da",
  832. 8: "pt",
  833. 9: "no",
  834. 10: "he",
  835. 11: "ja",
  836. 12: "ar",
  837. 13: "fi",
  838. 14: "el",
  839. 15: "is",
  840. 16: "mt",
  841. 17: "tr",
  842. 18: "hr",
  843. 19: "zh-Hant",
  844. 20: "ur",
  845. 21: "hi",
  846. 22: "th",
  847. 23: "ko",
  848. 24: "lt",
  849. 25: "pl",
  850. 26: "hu",
  851. 27: "es",
  852. 28: "lv",
  853. 29: "se",
  854. 30: "fo",
  855. 31: "fa",
  856. 32: "ru",
  857. 33: "zh",
  858. 34: "nl-BE",
  859. 35: "ga",
  860. 36: "sq",
  861. 37: "ro",
  862. 38: "cz",
  863. 39: "sk",
  864. 40: "sl",
  865. 41: "yi",
  866. 42: "sr",
  867. 43: "mk",
  868. 44: "bg",
  869. 45: "uk",
  870. 46: "be",
  871. 47: "uz",
  872. 48: "kk",
  873. 49: "az-Cyrl",
  874. 50: "az-Arab",
  875. 51: "hy",
  876. 52: "ka",
  877. 53: "mo",
  878. 54: "ky",
  879. 55: "tg",
  880. 56: "tk",
  881. 57: "mn-CN",
  882. 58: "mn",
  883. 59: "ps",
  884. 60: "ks",
  885. 61: "ku",
  886. 62: "sd",
  887. 63: "bo",
  888. 64: "ne",
  889. 65: "sa",
  890. 66: "mr",
  891. 67: "bn",
  892. 68: "as",
  893. 69: "gu",
  894. 70: "pa",
  895. 71: "or",
  896. 72: "ml",
  897. 73: "kn",
  898. 74: "ta",
  899. 75: "te",
  900. 76: "si",
  901. 77: "my",
  902. 78: "km",
  903. 79: "lo",
  904. 80: "vi",
  905. 81: "id",
  906. 82: "tl",
  907. 83: "ms",
  908. 84: "ms-Arab",
  909. 85: "am",
  910. 86: "ti",
  911. 87: "om",
  912. 88: "so",
  913. 89: "sw",
  914. 90: "rw",
  915. 91: "rn",
  916. 92: "ny",
  917. 93: "mg",
  918. 94: "eo",
  919. 128: "cy",
  920. 129: "eu",
  921. 130: "ca",
  922. 131: "la",
  923. 132: "qu",
  924. 133: "gn",
  925. 134: "ay",
  926. 135: "tt",
  927. 136: "ug",
  928. 137: "dz",
  929. 138: "jv",
  930. 139: "su",
  931. 140: "gl",
  932. 141: "af",
  933. 142: "br",
  934. 143: "iu",
  935. 144: "gd",
  936. 145: "gv",
  937. 146: "ga",
  938. 147: "to",
  939. 148: "el-polyton",
  940. 149: "kl",
  941. 150: "az",
  942. 151: "nn",
  943. }
  944. _WINDOWS_LANGUAGE_CODES = {
  945. lang.lower(): code for code, lang in _WINDOWS_LANGUAGES.items()
  946. }
  947. _MAC_LANGUAGE_CODES = {lang.lower(): code for code, lang in _MAC_LANGUAGES.items()}
  948. # MacOS language ID → MacOS script ID
  949. #
  950. # Note that the script ID is not sufficient to determine what encoding
  951. # to use in TrueType files. For some languages, MacOS used a modification
  952. # of a mainstream script. For example, an Icelandic name would be stored
  953. # with smRoman in the TrueType naming table, but the actual encoding
  954. # is a special Icelandic version of the normal Macintosh Roman encoding.
  955. # As another example, Inuktitut uses an 8-bit encoding for Canadian Aboriginal
  956. # Syllables but MacOS had run out of available script codes, so this was
  957. # done as a (pretty radical) “modification” of Ethiopic.
  958. #
  959. # http://unicode.org/Public/MAPPINGS/VENDORS/APPLE/Readme.txt
  960. _MAC_LANGUAGE_TO_SCRIPT = {
  961. 0: 0, # langEnglish → smRoman
  962. 1: 0, # langFrench → smRoman
  963. 2: 0, # langGerman → smRoman
  964. 3: 0, # langItalian → smRoman
  965. 4: 0, # langDutch → smRoman
  966. 5: 0, # langSwedish → smRoman
  967. 6: 0, # langSpanish → smRoman
  968. 7: 0, # langDanish → smRoman
  969. 8: 0, # langPortuguese → smRoman
  970. 9: 0, # langNorwegian → smRoman
  971. 10: 5, # langHebrew → smHebrew
  972. 11: 1, # langJapanese → smJapanese
  973. 12: 4, # langArabic → smArabic
  974. 13: 0, # langFinnish → smRoman
  975. 14: 6, # langGreek → smGreek
  976. 15: 0, # langIcelandic → smRoman (modified)
  977. 16: 0, # langMaltese → smRoman
  978. 17: 0, # langTurkish → smRoman (modified)
  979. 18: 0, # langCroatian → smRoman (modified)
  980. 19: 2, # langTradChinese → smTradChinese
  981. 20: 4, # langUrdu → smArabic
  982. 21: 9, # langHindi → smDevanagari
  983. 22: 21, # langThai → smThai
  984. 23: 3, # langKorean → smKorean
  985. 24: 29, # langLithuanian → smCentralEuroRoman
  986. 25: 29, # langPolish → smCentralEuroRoman
  987. 26: 29, # langHungarian → smCentralEuroRoman
  988. 27: 29, # langEstonian → smCentralEuroRoman
  989. 28: 29, # langLatvian → smCentralEuroRoman
  990. 29: 0, # langSami → smRoman
  991. 30: 0, # langFaroese → smRoman (modified)
  992. 31: 4, # langFarsi → smArabic (modified)
  993. 32: 7, # langRussian → smCyrillic
  994. 33: 25, # langSimpChinese → smSimpChinese
  995. 34: 0, # langFlemish → smRoman
  996. 35: 0, # langIrishGaelic → smRoman (modified)
  997. 36: 0, # langAlbanian → smRoman
  998. 37: 0, # langRomanian → smRoman (modified)
  999. 38: 29, # langCzech → smCentralEuroRoman
  1000. 39: 29, # langSlovak → smCentralEuroRoman
  1001. 40: 0, # langSlovenian → smRoman (modified)
  1002. 41: 5, # langYiddish → smHebrew
  1003. 42: 7, # langSerbian → smCyrillic
  1004. 43: 7, # langMacedonian → smCyrillic
  1005. 44: 7, # langBulgarian → smCyrillic
  1006. 45: 7, # langUkrainian → smCyrillic (modified)
  1007. 46: 7, # langByelorussian → smCyrillic
  1008. 47: 7, # langUzbek → smCyrillic
  1009. 48: 7, # langKazakh → smCyrillic
  1010. 49: 7, # langAzerbaijani → smCyrillic
  1011. 50: 4, # langAzerbaijanAr → smArabic
  1012. 51: 24, # langArmenian → smArmenian
  1013. 52: 23, # langGeorgian → smGeorgian
  1014. 53: 7, # langMoldavian → smCyrillic
  1015. 54: 7, # langKirghiz → smCyrillic
  1016. 55: 7, # langTajiki → smCyrillic
  1017. 56: 7, # langTurkmen → smCyrillic
  1018. 57: 27, # langMongolian → smMongolian
  1019. 58: 7, # langMongolianCyr → smCyrillic
  1020. 59: 4, # langPashto → smArabic
  1021. 60: 4, # langKurdish → smArabic
  1022. 61: 4, # langKashmiri → smArabic
  1023. 62: 4, # langSindhi → smArabic
  1024. 63: 26, # langTibetan → smTibetan
  1025. 64: 9, # langNepali → smDevanagari
  1026. 65: 9, # langSanskrit → smDevanagari
  1027. 66: 9, # langMarathi → smDevanagari
  1028. 67: 13, # langBengali → smBengali
  1029. 68: 13, # langAssamese → smBengali
  1030. 69: 11, # langGujarati → smGujarati
  1031. 70: 10, # langPunjabi → smGurmukhi
  1032. 71: 12, # langOriya → smOriya
  1033. 72: 17, # langMalayalam → smMalayalam
  1034. 73: 16, # langKannada → smKannada
  1035. 74: 14, # langTamil → smTamil
  1036. 75: 15, # langTelugu → smTelugu
  1037. 76: 18, # langSinhalese → smSinhalese
  1038. 77: 19, # langBurmese → smBurmese
  1039. 78: 20, # langKhmer → smKhmer
  1040. 79: 22, # langLao → smLao
  1041. 80: 30, # langVietnamese → smVietnamese
  1042. 81: 0, # langIndonesian → smRoman
  1043. 82: 0, # langTagalog → smRoman
  1044. 83: 0, # langMalayRoman → smRoman
  1045. 84: 4, # langMalayArabic → smArabic
  1046. 85: 28, # langAmharic → smEthiopic
  1047. 86: 28, # langTigrinya → smEthiopic
  1048. 87: 28, # langOromo → smEthiopic
  1049. 88: 0, # langSomali → smRoman
  1050. 89: 0, # langSwahili → smRoman
  1051. 90: 0, # langKinyarwanda → smRoman
  1052. 91: 0, # langRundi → smRoman
  1053. 92: 0, # langNyanja → smRoman
  1054. 93: 0, # langMalagasy → smRoman
  1055. 94: 0, # langEsperanto → smRoman
  1056. 128: 0, # langWelsh → smRoman (modified)
  1057. 129: 0, # langBasque → smRoman
  1058. 130: 0, # langCatalan → smRoman
  1059. 131: 0, # langLatin → smRoman
  1060. 132: 0, # langQuechua → smRoman
  1061. 133: 0, # langGuarani → smRoman
  1062. 134: 0, # langAymara → smRoman
  1063. 135: 7, # langTatar → smCyrillic
  1064. 136: 4, # langUighur → smArabic
  1065. 137: 26, # langDzongkha → smTibetan
  1066. 138: 0, # langJavaneseRom → smRoman
  1067. 139: 0, # langSundaneseRom → smRoman
  1068. 140: 0, # langGalician → smRoman
  1069. 141: 0, # langAfrikaans → smRoman
  1070. 142: 0, # langBreton → smRoman (modified)
  1071. 143: 28, # langInuktitut → smEthiopic (modified)
  1072. 144: 0, # langScottishGaelic → smRoman (modified)
  1073. 145: 0, # langManxGaelic → smRoman (modified)
  1074. 146: 0, # langIrishGaelicScript → smRoman (modified)
  1075. 147: 0, # langTongan → smRoman
  1076. 148: 6, # langGreekAncient → smRoman
  1077. 149: 0, # langGreenlandic → smRoman
  1078. 150: 0, # langAzerbaijanRoman → smRoman
  1079. 151: 0, # langNynorsk → smRoman
  1080. }
  1081. class NameRecordVisitor(TTVisitor):
  1082. # Font tables that have NameIDs we need to collect.
  1083. TABLES = ("GSUB", "GPOS", "fvar", "CPAL", "STAT")
  1084. def __init__(self):
  1085. self.seen = set()
  1086. @NameRecordVisitor.register_attrs(
  1087. (
  1088. (otTables.FeatureParamsSize, ("SubfamilyID", "SubfamilyNameID")),
  1089. (otTables.FeatureParamsStylisticSet, ("UINameID",)),
  1090. (
  1091. otTables.FeatureParamsCharacterVariants,
  1092. (
  1093. "FeatUILabelNameID",
  1094. "FeatUITooltipTextNameID",
  1095. "SampleTextNameID",
  1096. "FirstParamUILabelNameID",
  1097. ),
  1098. ),
  1099. (otTables.STAT, ("ElidedFallbackNameID",)),
  1100. (otTables.AxisRecord, ("AxisNameID",)),
  1101. (otTables.AxisValue, ("ValueNameID",)),
  1102. (otTables.FeatureName, ("FeatureNameID",)),
  1103. (otTables.Setting, ("SettingNameID",)),
  1104. )
  1105. )
  1106. def visit(visitor, obj, attr, value):
  1107. visitor.seen.add(value)
  1108. @NameRecordVisitor.register(ttLib.getTableClass("fvar"))
  1109. def visit(visitor, obj):
  1110. for inst in obj.instances:
  1111. if inst.postscriptNameID != 0xFFFF:
  1112. visitor.seen.add(inst.postscriptNameID)
  1113. visitor.seen.add(inst.subfamilyNameID)
  1114. for axis in obj.axes:
  1115. visitor.seen.add(axis.axisNameID)
  1116. @NameRecordVisitor.register(ttLib.getTableClass("CPAL"))
  1117. def visit(visitor, obj):
  1118. if obj.version == 1:
  1119. visitor.seen.update(obj.paletteLabels)
  1120. visitor.seen.update(obj.paletteEntryLabels)
  1121. @NameRecordVisitor.register(ttLib.TTFont)
  1122. def visit(visitor, font, *args, **kwargs):
  1123. if hasattr(visitor, "font"):
  1124. return False
  1125. visitor.font = font
  1126. for tag in visitor.TABLES:
  1127. if tag in font:
  1128. visitor.visit(font[tag], *args, **kwargs)
  1129. del visitor.font
  1130. return False