cmap.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. # Copyright 2013 Google, Inc. All Rights Reserved.
  2. #
  3. # Google Author(s): Behdad Esfahbod, Roozbeh Pournader
  4. from fontTools.merge.unicode import is_Default_Ignorable
  5. from fontTools.pens.recordingPen import DecomposingRecordingPen
  6. import logging
  7. log = logging.getLogger("fontTools.merge")
  8. def computeMegaGlyphOrder(merger, glyphOrders):
  9. """Modifies passed-in glyphOrders to reflect new glyph names.
  10. Stores merger.glyphOrder."""
  11. megaOrder = {}
  12. for glyphOrder in glyphOrders:
  13. for i, glyphName in enumerate(glyphOrder):
  14. if glyphName in megaOrder:
  15. n = megaOrder[glyphName]
  16. while (glyphName + "." + repr(n)) in megaOrder:
  17. n += 1
  18. megaOrder[glyphName] = n
  19. glyphName += "." + repr(n)
  20. glyphOrder[i] = glyphName
  21. megaOrder[glyphName] = 1
  22. merger.glyphOrder = megaOrder = list(megaOrder.keys())
  23. def _glyphsAreSame(
  24. glyphSet1,
  25. glyphSet2,
  26. glyph1,
  27. glyph2,
  28. advanceTolerance=0.05,
  29. advanceToleranceEmpty=0.20,
  30. ):
  31. pen1 = DecomposingRecordingPen(glyphSet1)
  32. pen2 = DecomposingRecordingPen(glyphSet2)
  33. g1 = glyphSet1[glyph1]
  34. g2 = glyphSet2[glyph2]
  35. g1.draw(pen1)
  36. g2.draw(pen2)
  37. if pen1.value != pen2.value:
  38. return False
  39. # Allow more width tolerance for glyphs with no ink
  40. tolerance = advanceTolerance if pen1.value else advanceToleranceEmpty
  41. # TODO Warn if advances not the same but within tolerance.
  42. if abs(g1.width - g2.width) > g1.width * tolerance:
  43. return False
  44. if hasattr(g1, "height") and g1.height is not None:
  45. if abs(g1.height - g2.height) > g1.height * tolerance:
  46. return False
  47. return True
  48. def computeMegaUvs(merger, uvsTables):
  49. """Returns merged UVS subtable (cmap format=14)."""
  50. uvsDict = {}
  51. cmap = merger.cmap
  52. for table in uvsTables:
  53. for variationSelector, uvsMapping in table.uvsDict.items():
  54. if variationSelector not in uvsDict:
  55. uvsDict[variationSelector] = {}
  56. for unicodeValue, glyphName in uvsMapping:
  57. if cmap.get(unicodeValue) == glyphName:
  58. # this is a default variation
  59. glyphName = None
  60. # prefer previous glyph id if both fonts defined UVS
  61. if unicodeValue not in uvsDict[variationSelector]:
  62. uvsDict[variationSelector][unicodeValue] = glyphName
  63. for variationSelector in uvsDict:
  64. uvsDict[variationSelector] = [*uvsDict[variationSelector].items()]
  65. return uvsDict
  66. # Valid (format, platformID, platEncID) triplets for cmap subtables containing
  67. # Unicode BMP-only and Unicode Full Repertoire semantics.
  68. # Cf. OpenType spec for "Platform specific encodings":
  69. # https://docs.microsoft.com/en-us/typography/opentype/spec/name
  70. class _CmapUnicodePlatEncodings:
  71. BMP = {(4, 3, 1), (4, 0, 3), (4, 0, 4), (4, 0, 6)}
  72. FullRepertoire = {(12, 3, 10), (12, 0, 4), (12, 0, 6)}
  73. UVS = {(14, 0, 5)}
  74. def computeMegaCmap(merger, cmapTables):
  75. """Sets merger.cmap and merger.uvsDict."""
  76. # TODO Handle format=14.
  77. # Only merge format 4 and 12 Unicode subtables, ignores all other subtables
  78. # If there is a format 12 table for a font, ignore the format 4 table of it
  79. chosenCmapTables = []
  80. chosenUvsTables = []
  81. for fontIdx, table in enumerate(cmapTables):
  82. format4 = None
  83. format12 = None
  84. format14 = None
  85. for subtable in table.tables:
  86. properties = (subtable.format, subtable.platformID, subtable.platEncID)
  87. if properties in _CmapUnicodePlatEncodings.BMP:
  88. format4 = subtable
  89. elif properties in _CmapUnicodePlatEncodings.FullRepertoire:
  90. format12 = subtable
  91. elif properties in _CmapUnicodePlatEncodings.UVS:
  92. format14 = subtable
  93. else:
  94. log.warning(
  95. "Dropped cmap subtable from font '%s':\t"
  96. "format %2s, platformID %2s, platEncID %2s",
  97. fontIdx,
  98. subtable.format,
  99. subtable.platformID,
  100. subtable.platEncID,
  101. )
  102. if format12 is not None:
  103. chosenCmapTables.append((format12, fontIdx))
  104. elif format4 is not None:
  105. chosenCmapTables.append((format4, fontIdx))
  106. if format14 is not None:
  107. chosenUvsTables.append(format14)
  108. # Build the unicode mapping
  109. merger.cmap = cmap = {}
  110. fontIndexForGlyph = {}
  111. glyphSets = [None for f in merger.fonts] if hasattr(merger, "fonts") else None
  112. for table, fontIdx in chosenCmapTables:
  113. # handle duplicates
  114. for uni, gid in table.cmap.items():
  115. oldgid = cmap.get(uni, None)
  116. if oldgid is None:
  117. cmap[uni] = gid
  118. fontIndexForGlyph[gid] = fontIdx
  119. elif is_Default_Ignorable(uni) or uni in (0x25CC,): # U+25CC DOTTED CIRCLE
  120. continue
  121. elif oldgid != gid:
  122. # Char previously mapped to oldgid, now to gid.
  123. # Record, to fix up in GSUB 'locl' later.
  124. if merger.duplicateGlyphsPerFont[fontIdx].get(oldgid) is None:
  125. if glyphSets is not None:
  126. oldFontIdx = fontIndexForGlyph[oldgid]
  127. for idx in (fontIdx, oldFontIdx):
  128. if glyphSets[idx] is None:
  129. glyphSets[idx] = merger.fonts[idx].getGlyphSet()
  130. # if _glyphsAreSame(glyphSets[oldFontIdx], glyphSets[fontIdx], oldgid, gid):
  131. # continue
  132. merger.duplicateGlyphsPerFont[fontIdx][oldgid] = gid
  133. elif merger.duplicateGlyphsPerFont[fontIdx][oldgid] != gid:
  134. # Char previously mapped to oldgid but oldgid is already remapped to a different
  135. # gid, because of another Unicode character.
  136. # TODO: Try harder to do something about these.
  137. log.warning(
  138. "Dropped mapping from codepoint %#06X to glyphId '%s'", uni, gid
  139. )
  140. merger.uvsDict = computeMegaUvs(merger, chosenUvsTables)
  141. def renameCFFCharStrings(merger, glyphOrder, cffTable):
  142. """Rename topDictIndex charStrings based on glyphOrder."""
  143. td = cffTable.cff.topDictIndex[0]
  144. charStrings = {}
  145. for i, v in enumerate(td.CharStrings.charStrings.values()):
  146. glyphName = glyphOrder[i]
  147. charStrings[glyphName] = v
  148. td.CharStrings.charStrings = charStrings
  149. td.charset = list(glyphOrder)