123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- """Reorder glyphs in a font."""
- __author__ = "Rod Sheeter"
- # See https://docs.google.com/document/d/1h9O-C_ndods87uY0QeIIcgAMiX2gDTpvO_IhMJsKAqs/
- # for details.
- from fontTools import ttLib
- from fontTools.ttLib.tables import otBase
- from fontTools.ttLib.tables import otTables as ot
- from abc import ABC, abstractmethod
- from dataclasses import dataclass
- from collections import deque
- from typing import (
- Optional,
- Any,
- Callable,
- Deque,
- Iterable,
- List,
- Tuple,
- )
- _COVERAGE_ATTR = "Coverage" # tables that have one coverage use this name
- def _sort_by_gid(
- get_glyph_id: Callable[[str], int],
- glyphs: List[str],
- parallel_list: Optional[List[Any]],
- ):
- if parallel_list:
- reordered = sorted(
- ((g, e) for g, e in zip(glyphs, parallel_list)),
- key=lambda t: get_glyph_id(t[0]),
- )
- sorted_glyphs, sorted_parallel_list = map(list, zip(*reordered))
- parallel_list[:] = sorted_parallel_list
- else:
- sorted_glyphs = sorted(glyphs, key=get_glyph_id)
- glyphs[:] = sorted_glyphs
- def _get_dotted_attr(value: Any, dotted_attr: str) -> Any:
- attr_names = dotted_attr.split(".")
- assert attr_names
- while attr_names:
- attr_name = attr_names.pop(0)
- value = getattr(value, attr_name)
- return value
- class ReorderRule(ABC):
- """A rule to reorder something in a font to match the fonts glyph order."""
- @abstractmethod
- def apply(self, font: ttLib.TTFont, value: otBase.BaseTable) -> None: ...
- @dataclass(frozen=True)
- class ReorderCoverage(ReorderRule):
- """Reorder a Coverage table, and optionally a list that is sorted parallel to it."""
- # A list that is parallel to Coverage
- parallel_list_attr: Optional[str] = None
- coverage_attr: str = _COVERAGE_ATTR
- def apply(self, font: ttLib.TTFont, value: otBase.BaseTable) -> None:
- coverage = _get_dotted_attr(value, self.coverage_attr)
- if type(coverage) is not list:
- # Normal path, process one coverage that might have a parallel list
- parallel_list = None
- if self.parallel_list_attr:
- parallel_list = _get_dotted_attr(value, self.parallel_list_attr)
- assert (
- type(parallel_list) is list
- ), f"{self.parallel_list_attr} should be a list"
- assert len(parallel_list) == len(coverage.glyphs), "Nothing makes sense"
- _sort_by_gid(font.getGlyphID, coverage.glyphs, parallel_list)
- else:
- # A few tables have a list of coverage. No parallel list can exist.
- assert (
- not self.parallel_list_attr
- ), f"Can't have multiple coverage AND a parallel list; {self}"
- for coverage_entry in coverage:
- _sort_by_gid(font.getGlyphID, coverage_entry.glyphs, None)
- @dataclass(frozen=True)
- class ReorderList(ReorderRule):
- """Reorder the items within a list to match the updated glyph order.
- Useful when a list ordered by coverage itself contains something ordered by a gid.
- For example, the PairSet table of https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-2-pair-adjustment-positioning-subtable.
- """
- list_attr: str
- key: str
- def apply(self, font: ttLib.TTFont, value: otBase.BaseTable) -> None:
- lst = _get_dotted_attr(value, self.list_attr)
- assert isinstance(lst, list), f"{self.list_attr} should be a list"
- lst.sort(key=lambda v: font.getGlyphID(getattr(v, self.key)))
- # (Type, Optional Format) => List[ReorderRule]
- # Encodes the relationships Cosimo identified
- _REORDER_RULES = {
- # GPOS
- (ot.SinglePos, 1): [ReorderCoverage()],
- (ot.SinglePos, 2): [ReorderCoverage(parallel_list_attr="Value")],
- (ot.PairPos, 1): [ReorderCoverage(parallel_list_attr="PairSet")],
- (ot.PairSet, None): [ReorderList("PairValueRecord", key="SecondGlyph")],
- (ot.PairPos, 2): [ReorderCoverage()],
- (ot.CursivePos, 1): [ReorderCoverage(parallel_list_attr="EntryExitRecord")],
- (ot.MarkBasePos, 1): [
- ReorderCoverage(
- coverage_attr="MarkCoverage", parallel_list_attr="MarkArray.MarkRecord"
- ),
- ReorderCoverage(
- coverage_attr="BaseCoverage", parallel_list_attr="BaseArray.BaseRecord"
- ),
- ],
- (ot.MarkLigPos, 1): [
- ReorderCoverage(
- coverage_attr="MarkCoverage", parallel_list_attr="MarkArray.MarkRecord"
- ),
- ReorderCoverage(
- coverage_attr="LigatureCoverage",
- parallel_list_attr="LigatureArray.LigatureAttach",
- ),
- ],
- (ot.MarkMarkPos, 1): [
- ReorderCoverage(
- coverage_attr="Mark1Coverage", parallel_list_attr="Mark1Array.MarkRecord"
- ),
- ReorderCoverage(
- coverage_attr="Mark2Coverage", parallel_list_attr="Mark2Array.Mark2Record"
- ),
- ],
- (ot.ContextPos, 1): [ReorderCoverage(parallel_list_attr="PosRuleSet")],
- (ot.ContextPos, 2): [ReorderCoverage()],
- (ot.ContextPos, 3): [ReorderCoverage()],
- (ot.ChainContextPos, 1): [ReorderCoverage(parallel_list_attr="ChainPosRuleSet")],
- (ot.ChainContextPos, 2): [ReorderCoverage()],
- (ot.ChainContextPos, 3): [
- ReorderCoverage(coverage_attr="BacktrackCoverage"),
- ReorderCoverage(coverage_attr="InputCoverage"),
- ReorderCoverage(coverage_attr="LookAheadCoverage"),
- ],
- # GSUB
- (ot.ContextSubst, 1): [ReorderCoverage(parallel_list_attr="SubRuleSet")],
- (ot.ContextSubst, 2): [ReorderCoverage()],
- (ot.ContextSubst, 3): [ReorderCoverage()],
- (ot.ChainContextSubst, 1): [ReorderCoverage(parallel_list_attr="ChainSubRuleSet")],
- (ot.ChainContextSubst, 2): [ReorderCoverage()],
- (ot.ChainContextSubst, 3): [
- ReorderCoverage(coverage_attr="BacktrackCoverage"),
- ReorderCoverage(coverage_attr="InputCoverage"),
- ReorderCoverage(coverage_attr="LookAheadCoverage"),
- ],
- (ot.ReverseChainSingleSubst, 1): [
- ReorderCoverage(parallel_list_attr="Substitute"),
- ReorderCoverage(coverage_attr="BacktrackCoverage"),
- ReorderCoverage(coverage_attr="LookAheadCoverage"),
- ],
- # GDEF
- (ot.AttachList, None): [ReorderCoverage(parallel_list_attr="AttachPoint")],
- (ot.LigCaretList, None): [ReorderCoverage(parallel_list_attr="LigGlyph")],
- (ot.MarkGlyphSetsDef, None): [ReorderCoverage()],
- # MATH
- (ot.MathGlyphInfo, None): [ReorderCoverage(coverage_attr="ExtendedShapeCoverage")],
- (ot.MathItalicsCorrectionInfo, None): [
- ReorderCoverage(parallel_list_attr="ItalicsCorrection")
- ],
- (ot.MathTopAccentAttachment, None): [
- ReorderCoverage(
- coverage_attr="TopAccentCoverage", parallel_list_attr="TopAccentAttachment"
- )
- ],
- (ot.MathKernInfo, None): [
- ReorderCoverage(
- coverage_attr="MathKernCoverage", parallel_list_attr="MathKernInfoRecords"
- )
- ],
- (ot.MathVariants, None): [
- ReorderCoverage(
- coverage_attr="VertGlyphCoverage",
- parallel_list_attr="VertGlyphConstruction",
- ),
- ReorderCoverage(
- coverage_attr="HorizGlyphCoverage",
- parallel_list_attr="HorizGlyphConstruction",
- ),
- ],
- }
- # TODO Port to otTraverse
- SubTablePath = Tuple[otBase.BaseTable.SubTableEntry, ...]
- def _bfs_base_table(
- root: otBase.BaseTable, root_accessor: str
- ) -> Iterable[SubTablePath]:
- yield from _traverse_ot_data(
- root, root_accessor, lambda frontier, new: frontier.extend(new)
- )
- # Given f(current frontier, new entries) add new entries to frontier
- AddToFrontierFn = Callable[[Deque[SubTablePath], List[SubTablePath]], None]
- def _traverse_ot_data(
- root: otBase.BaseTable, root_accessor: str, add_to_frontier_fn: AddToFrontierFn
- ) -> Iterable[SubTablePath]:
- # no visited because general otData is forward-offset only and thus cannot cycle
- frontier: Deque[SubTablePath] = deque()
- frontier.append((otBase.BaseTable.SubTableEntry(root_accessor, root),))
- while frontier:
- # path is (value, attr_name) tuples. attr_name is attr of parent to get value
- path = frontier.popleft()
- current = path[-1].value
- yield path
- new_entries = []
- for subtable_entry in current.iterSubTables():
- new_entries.append(path + (subtable_entry,))
- add_to_frontier_fn(frontier, new_entries)
- def reorderGlyphs(font: ttLib.TTFont, new_glyph_order: List[str]):
- old_glyph_order = font.getGlyphOrder()
- if len(new_glyph_order) != len(old_glyph_order):
- raise ValueError(
- f"New glyph order contains {len(new_glyph_order)} glyphs, "
- f"but font has {len(old_glyph_order)} glyphs"
- )
- if set(old_glyph_order) != set(new_glyph_order):
- raise ValueError(
- "New glyph order does not contain the same set of glyphs as the font:\n"
- f"* only in new: {set(new_glyph_order) - set(old_glyph_order)}\n"
- f"* only in old: {set(old_glyph_order) - set(new_glyph_order)}"
- )
- # Changing the order of glyphs in a TTFont requires that all tables that use
- # glyph indexes have been fully.
- # Cf. https://github.com/fonttools/fonttools/issues/2060
- font.ensureDecompiled()
- not_loaded = sorted(t for t in font.keys() if not font.isLoaded(t))
- if not_loaded:
- raise ValueError(f"Everything should be loaded, following aren't: {not_loaded}")
- font.setGlyphOrder(new_glyph_order)
- coverage_containers = {"GDEF", "GPOS", "GSUB", "MATH"}
- for tag in coverage_containers:
- if tag in font.keys():
- for path in _bfs_base_table(font[tag].table, f'font["{tag}"]'):
- value = path[-1].value
- reorder_key = (type(value), getattr(value, "Format", None))
- for reorder in _REORDER_RULES.get(reorder_key, []):
- reorder.apply(font, value)
- if "CFF " in font:
- cff_table = font["CFF "]
- charstrings = cff_table.cff.topDictIndex[0].CharStrings.charStrings
- cff_table.cff.topDictIndex[0].charset = new_glyph_order
- cff_table.cff.topDictIndex[0].CharStrings.charStrings = {
- k: charstrings.get(k) for k in new_glyph_order
- }
|