fontisan 0.4.10 → 0.4.12
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.
- checksums.yaml +4 -4
- data/README.adoc +27 -1
- data/Rakefile +53 -39
- data/docs/AFDKO_MIGRATION.adoc +175 -0
- data/docs/FEATURE_PARITY.adoc +263 -0
- data/docs/STITCHER_GUIDE.adoc +100 -6
- data/docs/TTX.adoc +176 -0
- data/docs/UFO_COMPILATION.adoc +281 -3
- data/lib/fontisan/stitcher/partition_strategy/by_block.rb +429 -0
- data/lib/fontisan/stitcher/partition_strategy/by_script.rb +305 -0
- data/lib/fontisan/stitcher/partition_strategy.rb +2 -0
- data/lib/fontisan/stitcher/source.rb +11 -7
- data/lib/fontisan/subset/table_subsetter.rb +227 -11
- data/lib/fontisan/svg/standalone_glyph.rb +129 -0
- data/lib/fontisan/svg.rb +1 -0
- data/lib/fontisan/tasks/fixture_downloader.rb +162 -0
- data/lib/fontisan/tasks.rb +11 -0
- data/lib/fontisan/ufo/cli.rb +34 -21
- data/lib/fontisan/ufo/compile/feature_writers/base.rb +56 -0
- data/lib/fontisan/ufo/compile/feature_writers/curs.rb +72 -0
- data/lib/fontisan/ufo/compile/feature_writers/gdef.rb +90 -0
- data/lib/fontisan/ufo/compile/feature_writers/kern.rb +51 -0
- data/lib/fontisan/ufo/compile/feature_writers/kern2.rb +57 -0
- data/lib/fontisan/ufo/compile/feature_writers/mark.rb +45 -0
- data/lib/fontisan/ufo/compile/feature_writers/mark_family_base.rb +122 -0
- data/lib/fontisan/ufo/compile/feature_writers/mkmk.rb +53 -0
- data/lib/fontisan/ufo/compile/feature_writers.rb +37 -0
- data/lib/fontisan/ufo/compile/filters/propagate_anchors.rb +102 -0
- data/lib/fontisan/ufo/compile/filters/remove_overlaps.rb +94 -0
- data/lib/fontisan/ufo/compile/filters/sort_contours.rb +69 -0
- data/lib/fontisan/ufo/compile/filters/transformations.rb +113 -0
- data/lib/fontisan/ufo/compile/filters.rb +12 -0
- data/lib/fontisan/ufo/compile.rb +1 -0
- data/lib/fontisan/ufo/convert/to_dfont.rb +41 -0
- data/lib/fontisan/ufo/convert/to_otc.rb +37 -0
- data/lib/fontisan/ufo/convert/to_otf.rb +18 -0
- data/lib/fontisan/ufo/convert/to_otf2.rb +20 -0
- data/lib/fontisan/ufo/convert/to_postscript.rb +59 -0
- data/lib/fontisan/ufo/convert/to_ttc.rb +35 -0
- data/lib/fontisan/ufo/convert/to_ttf.rb +21 -0
- data/lib/fontisan/ufo/convert/to_woff.rb +56 -0
- data/lib/fontisan/ufo/convert/to_woff2.rb +45 -0
- data/lib/fontisan/ufo/convert.rb +68 -5
- data/lib/fontisan/ufo/transformation.rb +11 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +1 -0
- metadata +32 -2
data/docs/STITCHER_GUIDE.adoc
CHANGED
|
@@ -21,6 +21,13 @@ There are no defaults and no after-the-fact splitting.
|
|
|
21
21
|
|
|
22
22
|
| `add_source(label, font)` | Register a source font under a Symbol label
|
|
23
23
|
|
|
24
|
+
| `add_source(label, font, remap:)` | Register a source with a
|
|
25
|
+
`{src_codepoint => target_codepoint}` remap. Useful for donor
|
|
26
|
+
fonts whose glyphs live at non-canonical codepoints (PUA, ASCII-
|
|
27
|
+
mapped pre-Unicode fonts). Source codepoints not in the remap
|
|
28
|
+
are hidden — the donor's other coverage (often ASCII/PUA noise)
|
|
29
|
+
does not leak into the output.
|
|
30
|
+
|
|
24
31
|
| `include_range(range, from:, into:)` | Add codepoint range from `from` into `into`
|
|
25
32
|
|
|
26
33
|
| `include_codepoints(cps, from:, into:)` | Add an explicit Array of codepoints
|
|
@@ -81,6 +88,37 @@ Each subfont is compiled independently, then packed by
|
|
|
81
88
|
(head, name, OS/2, ...) are stored once and referenced from each
|
|
82
89
|
subfont's offset table.
|
|
83
90
|
|
|
91
|
+
== Donor remap (non-canonical codepoints)
|
|
92
|
+
|
|
93
|
+
Some donor fonts ship glyphs at codepoints that don't match the
|
|
94
|
+
canonical Unicode assignment — a keyboard-mapped font whose letters
|
|
95
|
+
live at ASCII slots, or a PUA-allocated pre-Unicode font. Pass a
|
|
96
|
+
`remap:` hash to `add_source` to expose those glyphs at their
|
|
97
|
+
canonical targets without mutating the donor's cmap:
|
|
98
|
+
|
|
99
|
+
[source,ruby]
|
|
100
|
+
----
|
|
101
|
+
donor = Fontisan::FontLoader.load("TolongSikiMappedToASCII.ttf")
|
|
102
|
+
|
|
103
|
+
stitcher = Fontisan::Stitcher.new
|
|
104
|
+
stitcher.add_source(:tolong_siki, donor, remap: {
|
|
105
|
+
0x41 => 0x11DB0, # ASCII A → Tolong Siki KA
|
|
106
|
+
0x42 => 0x11DB1, # ASCII B → Tolong Siki NGA
|
|
107
|
+
# ...
|
|
108
|
+
})
|
|
109
|
+
stitcher.include_codepoints((0x11DB0..0x11DC0).to_a,
|
|
110
|
+
from: :tolong_siki, into: :tolong)
|
|
111
|
+
----
|
|
112
|
+
|
|
113
|
+
With a remap set, the source answers `gid_for_codepoint(target)`
|
|
114
|
+
by translating through the inverse remap to the donor's source
|
|
115
|
+
codepoint. Source codepoints not in the remap are hidden — only
|
|
116
|
+
remapped coverage is exposed.
|
|
117
|
+
|
|
118
|
+
The donor's cmap is never mutated. `remap:` is a translation layer
|
|
119
|
+
at the source's API boundary; multiple sources on the same
|
|
120
|
+
Stitcher can have different remaps (or none at all).
|
|
121
|
+
|
|
84
122
|
`write_collection` returns a `Stitcher::CollectionResult` Struct with
|
|
85
123
|
the output `path`, total `bytes`, and one `Stitcher::SubfontStats`
|
|
86
124
|
per declared subfont (`name`, `glyph_count`, `codepoint_count`). Stats
|
|
@@ -188,13 +226,69 @@ A `Blueprint` is a `Struct.new(:partitions, keyword_init: true)`;
|
|
|
188
226
|
`Blueprint#apply_to(stitcher)` calls `apply_to` on each partition in
|
|
189
227
|
order and returns the list of declared subfont names.
|
|
190
228
|
|
|
191
|
-
===
|
|
229
|
+
=== ByBlock — partition by Unicode Blocks.txt
|
|
230
|
+
|
|
231
|
+
`PartitionStrategy::ByBlock` partitions codepoints by their Unicode
|
|
232
|
+
block. Each non-empty block becomes one partition. The full Unicode
|
|
233
|
+
16.0 block list (~340 entries covering BMP, SMP, SIP, TIP, SSP) is
|
|
234
|
+
inlined as `ByBlock::BLOCKS` so the partitioner is self-contained
|
|
235
|
+
(no external data file). Codepoints in unassigned planes fall into
|
|
236
|
+
`:block_other`.
|
|
237
|
+
|
|
238
|
+
[source,ruby]
|
|
239
|
+
----
|
|
240
|
+
bp = Fontisan::Stitcher::PartitionStrategy::ByBlock.new
|
|
241
|
+
blueprint = bp.call(cp_map)
|
|
242
|
+
blueprint.names # => [:block_basic_latin, :block_cjk_unified_ideographs, ...]
|
|
243
|
+
----
|
|
244
|
+
|
|
245
|
+
Every block is treated as atomic — if a single block alone exceeds
|
|
246
|
+
`cap`, ByBlock raises `PartitionCapExceededError` (callers must
|
|
247
|
+
either use ByPlane with carve-outs or implement a custom
|
|
248
|
+
partitioner).
|
|
249
|
+
|
|
250
|
+
=== ByScript — partition by Unicode script property
|
|
251
|
+
|
|
252
|
+
`PartitionStrategy::ByScript` partitions codepoints by Unicode
|
|
253
|
+
script (Latin, Greek, Cyrillic, Han, Hangul, etc.). Derived from
|
|
254
|
+
`ByBlock::BLOCKS` via a `SCRIPT_OF_BLOCK` map — keeps the data DRY.
|
|
255
|
+
|
|
256
|
+
Scripts that overflow `cap` are chunked with alphabetic suffixes
|
|
257
|
+
matching ByPlane's sub-split convention (e.g. `:script_han`,
|
|
258
|
+
`:script_han_a`, `:script_han_b`). Common and Inherited scripts
|
|
259
|
+
get their own buckets — useful for separating shared punctuation
|
|
260
|
+
and combining marks.
|
|
261
|
+
|
|
262
|
+
[source,ruby]
|
|
263
|
+
----
|
|
264
|
+
sp = Fontisan::Stitcher::PartitionStrategy::ByScript.new
|
|
265
|
+
blueprint = sp.call(cp_map)
|
|
266
|
+
blueprint.names # => [:script_common, :script_latin, :script_han, ...]
|
|
267
|
+
----
|
|
268
|
+
|
|
269
|
+
=== Choosing a strategy
|
|
270
|
+
|
|
271
|
+
|===
|
|
272
|
+
| Strategy | When to use
|
|
273
|
+
|
|
274
|
+
| `ByPlane`
|
|
275
|
+
| CJK-heavy fonts that need to respect the 65,535-glyph cap per
|
|
276
|
+
plane. Sub-splits only the SIP CJK extensions and the BMP
|
|
277
|
+
carve-out blocks.
|
|
278
|
+
|
|
279
|
+
| `ByBlock`
|
|
280
|
+
| Fonts that span many small Unicode blocks where each block
|
|
281
|
+
should land in its own subfont (e.g. for selective subsetting
|
|
282
|
+
per block). Each block is atomic.
|
|
283
|
+
|
|
284
|
+
| `ByScript`
|
|
285
|
+
| Web font delivery where each subfont should cover one script
|
|
286
|
+
(Latin-only file, Greek-only file, etc.). Chunks large scripts
|
|
287
|
+
(Han) when they overflow the cap.
|
|
288
|
+
|===
|
|
192
289
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
add `by_block.rb` / `by_script.rb` under
|
|
196
|
-
`lib/fontisan/stitcher/partition_strategy/` and an autoload entry in
|
|
197
|
-
`partition_strategy.rb`.
|
|
290
|
+
All three autoload from `lib/fontisan/stitcher/partition_strategy/`.
|
|
291
|
+
Adding a new strategy = new file + one autoload entry (OCP).
|
|
198
292
|
|
|
199
293
|
== Per-subfont metadata (Ufo::Info.for_subfont)
|
|
200
294
|
|
data/docs/TTX.adoc
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
= TTX (FontTools XML) Support
|
|
2
|
+
|
|
3
|
+
fontisan can read and write the TTX format — the human-readable
|
|
4
|
+
XML representation of OpenType fonts defined by Adobe/FontTools'.
|
|
5
|
+
`ttx` tool. TTX is the standard interchange format for OpenType
|
|
6
|
+
table data: every binary table has a corresponding XML element,
|
|
7
|
+
and round-tripping TTX ↔ binary preserves the table contents.
|
|
8
|
+
|
|
9
|
+
== Why TTX
|
|
10
|
+
|
|
11
|
+
* **Diff-friendly**: TTX is text, so changes to a font's tables can
|
|
12
|
+
be reviewed in version control.
|
|
13
|
+
* **Edit-friendly**: you can hand-edit table values without writing
|
|
14
|
+
binary parsers.
|
|
15
|
+
* **Tool interchange**: any tool that understands TTX (fontTools,
|
|
16
|
+
AFDKO's `ttx`, several IDE plugins) can read fontisan's output
|
|
17
|
+
and vice versa.
|
|
18
|
+
|
|
19
|
+
== What fontisan supports
|
|
20
|
+
|
|
21
|
+
[cols="1,3", options="header"]
|
|
22
|
+
|===
|
|
23
|
+
| Capability | Notes
|
|
24
|
+
|
|
25
|
+
| Read `.ttx` (parse XML → typed tables)
|
|
26
|
+
| Every standard table that fontisan parses as binary also has a
|
|
27
|
+
TTX parser entry. Unknown tables fall through to a binary-blob
|
|
28
|
+
representation.
|
|
29
|
+
|
|
30
|
+
| Write `.ttx` (typed tables → XML)
|
|
31
|
+
| Same coverage as the read path. Each table knows how to render
|
|
32
|
+
itself as TTX.
|
|
33
|
+
|
|
34
|
+
| CLI `fontisan export FONT --format ttx`
|
|
35
|
+
| Dumps every table or a subset via `--tables`.
|
|
36
|
+
|
|
37
|
+
| CLI `fontisan info FONT` (auto-detect)
|
|
38
|
+
| Auto-detects `.ttx` files alongside binary formats. No
|
|
39
|
+
`--format` flag needed.
|
|
40
|
+
|
|
41
|
+
| Per-table type mapping (lib/fontisan/models/ttx/tables/*.rb)
|
|
42
|
+
| `head`, `hhea`, `maxp`, `name`, `OS/2`, `post` have typed
|
|
43
|
+
representations; everything else is `BinaryTable` (raw bytes).
|
|
44
|
+
|===
|
|
45
|
+
|
|
46
|
+
== CLI usage
|
|
47
|
+
|
|
48
|
+
[source,bash]
|
|
49
|
+
----
|
|
50
|
+
# Dump the entire font as TTX
|
|
51
|
+
fontisan export MyFont.ttf --format ttx --output MyFont.ttx
|
|
52
|
+
|
|
53
|
+
# Dump just the head + name tables
|
|
54
|
+
fontisan export MyFont.ttf --format ttx --tables head name --output subset.ttx
|
|
55
|
+
|
|
56
|
+
# Read a TTX file (auto-detected)
|
|
57
|
+
fontisan info MyFont.ttx
|
|
58
|
+
fontisan validate MyFont.ttx
|
|
59
|
+
----
|
|
60
|
+
|
|
61
|
+
== Ruby API
|
|
62
|
+
|
|
63
|
+
=== Generate TTX from a loaded font
|
|
64
|
+
|
|
65
|
+
[source,ruby]
|
|
66
|
+
----
|
|
67
|
+
require "fontisan"
|
|
68
|
+
|
|
69
|
+
font = Fontisan::FontLoader.load("MyFont.ttf")
|
|
70
|
+
generator = Fontisan::Export::TtxGenerator.new(font, "MyFont.ttf")
|
|
71
|
+
ttx_xml = generator.generate
|
|
72
|
+
File.write("MyFont.ttx", ttx_xml)
|
|
73
|
+
----
|
|
74
|
+
|
|
75
|
+
=== Parse TTX into typed tables
|
|
76
|
+
|
|
77
|
+
[source,ruby]
|
|
78
|
+
----
|
|
79
|
+
require "fontisan/export"
|
|
80
|
+
|
|
81
|
+
ttx_xml = File.read("MyFont.ttx")
|
|
82
|
+
parser = Fontisan::Export::TtxParser.new
|
|
83
|
+
tt_font = parser.parse(ttx_xml)
|
|
84
|
+
|
|
85
|
+
puts "sfnt version: #{tt_font.sfnt_version}"
|
|
86
|
+
puts "tables: #{tt_font.tables.keys.join(', ')}"
|
|
87
|
+
|
|
88
|
+
head = tt_font.tables["head"]
|
|
89
|
+
puts "units per em: #{head.units_per_em}" if head
|
|
90
|
+
----
|
|
91
|
+
|
|
92
|
+
=== Round-trip
|
|
93
|
+
|
|
94
|
+
Read TTX → modify → write TTX:
|
|
95
|
+
|
|
96
|
+
[source,ruby]
|
|
97
|
+
----
|
|
98
|
+
parser = Fontisan::Export::TtxParser.new
|
|
99
|
+
tt_font = parser.parse(File.read("MyFont.ttx"))
|
|
100
|
+
|
|
101
|
+
# Bump units-per-em (caution: this rescales visually)
|
|
102
|
+
tt_font.tables["head"].units_per_em = 2048
|
|
103
|
+
|
|
104
|
+
# Re-emit as TTX
|
|
105
|
+
generator = Fontisan::Export::TtxGenerator.from_tt_font(tt_font)
|
|
106
|
+
File.write("MyFont-edited.ttx", generator.generate)
|
|
107
|
+
----
|
|
108
|
+
|
|
109
|
+
== Typed table coverage
|
|
110
|
+
|
|
111
|
+
fontisan's TTX layer is a parallel hierarchy to the binary
|
|
112
|
+
`Tables::*` classes. Each TTX table class lives under
|
|
113
|
+
`Fontisan::Models::Ttx::Tables`:
|
|
114
|
+
|
|
115
|
+
[cols="1,1", options="header"]
|
|
116
|
+
|===
|
|
117
|
+
| Binary table | TTX class
|
|
118
|
+
|
|
119
|
+
| `head`
|
|
120
|
+
| `Models::Ttx::Tables::HeadTable`
|
|
121
|
+
|
|
122
|
+
| `hhea`
|
|
123
|
+
| `Models::Ttx::Tables::HheaTable`
|
|
124
|
+
|
|
125
|
+
| `maxp`
|
|
126
|
+
| `Models::Ttx::Tables::MaxpTable`
|
|
127
|
+
|
|
128
|
+
| `name`
|
|
129
|
+
| `Models::Ttx::Tables::NameTable`
|
|
130
|
+
|
|
131
|
+
| `OS/2`
|
|
132
|
+
| `Models::Ttx::Tables::Os2Table`
|
|
133
|
+
|
|
134
|
+
| `post`
|
|
135
|
+
| `Models::Ttx::Tables::PostTable`
|
|
136
|
+
|
|
137
|
+
| (any other)
|
|
138
|
+
| `Models::Ttx::Tables::BinaryTable` — raw bytes, no typed
|
|
139
|
+
interpretation
|
|
140
|
+
|===
|
|
141
|
+
|
|
142
|
+
Adding a new typed TTX representation = one file under
|
|
143
|
+
`models/ttx/tables/` + one entry in the parser dispatch table.
|
|
144
|
+
|
|
145
|
+
== Compatibility with fontTools `ttx`
|
|
146
|
+
|
|
147
|
+
fontisan's TTX output is byte-compatible with fontTools `ttx`
|
|
148
|
+
output for the typed tables. The XML element names, attribute
|
|
149
|
+
names, and value formats match the fontTools schema.
|
|
150
|
+
|
|
151
|
+
The `BinaryTable` representation differs slightly: fontTools
|
|
152
|
+
emits a hex-dumped `<hexdata>` block for tables it doesn't
|
|
153
|
+
understand; fontisan emits the same format.
|
|
154
|
+
|
|
155
|
+
== Related formats
|
|
156
|
+
|
|
157
|
+
fontisan also exports YAML and JSON via the same `fontisan export`
|
|
158
|
+
command — useful for scripting and AI/automation pipelines where
|
|
159
|
+
XML's verbosity is a drawback:
|
|
160
|
+
|
|
161
|
+
[source,bash]
|
|
162
|
+
----
|
|
163
|
+
fontisan export MyFont.ttf --format yaml --output MyFont.yaml
|
|
164
|
+
fontisan export MyFont.ttf --format json --output MyFont.json
|
|
165
|
+
----
|
|
166
|
+
|
|
167
|
+
== See also
|
|
168
|
+
|
|
169
|
+
* link:UFO_COMPILATION.adoc[UFO Compilation Guide] — UFO is a
|
|
170
|
+
richer source format than TTX (full glyph outlines + naming +
|
|
171
|
+
features); use TTX for binary-table inspection, UFO for source
|
|
172
|
+
editing.
|
|
173
|
+
* link:AFDKO_MIGRATION.adoc[AFDKO Migration Guide] — fontisan
|
|
174
|
+
replaces AFDKO's `ttx` command.
|
|
175
|
+
* link:FEATURE_PARITY.adoc[Feature Parity] — every external tool
|
|
176
|
+
fontisan replaces.
|
data/docs/UFO_COMPILATION.adoc
CHANGED
|
@@ -110,10 +110,288 @@ ufo = Fontisan::Ufo::Convert::FromBinData.convert(ttf)
|
|
|
110
110
|
Fontisan::Ufo::Writer.new(ufo).write("MyFont.ufo")
|
|
111
111
|
----
|
|
112
112
|
|
|
113
|
+
== Conversion wrappers (Phase 3)
|
|
114
|
+
|
|
115
|
+
For every binary format fontisan can write, there's a UFO → that
|
|
116
|
+
format wrapper under `Ufo::Convert`. They split into two flavors:
|
|
117
|
+
|
|
118
|
+
1-step wrappers (compile + write directly): `ToTtf`, `ToOtf`,
|
|
119
|
+
`ToOtf2`.
|
|
120
|
+
|
|
121
|
+
2-step wrappers (compile to TTF/OTF in a tmpfile, then encode):
|
|
122
|
+
`ToWoff`, `ToWoff2`, `ToDfont`, `ToTtc`, `ToOtc`, `ToPfb`,
|
|
123
|
+
`ToPfa`.
|
|
124
|
+
|
|
125
|
+
The 2-step chain looks like:
|
|
126
|
+
|
|
127
|
+
....
|
|
128
|
+
UFO → Compile::*Compiler → tmpfile → FontLoader.load →
|
|
129
|
+
WoffWriter / Woff2Encoder / DfontBuilder /
|
|
130
|
+
Collection::Builder / Type1::PFB/PFAGenerator → output
|
|
131
|
+
....
|
|
132
|
+
|
|
133
|
+
The tmpfile is auto-cleaned (`Dir.mktmpdir`); the original UFO is
|
|
134
|
+
never mutated.
|
|
135
|
+
|
|
136
|
+
Use the unified dispatcher to pick a format by symbol:
|
|
137
|
+
|
|
138
|
+
[source,ruby]
|
|
139
|
+
----
|
|
140
|
+
require "fontisan"
|
|
141
|
+
|
|
142
|
+
ufo = Fontisan::Ufo::Font.open("MyFont.ufo")
|
|
143
|
+
|
|
144
|
+
# 1-step
|
|
145
|
+
Fontisan::Ufo::Convert.convert(ufo, to: :ttf, output_path: "MyFont.ttf")
|
|
146
|
+
Fontisan::Ufo::Convert.convert(ufo, to: :otf, output_path: "MyFont.otf")
|
|
147
|
+
|
|
148
|
+
# 2-step
|
|
149
|
+
Fontisan::Ufo::Convert.convert(ufo, to: :woff2, output_path: "MyFont.woff2")
|
|
150
|
+
Fontisan::Ufo::Convert.convert(ufo, to: :pfb, output_path: "MyFont.pfb")
|
|
151
|
+
----
|
|
152
|
+
|
|
153
|
+
Format keys: `:ttf`, `:otf`, `:otf2`, `:woff`, `:woff2`, `:dfont`,
|
|
154
|
+
`:ttc`, `:otc`, `:pfb`, `:pfa`. Unknown keys raise `ArgumentError`.
|
|
155
|
+
|
|
156
|
+
For per-format options (zlib level, brotli quality, etc.) call the
|
|
157
|
+
wrapper module directly — options forward through:
|
|
158
|
+
|
|
159
|
+
[source,ruby]
|
|
160
|
+
----
|
|
161
|
+
Fontisan::Ufo::Convert::ToWoff2.convert(ufo,
|
|
162
|
+
output_path: "MyFont.woff2",
|
|
163
|
+
compiler: :otf, # intermediate format
|
|
164
|
+
brotli_quality: 11, # passed to Woff2Encoder
|
|
165
|
+
transform_tables: true)
|
|
166
|
+
----
|
|
167
|
+
|
|
168
|
+
== Glyph-processing filters (Phase 2)
|
|
169
|
+
|
|
170
|
+
Filters sit between the UFO model and the binary compilers. Each is
|
|
171
|
+
a stateless module under `Ufo::Compile::Filters` with a `.run(glyphs,
|
|
172
|
+
**opts)` entry that mutates the glyph list in place. They're
|
|
173
|
+
registered in `Filters::REGISTRY` so the compiler (or any caller)
|
|
174
|
+
can apply them by symbol:
|
|
175
|
+
|
|
176
|
+
[source,ruby]
|
|
177
|
+
----
|
|
178
|
+
glyphs = ufo.layers.default_layer.glyphs.values
|
|
179
|
+
Fontisan::Ufo::Compile::Filters.apply(
|
|
180
|
+
%i[cubic_to_quadratic reverse_contour_direction sort_contours],
|
|
181
|
+
glyphs
|
|
182
|
+
)
|
|
183
|
+
----
|
|
184
|
+
|
|
185
|
+
Available filters:
|
|
186
|
+
|
|
187
|
+
[cols="1,3", options="header"]
|
|
188
|
+
|===
|
|
189
|
+
| Filter | What it does
|
|
190
|
+
|
|
191
|
+
| `cubic_to_quadratic`
|
|
192
|
+
| Convert cubic Bezier curves to quadratic (TrueType only supports
|
|
193
|
+
quadratic; UFO sources typically use cubic).
|
|
194
|
+
|
|
195
|
+
| `reverse_contour_direction`
|
|
196
|
+
| Reverse point order in every contour (TrueType wants clockwise
|
|
197
|
+
outer winding; UFO is PostScript counter-clockwise).
|
|
198
|
+
|
|
199
|
+
| `decompose_components`
|
|
200
|
+
| Replace composite-glyph components with their base glyph's
|
|
201
|
+
contours.
|
|
202
|
+
|
|
203
|
+
| `flatten_components`
|
|
204
|
+
| Collapse nested component chains (A→B→C becomes A→C).
|
|
205
|
+
|
|
206
|
+
| `transformations`
|
|
207
|
+
| Apply a 2×3 affine matrix to every glyph's contours + components
|
|
208
|
+
(mirroring, slant, scaling, custom offsets).
|
|
209
|
+
|
|
210
|
+
| `sort_contours`
|
|
211
|
+
| Rotate each contour to start with an on-curve point and sort
|
|
212
|
+
contours within each glyph by Y (descending) for hinting
|
|
213
|
+
compatibility.
|
|
214
|
+
|
|
215
|
+
| `propagate_anchors`
|
|
216
|
+
| Copy anchors from base glyphs to composite glyphs (transformed by
|
|
217
|
+
the component matrix) so the GPOS mark feature writer can find
|
|
218
|
+
them on composites.
|
|
219
|
+
|===
|
|
220
|
+
|
|
221
|
+
Adding a new filter = one file under `compile/filters/` + one entry
|
|
222
|
+
in `REGISTRY`. No edits to other filters required (OCP).
|
|
223
|
+
|
|
224
|
+
== Feature writers (Phase 2)
|
|
225
|
+
|
|
226
|
+
Feature writers extract GSUB/GPOS feature data from a UFO source
|
|
227
|
+
into structured form for the table builders to encode. Each writer
|
|
228
|
+
extends `FeatureWriters::Base` with a `#write` method that returns
|
|
229
|
+
either a `FeatureOutput` value object or `nil` (when the UFO has
|
|
230
|
+
no data for the feature).
|
|
231
|
+
|
|
232
|
+
[source,ruby]
|
|
233
|
+
----
|
|
234
|
+
require "fontisan/ufo/compile/feature_writers"
|
|
235
|
+
|
|
236
|
+
font = Fontisan::Ufo::Font.open("MyFont.ufo")
|
|
237
|
+
Fontisan::Ufo::Compile::FeatureWriters::DEFAULT_WRITERS.each do |writer_cls|
|
|
238
|
+
output = writer_cls.new(font).write
|
|
239
|
+
next unless output
|
|
240
|
+
|
|
241
|
+
puts "#{output.table_tag} / #{output.feature_tag} (lookup #{output.lookup_type})"
|
|
242
|
+
end
|
|
243
|
+
----
|
|
244
|
+
|
|
245
|
+
Available writers:
|
|
246
|
+
|
|
247
|
+
[cols="1,1,3", options="header"]
|
|
248
|
+
|===
|
|
249
|
+
| Writer | GPOS / GSUB | What it emits
|
|
250
|
+
|
|
251
|
+
| `FeatureWriters::Kern`
|
|
252
|
+
| GPOS lookup 2 (`kern`)
|
|
253
|
+
| PairPos kerning pairs from `font.kerning` (parsed from
|
|
254
|
+
`kerning.plist`).
|
|
255
|
+
|
|
256
|
+
| `FeatureWriters::Kern2`
|
|
257
|
+
| GPOS lookup 2 (`kern`, format 2)
|
|
258
|
+
| Same pair data as Kern, tagged for the extended PairPos format
|
|
259
|
+
(device tables, cross-stream values). Use when you need
|
|
260
|
+
sub-pixel kerning.
|
|
261
|
+
|
|
262
|
+
| `FeatureWriters::Gdef`
|
|
263
|
+
| GDEF
|
|
264
|
+
| GlyphClassDef — classifies every glyph as base/ligature/mark/
|
|
265
|
+
spacing. Prefers `public.openTypeCategory` lib entry; falls
|
|
266
|
+
back to heuristic (mark anchors, `.liga` suffix).
|
|
267
|
+
|
|
268
|
+
| `FeatureWriters::Mark`
|
|
269
|
+
| GPOS lookup 4 (`mark`)
|
|
270
|
+
| MarkToBase attachment. Pairs every glyph with a `_<name>`
|
|
271
|
+
anchor (mark) against every glyph with the matching `<name>`
|
|
272
|
+
anchor (base).
|
|
273
|
+
|
|
274
|
+
| `FeatureWriters::Mkmk`
|
|
275
|
+
| GPOS lookup 6 (`mkmk`)
|
|
276
|
+
| MarkToMark attachment. Same convention as Mark but pairs marks
|
|
277
|
+
with other marks. Anchors are `_<name>mkmk` (attach-from) ↔
|
|
278
|
+
`<name>mkmk` (attach-to).
|
|
279
|
+
|
|
280
|
+
| `FeatureWriters::Curs`
|
|
281
|
+
| GPOS lookup 3 (`curs`)
|
|
282
|
+
| Cursive attachment. Emits entry/exit anchor positions for
|
|
283
|
+
joining-script glyphs.
|
|
284
|
+
|===
|
|
285
|
+
|
|
286
|
+
`FeatureWriters::DEFAULT_WRITERS = [Gdef, Kern, Mark, Mkmk, Curs]`.
|
|
287
|
+
The compiler runs all by default; each returns `nil` when the UFO
|
|
288
|
+
has no data for its feature, so it's safe to always run them all.
|
|
289
|
+
|
|
290
|
+
Adding a new writer = one file under `compile/feature_writers/` +
|
|
291
|
+
one entry in `DEFAULT_WRITERS`. No edits to other writers required
|
|
292
|
+
(OCP).
|
|
293
|
+
|
|
294
|
+
Prerequisite for Mark / Mkmk / Curs: run
|
|
295
|
+
`Filters::PropagateAnchors` first so composite glyphs carry their
|
|
296
|
+
inherited anchors.
|
|
297
|
+
|
|
298
|
+
=== Usage examples
|
|
299
|
+
|
|
300
|
+
==== Kern (pair kerning)
|
|
301
|
+
|
|
302
|
+
Extract kerning pairs from a UFO's `kerning.plist`:
|
|
303
|
+
|
|
304
|
+
[source,ruby]
|
|
305
|
+
----
|
|
306
|
+
font = Fontisan::Ufo::Font.open("MyFont.ufo")
|
|
307
|
+
out = Fontisan::Ufo::Compile::FeatureWriters::Kern.new(font).write
|
|
308
|
+
|
|
309
|
+
out.data[:pairs].each do |pair|
|
|
310
|
+
puts "#{pair[:left]} + #{pair[:right]} = #{pair[:value]}"
|
|
311
|
+
end
|
|
312
|
+
# A + V = -80.0
|
|
313
|
+
# A + W = -60.0
|
|
314
|
+
# ...
|
|
315
|
+
----
|
|
316
|
+
|
|
317
|
+
==== Gdef (glyph classification)
|
|
318
|
+
|
|
319
|
+
Classify every glyph for GDEF:
|
|
320
|
+
|
|
321
|
+
[source,ruby]
|
|
322
|
+
----
|
|
323
|
+
out = Fontisan::Ufo::Compile::FeatureWriters::Gdef.new(font).write
|
|
324
|
+
out.data[:classes].each { |name, cls| puts "#{name}: #{cls}" }
|
|
325
|
+
# A: 1 (base)
|
|
326
|
+
# f_f_i.liga: 2 (ligature)
|
|
327
|
+
# acutecomb: 3 (mark)
|
|
328
|
+
# ...
|
|
329
|
+
----
|
|
330
|
+
|
|
331
|
+
==== Mark (mark-to-base attachment)
|
|
332
|
+
|
|
333
|
+
Pair diacritical marks with base glyphs:
|
|
334
|
+
|
|
335
|
+
[source,ruby]
|
|
336
|
+
----
|
|
337
|
+
out = Fontisan::Ufo::Compile::FeatureWriters::Mark.new(font).write
|
|
338
|
+
out.data[:attachments]["top"][:marks] # => {"acutecomb" => [100, 600], ...}
|
|
339
|
+
out.data[:attachments]["top"][:bases] # => {"A" => [100, 700], "B" => [90, 700], ...}
|
|
340
|
+
----
|
|
341
|
+
|
|
342
|
+
==== Mkmk (mark-to-mark attachment)
|
|
343
|
+
|
|
344
|
+
Stack marks above each other:
|
|
345
|
+
|
|
346
|
+
[source,ruby]
|
|
347
|
+
----
|
|
348
|
+
out = Fontisan::Ufo::Compile::FeatureWriters::Mkmk.new(font).write
|
|
349
|
+
out.data[:attachments]["top"][:marks] # => {"acutecomb" => [100, 650]}
|
|
350
|
+
out.data[:attachments]["top"][:base_marks] # => {"dieresiscomb" => [100, 700]}
|
|
351
|
+
----
|
|
352
|
+
|
|
353
|
+
==== Curs (cursive joining)
|
|
354
|
+
|
|
355
|
+
Emit entry/exit anchors for Arabic-script glyphs:
|
|
356
|
+
|
|
357
|
+
[source,ruby]
|
|
358
|
+
----
|
|
359
|
+
out = Fontisan::Ufo::Compile::FeatureWriters::Curs.new(font).write
|
|
360
|
+
out.data[:attachments]["alef"] # => {entry: [0, 100], exit: [100, 100]}
|
|
361
|
+
out.data[:attachments"]["beh"] # => {entry: [50, 50], exit: nil}
|
|
362
|
+
----
|
|
363
|
+
|
|
364
|
+
== CLI
|
|
365
|
+
|
|
366
|
+
`fontisan ufo` exposes the four common operations:
|
|
367
|
+
|
|
368
|
+
[source,bash]
|
|
369
|
+
----
|
|
370
|
+
# Compile a UFO to a binary font
|
|
371
|
+
fontisan ufo build MyFont.ufo --output MyFont.ttf [--to otf|otf2]
|
|
372
|
+
|
|
373
|
+
# Convert in either direction (UFO ↔ binary)
|
|
374
|
+
fontisan ufo convert MyFont.ufo MyFont.woff2
|
|
375
|
+
fontisan ufo convert MyFont.ttf MyFont.ufo
|
|
376
|
+
|
|
377
|
+
# Structural checks (no .notdef, no family name, etc.)
|
|
378
|
+
fontisan ufo validate MyFont.ufo
|
|
379
|
+
|
|
380
|
+
# Render one glyph as a standalone SVG
|
|
381
|
+
fontisan ufo extract MyFont.ufo A A.svg
|
|
382
|
+
----
|
|
383
|
+
|
|
384
|
+
The `build` and `convert` commands route through the
|
|
385
|
+
`Ufo::Convert.convert` dispatcher, so adding a new format = one
|
|
386
|
+
registry entry — no CLI edits.
|
|
387
|
+
|
|
113
388
|
== See also
|
|
114
389
|
|
|
390
|
+
* link:AFDKO_MIGRATION.adoc[AFDKO Migration Guide] — switching from
|
|
391
|
+
`makeotf` / `otf2ttf` / `ttx` to fontisan's UFO pipeline.
|
|
115
392
|
* link:CFF2_SUPPORT.adoc[CFF2 Support Guide] — variable-font blend
|
|
116
|
-
operators, FDSelect, subroutines, VariationStore
|
|
393
|
+
operators, FDSelect, subroutines, VariationStore.
|
|
117
394
|
* link:VARIABLE_FONT_OPERATIONS.adoc[Variable Font Operations] —
|
|
118
|
-
reading and modifying existing variable fonts
|
|
119
|
-
|
|
395
|
+
reading and modifying existing variable fonts.
|
|
396
|
+
* link:STITCHER_GUIDE.adoc[Stitcher Guide] — multi-source assembly.
|
|
397
|
+
|