reorderGlyphs.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. """Reorder glyphs in a font."""
  2. __author__ = "Rod Sheeter"
  3. # See https://docs.google.com/document/d/1h9O-C_ndods87uY0QeIIcgAMiX2gDTpvO_IhMJsKAqs/
  4. # for details.
  5. from fontTools import ttLib
  6. from fontTools.ttLib.tables import otBase
  7. from fontTools.ttLib.tables import otTables as ot
  8. from abc import ABC, abstractmethod
  9. from dataclasses import dataclass
  10. from collections import deque
  11. from typing import (
  12. Optional,
  13. Any,
  14. Callable,
  15. Deque,
  16. Iterable,
  17. List,
  18. Tuple,
  19. )
  20. _COVERAGE_ATTR = "Coverage" # tables that have one coverage use this name
  21. def _sort_by_gid(
  22. get_glyph_id: Callable[[str], int],
  23. glyphs: List[str],
  24. parallel_list: Optional[List[Any]],
  25. ):
  26. if parallel_list:
  27. reordered = sorted(
  28. ((g, e) for g, e in zip(glyphs, parallel_list)),
  29. key=lambda t: get_glyph_id(t[0]),
  30. )
  31. sorted_glyphs, sorted_parallel_list = map(list, zip(*reordered))
  32. parallel_list[:] = sorted_parallel_list
  33. else:
  34. sorted_glyphs = sorted(glyphs, key=get_glyph_id)
  35. glyphs[:] = sorted_glyphs
  36. def _get_dotted_attr(value: Any, dotted_attr: str) -> Any:
  37. attr_names = dotted_attr.split(".")
  38. assert attr_names
  39. while attr_names:
  40. attr_name = attr_names.pop(0)
  41. value = getattr(value, attr_name)
  42. return value
  43. class ReorderRule(ABC):
  44. """A rule to reorder something in a font to match the fonts glyph order."""
  45. @abstractmethod
  46. def apply(self, font: ttLib.TTFont, value: otBase.BaseTable) -> None: ...
  47. @dataclass(frozen=True)
  48. class ReorderCoverage(ReorderRule):
  49. """Reorder a Coverage table, and optionally a list that is sorted parallel to it."""
  50. # A list that is parallel to Coverage
  51. parallel_list_attr: Optional[str] = None
  52. coverage_attr: str = _COVERAGE_ATTR
  53. def apply(self, font: ttLib.TTFont, value: otBase.BaseTable) -> None:
  54. coverage = _get_dotted_attr(value, self.coverage_attr)
  55. if type(coverage) is not list:
  56. # Normal path, process one coverage that might have a parallel list
  57. parallel_list = None
  58. if self.parallel_list_attr:
  59. parallel_list = _get_dotted_attr(value, self.parallel_list_attr)
  60. assert (
  61. type(parallel_list) is list
  62. ), f"{self.parallel_list_attr} should be a list"
  63. assert len(parallel_list) == len(coverage.glyphs), "Nothing makes sense"
  64. _sort_by_gid(font.getGlyphID, coverage.glyphs, parallel_list)
  65. else:
  66. # A few tables have a list of coverage. No parallel list can exist.
  67. assert (
  68. not self.parallel_list_attr
  69. ), f"Can't have multiple coverage AND a parallel list; {self}"
  70. for coverage_entry in coverage:
  71. _sort_by_gid(font.getGlyphID, coverage_entry.glyphs, None)
  72. @dataclass(frozen=True)
  73. class ReorderList(ReorderRule):
  74. """Reorder the items within a list to match the updated glyph order.
  75. Useful when a list ordered by coverage itself contains something ordered by a gid.
  76. For example, the PairSet table of https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-2-pair-adjustment-positioning-subtable.
  77. """
  78. list_attr: str
  79. key: str
  80. def apply(self, font: ttLib.TTFont, value: otBase.BaseTable) -> None:
  81. lst = _get_dotted_attr(value, self.list_attr)
  82. assert isinstance(lst, list), f"{self.list_attr} should be a list"
  83. lst.sort(key=lambda v: font.getGlyphID(getattr(v, self.key)))
  84. # (Type, Optional Format) => List[ReorderRule]
  85. # Encodes the relationships Cosimo identified
  86. _REORDER_RULES = {
  87. # GPOS
  88. (ot.SinglePos, 1): [ReorderCoverage()],
  89. (ot.SinglePos, 2): [ReorderCoverage(parallel_list_attr="Value")],
  90. (ot.PairPos, 1): [ReorderCoverage(parallel_list_attr="PairSet")],
  91. (ot.PairSet, None): [ReorderList("PairValueRecord", key="SecondGlyph")],
  92. (ot.PairPos, 2): [ReorderCoverage()],
  93. (ot.CursivePos, 1): [ReorderCoverage(parallel_list_attr="EntryExitRecord")],
  94. (ot.MarkBasePos, 1): [
  95. ReorderCoverage(
  96. coverage_attr="MarkCoverage", parallel_list_attr="MarkArray.MarkRecord"
  97. ),
  98. ReorderCoverage(
  99. coverage_attr="BaseCoverage", parallel_list_attr="BaseArray.BaseRecord"
  100. ),
  101. ],
  102. (ot.MarkLigPos, 1): [
  103. ReorderCoverage(
  104. coverage_attr="MarkCoverage", parallel_list_attr="MarkArray.MarkRecord"
  105. ),
  106. ReorderCoverage(
  107. coverage_attr="LigatureCoverage",
  108. parallel_list_attr="LigatureArray.LigatureAttach",
  109. ),
  110. ],
  111. (ot.MarkMarkPos, 1): [
  112. ReorderCoverage(
  113. coverage_attr="Mark1Coverage", parallel_list_attr="Mark1Array.MarkRecord"
  114. ),
  115. ReorderCoverage(
  116. coverage_attr="Mark2Coverage", parallel_list_attr="Mark2Array.Mark2Record"
  117. ),
  118. ],
  119. (ot.ContextPos, 1): [ReorderCoverage(parallel_list_attr="PosRuleSet")],
  120. (ot.ContextPos, 2): [ReorderCoverage()],
  121. (ot.ContextPos, 3): [ReorderCoverage()],
  122. (ot.ChainContextPos, 1): [ReorderCoverage(parallel_list_attr="ChainPosRuleSet")],
  123. (ot.ChainContextPos, 2): [ReorderCoverage()],
  124. (ot.ChainContextPos, 3): [
  125. ReorderCoverage(coverage_attr="BacktrackCoverage"),
  126. ReorderCoverage(coverage_attr="InputCoverage"),
  127. ReorderCoverage(coverage_attr="LookAheadCoverage"),
  128. ],
  129. # GSUB
  130. (ot.ContextSubst, 1): [ReorderCoverage(parallel_list_attr="SubRuleSet")],
  131. (ot.ContextSubst, 2): [ReorderCoverage()],
  132. (ot.ContextSubst, 3): [ReorderCoverage()],
  133. (ot.ChainContextSubst, 1): [ReorderCoverage(parallel_list_attr="ChainSubRuleSet")],
  134. (ot.ChainContextSubst, 2): [ReorderCoverage()],
  135. (ot.ChainContextSubst, 3): [
  136. ReorderCoverage(coverage_attr="BacktrackCoverage"),
  137. ReorderCoverage(coverage_attr="InputCoverage"),
  138. ReorderCoverage(coverage_attr="LookAheadCoverage"),
  139. ],
  140. (ot.ReverseChainSingleSubst, 1): [
  141. ReorderCoverage(parallel_list_attr="Substitute"),
  142. ReorderCoverage(coverage_attr="BacktrackCoverage"),
  143. ReorderCoverage(coverage_attr="LookAheadCoverage"),
  144. ],
  145. # GDEF
  146. (ot.AttachList, None): [ReorderCoverage(parallel_list_attr="AttachPoint")],
  147. (ot.LigCaretList, None): [ReorderCoverage(parallel_list_attr="LigGlyph")],
  148. (ot.MarkGlyphSetsDef, None): [ReorderCoverage()],
  149. # MATH
  150. (ot.MathGlyphInfo, None): [ReorderCoverage(coverage_attr="ExtendedShapeCoverage")],
  151. (ot.MathItalicsCorrectionInfo, None): [
  152. ReorderCoverage(parallel_list_attr="ItalicsCorrection")
  153. ],
  154. (ot.MathTopAccentAttachment, None): [
  155. ReorderCoverage(
  156. coverage_attr="TopAccentCoverage", parallel_list_attr="TopAccentAttachment"
  157. )
  158. ],
  159. (ot.MathKernInfo, None): [
  160. ReorderCoverage(
  161. coverage_attr="MathKernCoverage", parallel_list_attr="MathKernInfoRecords"
  162. )
  163. ],
  164. (ot.MathVariants, None): [
  165. ReorderCoverage(
  166. coverage_attr="VertGlyphCoverage",
  167. parallel_list_attr="VertGlyphConstruction",
  168. ),
  169. ReorderCoverage(
  170. coverage_attr="HorizGlyphCoverage",
  171. parallel_list_attr="HorizGlyphConstruction",
  172. ),
  173. ],
  174. }
  175. # TODO Port to otTraverse
  176. SubTablePath = Tuple[otBase.BaseTable.SubTableEntry, ...]
  177. def _bfs_base_table(
  178. root: otBase.BaseTable, root_accessor: str
  179. ) -> Iterable[SubTablePath]:
  180. yield from _traverse_ot_data(
  181. root, root_accessor, lambda frontier, new: frontier.extend(new)
  182. )
  183. # Given f(current frontier, new entries) add new entries to frontier
  184. AddToFrontierFn = Callable[[Deque[SubTablePath], List[SubTablePath]], None]
  185. def _traverse_ot_data(
  186. root: otBase.BaseTable, root_accessor: str, add_to_frontier_fn: AddToFrontierFn
  187. ) -> Iterable[SubTablePath]:
  188. # no visited because general otData is forward-offset only and thus cannot cycle
  189. frontier: Deque[SubTablePath] = deque()
  190. frontier.append((otBase.BaseTable.SubTableEntry(root_accessor, root),))
  191. while frontier:
  192. # path is (value, attr_name) tuples. attr_name is attr of parent to get value
  193. path = frontier.popleft()
  194. current = path[-1].value
  195. yield path
  196. new_entries = []
  197. for subtable_entry in current.iterSubTables():
  198. new_entries.append(path + (subtable_entry,))
  199. add_to_frontier_fn(frontier, new_entries)
  200. def reorderGlyphs(font: ttLib.TTFont, new_glyph_order: List[str]):
  201. old_glyph_order = font.getGlyphOrder()
  202. if len(new_glyph_order) != len(old_glyph_order):
  203. raise ValueError(
  204. f"New glyph order contains {len(new_glyph_order)} glyphs, "
  205. f"but font has {len(old_glyph_order)} glyphs"
  206. )
  207. if set(old_glyph_order) != set(new_glyph_order):
  208. raise ValueError(
  209. "New glyph order does not contain the same set of glyphs as the font:\n"
  210. f"* only in new: {set(new_glyph_order) - set(old_glyph_order)}\n"
  211. f"* only in old: {set(old_glyph_order) - set(new_glyph_order)}"
  212. )
  213. # Changing the order of glyphs in a TTFont requires that all tables that use
  214. # glyph indexes have been fully.
  215. # Cf. https://github.com/fonttools/fonttools/issues/2060
  216. font.ensureDecompiled()
  217. not_loaded = sorted(t for t in font.keys() if not font.isLoaded(t))
  218. if not_loaded:
  219. raise ValueError(f"Everything should be loaded, following aren't: {not_loaded}")
  220. font.setGlyphOrder(new_glyph_order)
  221. coverage_containers = {"GDEF", "GPOS", "GSUB", "MATH"}
  222. for tag in coverage_containers:
  223. if tag in font.keys():
  224. for path in _bfs_base_table(font[tag].table, f'font["{tag}"]'):
  225. value = path[-1].value
  226. reorder_key = (type(value), getattr(value, "Format", None))
  227. for reorder in _REORDER_RULES.get(reorder_key, []):
  228. reorder.apply(font, value)
  229. if "CFF " in font:
  230. cff_table = font["CFF "]
  231. charstrings = cff_table.cff.topDictIndex[0].CharStrings.charStrings
  232. cff_table.cff.topDictIndex[0].charset = new_glyph_order
  233. cff_table.cff.topDictIndex[0].CharStrings.charStrings = {
  234. k: charstrings.get(k) for k in new_glyph_order
  235. }