mongfontbuilder 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,275 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from collections.abc import Iterator
5
+ from dataclasses import dataclass, field, replace
6
+ from typing import Iterable
7
+
8
+ from fontTools import unicodedata
9
+ from ufoLib2.objects import Component, Font, Glyph
10
+
11
+ from .data import (
12
+ CharacterName,
13
+ JoiningPosition,
14
+ LocaleID,
15
+ VariantData,
16
+ VariantReference,
17
+ WrittenUnitID,
18
+ codePointToCmapVariant,
19
+ )
20
+ from .data.misc import fina, init, isol, medi
21
+
22
+
23
+ def constructFont(font: Font, locales: list[LocaleID]) -> None:
24
+ from .otl import MongFeaComposer
25
+
26
+ constructPredefinedGlyphs(font, locales)
27
+
28
+ composer = MongFeaComposer(font, locales)
29
+ assert not font.features.text, font.features.text
30
+ font.features.text = composer.asFeatureFile().asFea().replace(":", "-") # HACK
31
+
32
+
33
+ def splitWrittens(writtens: str | Iterable[str]) -> list[WrittenUnitID]:
34
+ """
35
+ >>> splitWrittens("ABbCcc")
36
+ ['A', 'Bb', 'Ccc']
37
+ """
38
+
39
+ if isinstance(writtens, str):
40
+ return re.sub(r"[A-Z]", lambda x: " " + x[0], writtens).removeprefix(" ").split(" ")
41
+ return list(writtens)
42
+
43
+
44
+ def getPosition(index: int, length: int):
45
+ return isol if length == 1 else (init if index == 0 else fina if index == length - 1 else medi)
46
+
47
+
48
+ def writtenCombinations(writtens: str, position: JoiningPosition) -> Iterator[list[str]]:
49
+ """
50
+ >>> [*writtenCombinations("ABCD", "isol")]
51
+ [['A.init', 'B.medi', 'C.medi', 'D.fina'], ['A.init', 'B.medi', 'CD.fina'], ['A.init', 'BC.medi', 'D.fina'], ['A.init', 'BCD.fina'], ['AB.init', 'C.medi', 'D.fina'], ['AB.init', 'CD.fina'], ['ABC.init', 'D.fina'], ['ABCD.isol']]
52
+ """
53
+
54
+ parts = splitWrittens(writtens)
55
+ if "Lv" in parts:
56
+ index = parts.index("Lv")
57
+ if index > 0:
58
+ parts[index - 1] += parts.pop(index)
59
+
60
+ leftJoin = 1 if position in (medi, fina) else 0
61
+ rightJoin = 1 if position in (init, medi) else 0
62
+ placeholder = "X"
63
+ if leftJoin:
64
+ parts = [placeholder] + parts
65
+ if rightJoin:
66
+ parts += [placeholder]
67
+
68
+ combinations = [[]]
69
+ for part in parts:
70
+ newCombinations = []
71
+ for comb in combinations:
72
+ newCombinations.append(comb + [part])
73
+ if comb:
74
+ newCombinations.append(comb[:-1] + [comb[-1] + part])
75
+ combinations = newCombinations
76
+
77
+ for comb in combinations:
78
+ result = [
79
+ f"{written}.{getPosition(index, len(comb))}" for index, written in enumerate(comb)
80
+ ][leftJoin : (len(comb) - rightJoin)]
81
+ if (
82
+ len([l for w in result for l in w if l.isupper()])
83
+ == len(list(l for l in str(writtens) if l.isupper()))
84
+ and result
85
+ ):
86
+ yield result
87
+
88
+
89
+ @dataclass
90
+ class GlyphDescriptor:
91
+ codePoints: list[int]
92
+ units: list[WrittenUnitID]
93
+ position: JoiningPosition
94
+ suffixes: list[str] = field(default_factory=list)
95
+
96
+ @classmethod
97
+ def parse(cls, name: str) -> GlyphDescriptor:
98
+ """
99
+ >>> GlyphDescriptor.parse('u1820.A.init')
100
+ GlyphDescriptor(codePoints=[6176], units=['A'], position='init', suffixes=[])
101
+ >>> GlyphDescriptor.parse('_A.init')
102
+ GlyphDescriptor(codePoints=[], units=['A'], position='init', suffixes=[])
103
+ """
104
+
105
+ x, y, position, *suffixes = (
106
+ "." + name.removeprefix("_") # _A.init
107
+ if name.startswith("_")
108
+ else name # u1820.A.init
109
+ ).split(".")
110
+ units = splitWrittens(y)
111
+ assert units and all(i in data.writtenUnits for i in units), name
112
+ assert position in data.types.joiningPositions, name
113
+ instance = cls(
114
+ codePoints=[int(i.removeprefix("u"), 16) for i in x.split("_")] if x else [],
115
+ units=units,
116
+ position=position,
117
+ suffixes=suffixes,
118
+ )
119
+ assert str(instance) == name
120
+ return instance
121
+
122
+ @classmethod
123
+ def fromData(
124
+ cls,
125
+ charName: CharacterName,
126
+ position: JoiningPosition,
127
+ variantData: VariantData | None = None,
128
+ suffixes: list[str] = [],
129
+ locale: LocaleID | None = None,
130
+ ) -> GlyphDescriptor:
131
+ from .data import variantFromReference
132
+
133
+ if not variantData:
134
+ variantData = next(i for i in data.variants[charName][position].values() if i.default)
135
+
136
+ written = None
137
+ if locale and locale in variantData.locales:
138
+ written = variantData.locales[locale].written
139
+ if not written:
140
+ written = variantData.written
141
+ assert written, variantData
142
+
143
+ if isinstance(written, VariantReference):
144
+ units = variantFromReference(written, data.variants[charName])
145
+ suffixes = ["_" + position, *suffixes]
146
+ position = written.position
147
+ else:
148
+ units = written
149
+ return cls([ord(unicodedata.lookup(charName))], units, position, suffixes)
150
+
151
+ def __str__(self) -> str:
152
+ assert self.units, self
153
+ if self.codePoints:
154
+ name = "_".join(uNameFromCodePoint(i) for i in self.codePoints) + "."
155
+ else:
156
+ name = "_"
157
+ return name + ".".join(["".join(self.units), self.position, *self.suffixes])
158
+
159
+ def __add__(self, other: GlyphDescriptor) -> GlyphDescriptor:
160
+ """
161
+ >>> GlyphDescriptor.parse('u1820.A.init') + GlyphDescriptor.parse('u1820.A.medi')
162
+ GlyphDescriptor(codePoints=[6176, 6176], units=['A', 'A'], position='init', suffixes=[])
163
+ """
164
+ joiningType: dict[tuple[JoiningPosition, JoiningPosition], JoiningPosition] = {
165
+ ("init", "fina"): "isol",
166
+ ("init", "medi"): "init",
167
+ ("medi", "medi"): "medi",
168
+ ("medi", "fina"): "fina",
169
+ }
170
+ assert (self.position, other.position) in joiningType
171
+ return GlyphDescriptor(
172
+ self.codePoints + other.codePoints,
173
+ self.units + other.units,
174
+ joiningType[(self.position, other.position)],
175
+ )
176
+
177
+ def pseudoPosition(self) -> JoiningPosition | None:
178
+ if self.suffixes:
179
+ suffix = self.suffixes[0]
180
+ if suffix in pseudoPositionSuffixes:
181
+ position = suffix.removeprefix("_")
182
+ assert position in data.misc.joiningPositions
183
+ return position
184
+
185
+ def __hash__(self) -> int:
186
+ return hash(self.__str__())
187
+
188
+
189
+ def uNameFromCodePoint(codePoint: int) -> str:
190
+ return f"u{codePoint:04X}"
191
+
192
+
193
+ pseudoPositionSuffixes = ["_" + i for i in data.misc.joiningPositions]
194
+
195
+
196
+ def constructPredefinedGlyphs(
197
+ font: Font,
198
+ locales: list[LocaleID],
199
+ *,
200
+ initPadding: float = 40,
201
+ finaPadding: float = 100,
202
+ ) -> None:
203
+ sources = list[GlyphDescriptor]()
204
+ for name in font.keys():
205
+ try:
206
+ target = GlyphDescriptor.parse(name)
207
+ except:
208
+ continue
209
+ sources.append(target)
210
+
211
+ targetedLocales = {*locales}
212
+ for charName, positionToFVSToVariant in data.variants.items():
213
+ variantNames = list[str]()
214
+ for position, fvsToVariant in positionToFVSToVariant.items():
215
+ for variant in fvsToVariant.values():
216
+ if not targetedLocales.intersection(variant.locales):
217
+ continue
218
+
219
+ target = GlyphDescriptor.fromData(charName, position, variant)
220
+ targetName = str(target)
221
+ variantNames.append(targetName)
222
+
223
+ glyph = font.get(targetName)
224
+ if glyph is not None:
225
+ glyph.unicode = None
226
+ continue
227
+
228
+ memberNames: list[str]
229
+ writtenTarget = replace(target, codePoints=[], suffixes=[])
230
+ for source in sources:
231
+ if source == writtenTarget:
232
+ memberNames = [str(source)]
233
+ break
234
+ else:
235
+ for source in sources:
236
+ if replace(source, codePoints=[]) == writtenTarget:
237
+ memberNames = [str(source)]
238
+ break
239
+ else:
240
+ for writtenVariants in writtenCombinations(
241
+ "".join(writtenTarget.units), writtenTarget.position
242
+ ):
243
+ if len(writtenVariants) == len(writtenTarget.units):
244
+ memberNames = ["_" + i for i in writtenVariants]
245
+ break
246
+ else:
247
+ raise NotImplementedError(target)
248
+
249
+ members: list[Glyph | float] = [font[i] for i in memberNames]
250
+ if pseudoPosition := target.pseudoPosition():
251
+ if pseudoPosition in ["isol", "init"]:
252
+ members = [initPadding, *members]
253
+ if pseudoPosition in ["isol", "fina"]:
254
+ members = [*members, finaPadding]
255
+ composeGlyph(font, targetName, members)
256
+
257
+ if variantNames:
258
+ codePoint = ord(unicodedata.lookup(charName))
259
+ variant = GlyphDescriptor([codePoint], *codePointToCmapVariant[codePoint])
260
+ glyph = composeGlyph(font, uNameFromCodePoint(codePoint), [font[str(variant)]])
261
+ glyph.unicode = codePoint
262
+
263
+
264
+ def composeGlyph(font: Font, name: str, members: list[Glyph | float]) -> Glyph:
265
+ font.lib.get("public.glyphOrder", []).append(name)
266
+ glyph = font.newGlyph(name)
267
+ for member in members:
268
+ if isinstance(member, Glyph):
269
+ component = Component(str(member.name))
270
+ component.move((glyph.width, 0))
271
+ glyph.components.append(component)
272
+ glyph.width += member.width
273
+ else:
274
+ glyph.width += member
275
+ return glyph
@@ -0,0 +1,36 @@
1
+ """
2
+ poetry run python -m mongfontbuilder
3
+ """
4
+
5
+ from argparse import ArgumentParser
6
+ from pathlib import Path
7
+
8
+ from ufoLib2 import Font
9
+
10
+ from . import constructFont
11
+ from .data import locales
12
+
13
+ parser = ArgumentParser()
14
+ parser.add_argument(
15
+ "input",
16
+ type=Path,
17
+ help="path to read source UFO font from",
18
+ )
19
+ parser.add_argument(
20
+ "output",
21
+ type=Path,
22
+ help="path to write constructed UFO font to",
23
+ )
24
+ parser.add_argument(
25
+ "--locales",
26
+ metavar="LOCALE",
27
+ choices=locales,
28
+ nargs="+",
29
+ required=True,
30
+ help="targeted locales, one or more from: " + ", ".join(locales),
31
+ )
32
+
33
+ args = parser.parse_args()
34
+ font = Font.open(args.input)
35
+ constructFont(font, args.locales)
36
+ font.save(args.output, overwrite=True)
@@ -0,0 +1,3 @@
1
+ # Data files
2
+
3
+ JSON files in this directory are not manually maintained. They are exported from [/data/](/data/) with [/data/export.ts](/data/export.ts).
@@ -0,0 +1,100 @@
1
+ import json
2
+ from importlib.resources import files
3
+ from typing import Literal
4
+
5
+ from cattrs import structure
6
+ from fontTools import unicodedata
7
+
8
+ from .misc import JoiningPosition, joiningPositions
9
+ from .types import (
10
+ FVS,
11
+ AliasData,
12
+ CharacterName,
13
+ LocaleData,
14
+ LocaleID,
15
+ VariantData,
16
+ VariantReference,
17
+ WrittenUnitID,
18
+ )
19
+
20
+ assert __package__
21
+ dir = files(__package__)
22
+
23
+ with (dir / "writtenUnits.json").open(encoding="utf-8") as f:
24
+ writtenUnits: list[WrittenUnitID] = json.load(f)
25
+
26
+ with (dir / "ligatures.json").open(encoding="utf-8") as f:
27
+ ligatures: dict[
28
+ Literal["required", "optional"],
29
+ dict[str, list[JoiningPosition]],
30
+ ] = json.load(f)
31
+
32
+ with (dir / "locales.json").open(encoding="utf-8") as f:
33
+ locales = structure(
34
+ json.load(f),
35
+ dict[LocaleID, LocaleData],
36
+ )
37
+
38
+ with (dir / "aliases.json").open(encoding="utf-8") as f:
39
+ aliases = structure(
40
+ json.load(f),
41
+ dict[CharacterName, AliasData],
42
+ )
43
+
44
+ with (dir / "variants.json").open(encoding="utf-8") as f:
45
+ variants = structure(
46
+ json.load(f),
47
+ dict[CharacterName, dict[JoiningPosition, dict[FVS, VariantData]]],
48
+ )
49
+
50
+ with (dir / "particles.json").open(encoding="utf-8") as f:
51
+ particles = structure(json.load(f), dict[LocaleID, dict[str, list[FVS]]])
52
+
53
+
54
+ def variantFromReference(
55
+ reference: VariantReference,
56
+ positionToFVSToVariantData: dict[JoiningPosition, dict[FVS, VariantData]],
57
+ ) -> list[WrittenUnitID]:
58
+ position, fvs, locale = reference
59
+ if not locale:
60
+ written = positionToFVSToVariantData[position][fvs].written
61
+ else:
62
+ written = positionToFVSToVariantData[position][fvs].locales[locale].written
63
+ assert isinstance(written, list)
64
+ return written
65
+
66
+
67
+ def _resolveCmapVariants() -> dict[int, tuple[list[WrittenUnitID], JoiningPosition]]:
68
+ codePointToPositionToVariant = dict[
69
+ int, dict[JoiningPosition, tuple[list[WrittenUnitID], JoiningPosition]]
70
+ ]()
71
+ for charName, positionToFVSToVariantData in variants.items():
72
+ codePoint = ord(unicodedata.lookup(charName))
73
+ for position, fvsToVariantData in positionToFVSToVariantData.items():
74
+ for data in fvsToVariantData.values():
75
+ if data.default:
76
+ written = data.written
77
+ if isinstance(written, VariantReference):
78
+ variant = (
79
+ variantFromReference(written, positionToFVSToVariantData),
80
+ written.position,
81
+ )
82
+ else:
83
+ variant = written, position
84
+ codePointToPositionToVariant.setdefault(codePoint, {})[position] = variant
85
+ break
86
+
87
+ codePointToVariant = dict[int, tuple[list[WrittenUnitID], JoiningPosition]]()
88
+ for codePoint, positionToVariant in sorted(codePointToPositionToVariant.items()):
89
+ for position in joiningPositions:
90
+ if variant := positionToVariant.get(position):
91
+ if variant not in codePointToVariant.values():
92
+ codePointToVariant[codePoint] = variant
93
+ break
94
+ else:
95
+ raise NotImplementedError
96
+
97
+ return codePointToVariant
98
+
99
+
100
+ codePointToCmapVariant = _resolveCmapVariants()