reorderGlyphs.py 9.8 KB

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