fontisan 0.4.24 → 0.4.26
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/TODO.improvements/01-cbdt-cblc-gid-stable-propagation.md +73 -0
- data/TODO.improvements/02-collection-outline-priority.md +48 -0
- data/TODO.improvements/03-fontisan-audit-command.md +219 -0
- data/TODO.improvements/04-ufo-composite-glyph-encoding.md +46 -0
- data/TODO.improvements/05-otf-compiler-real-cff.md +63 -0
- data/TODO.improvements/06-cff2-blend-vsindex-operators.md +51 -0
- data/TODO.improvements/07-cpal-v1-header-fields.md +71 -0
- data/TODO.improvements/08-cff-standard-string-table.md +52 -0
- data/TODO.improvements/09-type1-seac-expansion.md +57 -0
- data/TODO.improvements/10-ufo-image-set-feature-writers.md +71 -0
- data/TODO.improvements/11-kern-groups-plist.md +49 -0
- data/TODO.improvements/12-cbdt-fixture-bindata-conversion.md +68 -0
- data/TODO.improvements/13-split-octokit-fetcher.md +45 -0
- data/TODO.improvements/14-rubocop-baseline-chip.md +62 -0
- data/TODO.improvements/README.md +57 -0
- data/lib/fontisan/tables/cpal.rb +112 -11
- data/lib/fontisan/version.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d9d761c9308f66218312a055b752135a728413c634fba54caa309c37076a03f0
|
|
4
|
+
data.tar.gz: 8ec9394bfde51dec443f7b854ab29d698a9cb70c63543ea4067b0fc23ffd10fc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a123990197a2754d9039c7c12f9ed8432165ded5f1c8c1b10c6f150d03907ecd09bc0baf50d40e853ccbad4c340b0694718ea74759b6dacb7fe3796212e8c703
|
|
7
|
+
data.tar.gz: c0510eee21f156b9f9d7e0110da16372a4c8418774144294efa7fcfcb7b049da72b42f6d683b1a8043e91b2f5ce66d201f3ec84173eead40d5bdab36d8d41ca2
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# 01 — CBDT/CBLC GID-stable propagation
|
|
2
|
+
|
|
3
|
+
## Priority
|
|
4
|
+
P0
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
`Stitcher::CbdtPropagator#propagate_tables_into` (`lib/fontisan/stitcher/cbdt_propagator.rb:147`) copies CBDT/CBLC bytes raw from the CBDT source into the compiled font. CBLC indexes glyphs by **source GID**. But the Stitcher's compile order is:
|
|
9
|
+
|
|
10
|
+
1. `inject_notdef` → compiled GID 0
|
|
11
|
+
2. `copy_outlines` → compiled GIDs 1..N (outline donor glyphs)
|
|
12
|
+
3. `add_placeholder_glyphs` → compiled GIDs N+1..M (CBDT placeholders, often renamed to `"gid{N}.1"` to avoid name collisions per PR #108)
|
|
13
|
+
|
|
14
|
+
So the CBDT source's GID 110 may end up at compiled GID 4027+ — but the raw-bytes CBLC still says "GID 110 → bitmap for source GID 110". The bitmap data dangles, pointing at whatever outline glyph happens to be at compiled GID 110.
|
|
15
|
+
|
|
16
|
+
This is masked in the cited Essenfont use case because FSung-3 (Ext G outlines) and NotoColorEmoji (emoji bitmaps) cover disjoint codepoint ranges. But for any stitch where CBDT and outline donors cover overlapping codepoint ranges (e.g. assembling a combined Latin+emoji font from a single source that has both glyf and CBDT), the bitmap-to-glyph mapping is wrong.
|
|
17
|
+
|
|
18
|
+
Documented as a known limitation in `docs/STITCHER_GUIDE.adoc` (added in PR #110).
|
|
19
|
+
|
|
20
|
+
## Goal
|
|
21
|
+
|
|
22
|
+
After compile, `propagate_tables_into` rebuilds the CBLC by:
|
|
23
|
+
1. Walking the compiled font's cmap to find each compiled GID for every CBDT-covered codepoint.
|
|
24
|
+
2. Mapping source GID → compiled GID via the Stitcher's `GlyphMapping`.
|
|
25
|
+
3. Building a new CBLC whose `BitmapSize.startGlyphIndex`/`endGlyphIndex` and IndexSubTableArray entries reference compiled GIDs.
|
|
26
|
+
4. Building a new CBDT that lays out bitmap blocks in compiled-GID order so each block matches its CBLC offset.
|
|
27
|
+
|
|
28
|
+
The result: any stitch — overlapping codepoint ranges or not — produces a font where CBLC's GID references line up with the compiled GIDs that actually have the right bitmaps.
|
|
29
|
+
|
|
30
|
+
## Approach
|
|
31
|
+
|
|
32
|
+
Reuse the CBDT/CBLC BinData models from PR #106:
|
|
33
|
+
- `Tables::Cblc`, `Tables::CblcBitmapSize`, `Tables::CblcIndexSubTableArrayEntry`, `Tables::CblcIndexSubTable`
|
|
34
|
+
- `Subset::TableStrategy::ColorBitmapSubsetter` and its plan classes — already do the offset-remap math for the subsetter; same algorithm shape, different mapping source
|
|
35
|
+
|
|
36
|
+
New collaborator: `Stitcher::CbdtReindexer`. Sits between compile and table propagation:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
compile TTF
|
|
40
|
+
↓
|
|
41
|
+
CbdtReindexer.reindex(source_cblc, source_cbdt, glyph_mapping) → new (CBLC, CBDT) bytes
|
|
42
|
+
↓
|
|
43
|
+
FontWriter.write_to_file(tables.merge("CBLC" => new_cblc, "CBDT" => new_cbdt))
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The reindexer is stateless given inputs — testable in isolation.
|
|
47
|
+
|
|
48
|
+
Update `propagate_tables_into` to call the reindexer instead of raw-bytes copy.
|
|
49
|
+
|
|
50
|
+
Update `docs/STITCHER_GUIDE.adoc` to remove the "known limitation" note.
|
|
51
|
+
|
|
52
|
+
## Out of scope
|
|
53
|
+
|
|
54
|
+
- Handling IndexSubTable format 4 (bit-packed offsets) — already unsupported in the subsetter; same gap.
|
|
55
|
+
- Multi-strike CBLC where each strike has a different glyph range — covered (reindexer iterates strikes).
|
|
56
|
+
- Subsetting the source CBDT before stitching — separate concern (the subsetter already does this for `Subset::Builder`).
|
|
57
|
+
|
|
58
|
+
## Effort
|
|
59
|
+
|
|
60
|
+
~1 day.
|
|
61
|
+
|
|
62
|
+
## Dependencies
|
|
63
|
+
|
|
64
|
+
None. Unblocks TODO 02.
|
|
65
|
+
|
|
66
|
+
## Acceptance criteria
|
|
67
|
+
|
|
68
|
+
- New spec `spec/fontisan/stitcher/cbdt_reindexer_spec.rb` covers:
|
|
69
|
+
- Disjoint-range stitch: CBLC's compiled GID references line up.
|
|
70
|
+
- Overlapping-range stitch: CBDT placeholder at compiled GID N gets the bitmap originally at source GID M (mapping via GlyphMapping).
|
|
71
|
+
- Multi-strike CBLC: every strike's bitmap data lands at the right compiled GID.
|
|
72
|
+
- Existing `spec/fontisan/stitcher/outline_priority_spec.rb` collection-mode test (currently skipped) is unblocked and passes.
|
|
73
|
+
- The "GID-stability limitation" section in `docs/STITCHER_GUIDE.adoc` is removed.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# 02 — Collection-mode outline-priority regression
|
|
2
|
+
|
|
3
|
+
## Priority
|
|
4
|
+
P0
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
`spec/fontisan/stitcher/outline_priority_spec.rb:160` ("write_collection preserves outline-first cmap priority across faces") was unblocked for single-face mode in PR #108 but the **collection variant** is still skipped:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
skip "Stitcher currently raises on a CBDT source without at least one outline codepoint in each face — " \
|
|
12
|
+
"collection-mode coverage needs CbdtPropagator hardening tracked separately."
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
When the Stitcher builds a TTC/OTC, each face compiles independently. The CBDT source is global — its placeholders need to land in EVERY face that covers any of its codepoints. The current `CbdtPropagator#add_placeholder_glyphs` raises if a face has no outline glyph from any other donor (the CBDT source alone can't seed a face).
|
|
16
|
+
|
|
17
|
+
## Goal
|
|
18
|
+
|
|
19
|
+
The skipped test passes. A CBDT source can be stitched into a collection where some faces cover only CBDT codepoints (no outline donor coverage). Each face gets the CBDT/CBLC tables propagated.
|
|
20
|
+
|
|
21
|
+
## Approach
|
|
22
|
+
|
|
23
|
+
Two pieces:
|
|
24
|
+
|
|
25
|
+
1. **`CbdtPropagator#add_placeholder_glyphs`** — relax the "must have an outline codepoint" precondition. Allow a face to be seeded with just CBDT placeholders (plus the `.notdef` that's always required).
|
|
26
|
+
|
|
27
|
+
2. **`CbdtPropagator#propagate_tables_into`** — for collections, run per-face rather than once. The collection writer already iterates faces; this is a matter of plumbing.
|
|
28
|
+
|
|
29
|
+
This depends on TODO 01 (GID-stable propagation) because the collection case multiplies the GID-mapping complexity — each face has its own compiled GID space.
|
|
30
|
+
|
|
31
|
+
## Out of scope
|
|
32
|
+
|
|
33
|
+
- Multi-CBDT-source support (the `MultipleCbdtSourcesError` guard remains).
|
|
34
|
+
- Cross-face glyph deduplication (already handled by the existing dedup pass).
|
|
35
|
+
|
|
36
|
+
## Effort
|
|
37
|
+
|
|
38
|
+
~2 hours once TODO 01 lands.
|
|
39
|
+
|
|
40
|
+
## Dependencies
|
|
41
|
+
|
|
42
|
+
- TODO 01 (CBDT/CBLC GID-stable propagation).
|
|
43
|
+
|
|
44
|
+
## Acceptance criteria
|
|
45
|
+
|
|
46
|
+
- The skipped test at `outline_priority_spec.rb:160` is unskipped and passes.
|
|
47
|
+
- A new test covers the "CBDT-only face" case (a face with no outline donor coverage, only CBDT placeholders).
|
|
48
|
+
- `MultipleCbdtSourcesError` is still raised when two CBDT sources are declared.
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# 03 — `fontisan audit` command (identity+style+features lens)
|
|
2
|
+
|
|
3
|
+
## Priority
|
|
4
|
+
P1
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
The `fontist-archive-private` pipeline currently reaches into Fontisan internals to extract font identity + metadata:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
# Current (wrong) approach — manual cmap + table poking
|
|
12
|
+
font = Fontisan::FontLoader.load(font_path)
|
|
13
|
+
cmap = font.table("cmap")
|
|
14
|
+
codepoints = cmap ? cmap.unicode_mappings.keys : []
|
|
15
|
+
# ...plus ad-hoc reads of name, OS/2, head, fvar, GSUB, GPOS
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Fragile and bypasses Fontisan's API. Fontisan has `InfoCommand` (font metadata) and `UnicodeCommand` (codepoint/glyph mappings), but neither produces a complete, self-describing audit record suitable for archival use.
|
|
19
|
+
|
|
20
|
+
## Goal
|
|
21
|
+
|
|
22
|
+
A `fontisan audit` command that produces a structured **font audit report** (YAML or JSON) covering what ucode's audit does NOT:
|
|
23
|
+
|
|
24
|
+
- **Identity** — name-table strings (family, full, PostScript, version)
|
|
25
|
+
- **Style** — OS/2 weight/width classes, head macStyle, fsSelection italic/bold, panose, fvar axes
|
|
26
|
+
- **Coverage facts** — total codepoints, total glyphs, cmap subtable provenance, optional codepoint list
|
|
27
|
+
- **OpenType layout** — GSUB/GPOS scripts (distinct from Unicode scripts), features list
|
|
28
|
+
- **Provenance** — `generated_at`, `fontisan_version`, `source_sha256`
|
|
29
|
+
|
|
30
|
+
## MECE split with ucode
|
|
31
|
+
|
|
32
|
+
ucode owns the Unicode-coverage axis (UCD parsing, block/script aggregation, per-codepoint glyph extraction). fontisan owns the font-identity axis.
|
|
33
|
+
|
|
34
|
+
| Concern | Owner |
|
|
35
|
+
|---------|-------|
|
|
36
|
+
| UCD fetching + parsing | ucode |
|
|
37
|
+
| Block/script aggregation from codepoints | ucode |
|
|
38
|
+
| Per-codepoint SVG extraction | ucode |
|
|
39
|
+
| HTML browsers | ucode |
|
|
40
|
+
| Font identity (name table) | **fontisan audit** |
|
|
41
|
+
| Style metadata (OS/2, head, fvar) | **fontisan audit** |
|
|
42
|
+
| OpenType scripts + features | **fontisan audit** |
|
|
43
|
+
| Source provenance (sha256, fontisan_version) | **fontisan audit** |
|
|
44
|
+
| cmap subtable provenance | **fontisan audit** |
|
|
45
|
+
|
|
46
|
+
**The fontisan audit does NOT bundle UCDXML parsing.** Block aggregation is delegated: fontisan audit accepts an optional pre-built ucode index path, OR emits the codepoint list and lets the consumer (fontist-archive) run ucode separately.
|
|
47
|
+
|
|
48
|
+
## Beyond ucode: fontisan as the pure-Ruby replacement for ALL font validators
|
|
49
|
+
|
|
50
|
+
**Positioning correction (after self-debate):** fontisan's stated mission is *"replaces two tools: otfinfo and extract_ttc"*. The natural extension: `fontisan audit` should replace **every** non-Ruby font validator — `fontbakery`, `ots-sanitize`, MS Font Validator, Apple `ftxvalidator`, `ttfautohint`'s verify mode. Each of these is a non-Ruby dependency that fontisan can eliminate for any Ruby-based font workflow.
|
|
51
|
+
|
|
52
|
+
The first-draft of this TODO deferred to those tools ("fontbakery owns this; wrap it"). That was wrong — fontbakery being mature is exactly the reason fontisan should match it: eliminating Python from font toolchains is the value proposition. fontTools tracks the OT spec; fontisan tracks it the same way. The "always behind Chrome" concern is a maintenance discipline, not a reason not to build it.
|
|
53
|
+
|
|
54
|
+
### The full audit axis inventory
|
|
55
|
+
|
|
56
|
+
Each axis below is currently owned by a non-Ruby tool. fontisan's audit command should provide a pure-Ruby equivalent.
|
|
57
|
+
|
|
58
|
+
| Axis | Replaces | Why fontisan |
|
|
59
|
+
|------|----------|--------------|
|
|
60
|
+
| Identity + style + features | `otfinfo` (LCDF) | Already replaced; `audit` is the structured-report sibling of `info` |
|
|
61
|
+
| Subset report | (no current tool) | fontisan-unique — leverages subset infrastructure |
|
|
62
|
+
| Cross-format equivalence | `ttx` diff (fontTools) | Less noisy; first-class "is this the same font?" |
|
|
63
|
+
| License/rights extraction | (manual OS/2 reading) | One-liner compliance check |
|
|
64
|
+
| Collection integrity | (no current tool for TTC) | TTC is undertooled; CJK/Apple narrow but real |
|
|
65
|
+
| **OTS-rejection predictor** | **`ots-sanitize` (C++)** | Pure-Ruby; no system dependency; cross-platform |
|
|
66
|
+
| **Variable-font readiness** | **`fontbakery` (Python)** | Pure-Ruby replacement; eliminate Python from font toolchains |
|
|
67
|
+
| **Hinting audit** | **`ftxvalidator` + FontLab** | Pure-Ruby; covers Windows/embedded use cases |
|
|
68
|
+
| **Format-round-trip report** | **`ttx` diff** | Verifies fontisan's own conversions + cross-tool comparison |
|
|
69
|
+
| **WOFF2 spec validation** | (informal) | fontisan has the encoder; expose validation |
|
|
70
|
+
| **Glyph name validation** | (informal) | OT spec regex/length/reserved-name rules |
|
|
71
|
+
| **cmap subtable validation** | (informal) | Format 4 segment mismatches, format 12 group ordering — common bugs |
|
|
72
|
+
| **Table directory validation** | (informal) | Checksum, alignment, offset arithmetic |
|
|
73
|
+
| **OpenType conformance** | **`fontbakery` + MS Font Validator** | Hundreds of OT spec "should"/"must" rules |
|
|
74
|
+
|
|
75
|
+
### Build order (priority)
|
|
76
|
+
|
|
77
|
+
Phase 1 (baseline + immediate-value workflow reports):
|
|
78
|
+
1. Identity + style + features (archival format)
|
|
79
|
+
2. Subset report
|
|
80
|
+
3. Cross-format equivalence
|
|
81
|
+
4. License/rights extraction
|
|
82
|
+
|
|
83
|
+
Phase 2 (replace the validators — incremental):
|
|
84
|
+
5. Table directory validation (checksums, alignment — easy, foundational)
|
|
85
|
+
6. cmap subtable validation (common real-world bugs)
|
|
86
|
+
7. OTS-rejection predictor (port `ots-sanitize` rule set)
|
|
87
|
+
8. Glyph name validation (cheap OT spec rules)
|
|
88
|
+
|
|
89
|
+
Phase 3 (broader scope):
|
|
90
|
+
9. Variable-font readiness (port fontbakery's GF-VF check subset)
|
|
91
|
+
10. Hinting audit
|
|
92
|
+
11. WOFF2 spec validation
|
|
93
|
+
12. Format-round-trip report
|
|
94
|
+
13. Collection integrity
|
|
95
|
+
14. Full OpenType conformance (long-running — tracks OT spec evolution)
|
|
96
|
+
|
|
97
|
+
### Cross-language integration policy
|
|
98
|
+
|
|
99
|
+
When fontisan reaches parity with fontbakery/ots-sanitize on a given check, it's the consumer's choice: use the pure-Ruby implementation (zero system deps) OR shell out to the established tool (mature, may have checks fontisan hasn't ported yet). fontisan shouldn't *force* the replacement — it should *earn* it by being correct and cross-platform.
|
|
100
|
+
|
|
101
|
+
## Approach
|
|
102
|
+
|
|
103
|
+
### Models (`lib/fontisan/models/audit/`)
|
|
104
|
+
|
|
105
|
+
Use `lutaml-model` like all other fontisan models:
|
|
106
|
+
|
|
107
|
+
- `AuditReport` — top-level report, attributes per the schema below
|
|
108
|
+
- `AuditAxis` — variable-font axis descriptor
|
|
109
|
+
- `AuditFeature` — feature tag + scripts list
|
|
110
|
+
|
|
111
|
+
### Command (`lib/fontisan/commands/audit_command.rb`)
|
|
112
|
+
|
|
113
|
+
`Commands::AuditCommand` orchestrates — does NOT re-parse tables. Delegates to existing commands:
|
|
114
|
+
|
|
115
|
+
- `InfoCommand` — identity + name-table strings
|
|
116
|
+
- `UnicodeCommand` — codepoint list + cmap subtable provenance
|
|
117
|
+
- `ScriptsCommand` — `opentype_scripts`
|
|
118
|
+
- `FeaturesCommand` — `features`
|
|
119
|
+
- `FormatDetector` — `source_format`
|
|
120
|
+
|
|
121
|
+
This makes `AuditCommand` a thin orchestration layer.
|
|
122
|
+
|
|
123
|
+
### CLI (`lib/fontisan/cli.rb`)
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# Standalone
|
|
127
|
+
fontisan audit Inter-Regular.ttf # → stdout (YAML)
|
|
128
|
+
fontisan audit Inter-Regular.ttf -o report.yaml # → file
|
|
129
|
+
fontisan audit Inter-Regular.ttf --format json # → stdout (JSON)
|
|
130
|
+
|
|
131
|
+
# Collection
|
|
132
|
+
fontisan audit Inter.ttc -o reports/ # → reports/Inter.ttc/{00..08}-*.yaml
|
|
133
|
+
|
|
134
|
+
# Single face from collection
|
|
135
|
+
fontisan audit Inter.ttc --font-index 6 -o Inter-Bold.yaml
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Output schema (one face per file)
|
|
139
|
+
|
|
140
|
+
```yaml
|
|
141
|
+
generated_at: 2026-07-10T12:30:00Z
|
|
142
|
+
fontisan_version: 0.4.24
|
|
143
|
+
source_file: Inter-Regular.ttf
|
|
144
|
+
source_sha256: 3b1a...
|
|
145
|
+
source_format: ttf
|
|
146
|
+
|
|
147
|
+
font_index: null
|
|
148
|
+
num_fonts_in_source: 1
|
|
149
|
+
|
|
150
|
+
# Identity
|
|
151
|
+
family_name: Inter
|
|
152
|
+
subfamily_name: Regular
|
|
153
|
+
full_name: Inter Regular
|
|
154
|
+
postscript_name: Inter-Regular
|
|
155
|
+
version: Version 4.000;git-a52131595
|
|
156
|
+
font_revision: 4.0
|
|
157
|
+
|
|
158
|
+
# Style
|
|
159
|
+
weight_class: 400
|
|
160
|
+
width_class: 5
|
|
161
|
+
italic: false
|
|
162
|
+
bold: false
|
|
163
|
+
panose: "2 0 5 3 0 0 0 0 0 0"
|
|
164
|
+
is_variable: false
|
|
165
|
+
axes: []
|
|
166
|
+
|
|
167
|
+
# Coverage
|
|
168
|
+
total_codepoints: 2857
|
|
169
|
+
total_glyphs: 1486
|
|
170
|
+
cmap_subtables: [4, 12, 14]
|
|
171
|
+
codepoints: [U+0000, U+0020, ...] # omitted with --no-codepoints
|
|
172
|
+
|
|
173
|
+
# OpenType layout
|
|
174
|
+
opentype_scripts: [latn, cyrl]
|
|
175
|
+
features: [kern, liga, calt]
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Collection layout
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
reports/
|
|
182
|
+
Inter.ttc/
|
|
183
|
+
00-Inter-Regular.yaml
|
|
184
|
+
01-Inter-Italic.yaml
|
|
185
|
+
...
|
|
186
|
+
08-Inter-Black.yaml
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Filename pattern: `{font_index:02d}-{postscript_name}.yaml`. Index prefix guarantees face-order sort and disambiguates broken fonts where two faces share a PostScript name.
|
|
190
|
+
|
|
191
|
+
## Out of scope
|
|
192
|
+
|
|
193
|
+
- UCDXML parsing (ucode's domain)
|
|
194
|
+
- Block/script aggregation (consumer runs ucode separately, OR a future `--with-ucode` flag calls ucode as a subprocess)
|
|
195
|
+
- HTML browsers (ucode's domain)
|
|
196
|
+
- Per-codepoint glyph extraction (ucode's 4-tier resolver)
|
|
197
|
+
- Reading formula YAML — this command reports what the FONT FILE declares
|
|
198
|
+
- Generating WOFF specimens (ConvertCommand's domain)
|
|
199
|
+
|
|
200
|
+
## Effort
|
|
201
|
+
|
|
202
|
+
~1-2 days.
|
|
203
|
+
|
|
204
|
+
## Dependencies
|
|
205
|
+
|
|
206
|
+
None directly. Existing commands (`InfoCommand`, `UnicodeCommand`, `ScriptsCommand`, `FeaturesCommand`) provide the data; `AuditCommand` just orchestrates.
|
|
207
|
+
|
|
208
|
+
## Acceptance criteria
|
|
209
|
+
|
|
210
|
+
- New spec `spec/fontisan/commands/audit_command_spec.rb` covers:
|
|
211
|
+
- Standalone TTF → YAML + JSON output
|
|
212
|
+
- Collection TTC → directory of N face reports
|
|
213
|
+
- `--font-index N` → single-face report
|
|
214
|
+
- `--no-codepoints` omits the codepoint list
|
|
215
|
+
- Source sha256 matches `Digest::SHA256.file(path)`
|
|
216
|
+
- Variable font with fvar → axes array populated
|
|
217
|
+
- New spec `spec/fontisan/models/audit_report_spec.rb` covers the model's serialization.
|
|
218
|
+
- CLI smoke test verifies `fontisan audit path/to/font.ttf` produces valid YAML on stdout.
|
|
219
|
+
- README/docs updated.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# 04 — UFO composite glyph encoding
|
|
2
|
+
|
|
3
|
+
## Priority
|
|
4
|
+
P1
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
`Ufo::Compile::GlyfLoca` (`lib/fontisan/ufo/compile/glyf_loca.rb:137`) emits simple glyphs only. UFO sources with composite glyphs (e.g., é defined as a component reference to e + acute) lose their component structure on compile — the compiled glyf either drops them or depends on `flatten_components` filter pre-processing.
|
|
9
|
+
|
|
10
|
+
## Goal
|
|
11
|
+
|
|
12
|
+
`GlyfLoca` encodes composite glyphs natively per OpenType spec section 5.6. A UFO `<component>` element maps to a glyf component record referencing the target glyph by index, preserving the original transform.
|
|
13
|
+
|
|
14
|
+
## Approach
|
|
15
|
+
|
|
16
|
+
Add a `CompositeEncoder` collaborator in `lib/fontisan/ufo/compile/glyf_loca/composite_encoder.rb` that takes a UFO `<component>` plus a GID-resolver (component name → compiled GID) and emits the binary component record per OT spec:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
uint16 flags
|
|
20
|
+
uint16 glyphIndex
|
|
21
|
+
(uint8|uint8|uint8|uint8 | int8|int8|int8|int8 | uint8) args # depends on flags bits 0/1
|
|
22
|
+
(F2Dot14 × 4 | F2Dot14 × 2 | F2Dot14) transformation # depends on flags bits 7/6/3
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`GlyfLoca` invokes the encoder when a UFO glyph has components (no contours, OR contours + components — latter is rare but spec-legal).
|
|
26
|
+
|
|
27
|
+
The existing `flatten_components` filter remains for callers who want pre-flattening (e.g., for fonts that target renderers without component support).
|
|
28
|
+
|
|
29
|
+
## Out of scope
|
|
30
|
+
|
|
31
|
+
- Variable-font composite variation deltas (`gvar`) — separate concern, TODO 06's blast radius.
|
|
32
|
+
- Cycling component references (A → B → A) — these need pre-validation, tracked separately.
|
|
33
|
+
|
|
34
|
+
## Effort
|
|
35
|
+
|
|
36
|
+
~1 day.
|
|
37
|
+
|
|
38
|
+
## Dependencies
|
|
39
|
+
|
|
40
|
+
None.
|
|
41
|
+
|
|
42
|
+
## Acceptance criteria
|
|
43
|
+
|
|
44
|
+
- New spec covers encoding of all 8 component arg/transform combinations (flags bit patterns).
|
|
45
|
+
- Round-trip spec: UFO with composite → compile → re-read via `FontLoader` → components match original.
|
|
46
|
+
- The `flatten_components` filter still works as a pre-pass for callers that want flattened output.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# 05 — OTF compiler real CFF charstrings
|
|
2
|
+
|
|
3
|
+
## Priority
|
|
4
|
+
P1
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
`Ufo::Compile::OtfCompiler` (`lib/fontisan/ufo/compile/otf_compiler.rb:9`) currently emits a placeholder CFF table — no real charstrings. Any OTF output is incomplete:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
# TODO.full/10: this currently emits a placeholder CFF table
|
|
12
|
+
# real charstrings. Full CFF construction lands when TODO 10
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The font loads but renders nothing useful. Production OTF output is blocked.
|
|
16
|
+
|
|
17
|
+
## Goal
|
|
18
|
+
|
|
19
|
+
`OtfCompiler` emits a complete CFF table with:
|
|
20
|
+
- Real Type 2 charstrings for every glyph (converted from UFO contours)
|
|
21
|
+
- Valid Top DICT (font matrix, charset, encoding, CharStrings INDEX offset)
|
|
22
|
+
- Valid Private DICT (default width, nominal width, SubrZERO subroutines if any)
|
|
23
|
+
- Correct name INDEX, charset, FDSelect/FDArray (for CID-keyed fonts)
|
|
24
|
+
|
|
25
|
+
Round-trip: UFO → compile → re-read via `FontLoader` → contours match.
|
|
26
|
+
|
|
27
|
+
## Approach
|
|
28
|
+
|
|
29
|
+
Reuse the Type 2 charstring conversion logic from `lib/fontisan/type1/charstring_converter.rb` — UFO contours → moveto/lineto/curveto closepath is structurally identical to Type 1 → Type 2 charstring conversion.
|
|
30
|
+
|
|
31
|
+
Add a new `CffBuilder` collaborator under `lib/fontisan/ufo/compile/otf_compiler/cff_builder.rb` that owns:
|
|
32
|
+
|
|
33
|
+
1. **Top DICT construction** — font matrix, ROS (if CID), charstrings offset.
|
|
34
|
+
2. **CharString INDEX** — per-glyph Type 2 charstring bytes.
|
|
35
|
+
3. **Charset** — glyph name → GID map (uses post.names if available; else `gid{N}`).
|
|
36
|
+
4. **Private DICT** — default width, nominal width, hint-mask operators (no-op when source has no hints).
|
|
37
|
+
5. **Global / local subroutines** — empty INDEXs initially; TODO 06 wires hint subroutines.
|
|
38
|
+
|
|
39
|
+
The existing `Tables::Cff` BinData record already declares the on-disk structure; the builder produces the values to populate it.
|
|
40
|
+
|
|
41
|
+
## Out of scope
|
|
42
|
+
|
|
43
|
+
- CFF2 (variable fonts) — TODO 06.
|
|
44
|
+
- Subroutine packing / optimization — `Optimizers::SubroutineOptimizer` already exists for Type 1; CFF equivalent is a separate task.
|
|
45
|
+
- CID-keyed fonts with multi-FD — single FD only initially.
|
|
46
|
+
|
|
47
|
+
## Effort
|
|
48
|
+
|
|
49
|
+
~2-3 days.
|
|
50
|
+
|
|
51
|
+
## Dependencies
|
|
52
|
+
|
|
53
|
+
None (the placeholder CFF was the only thing blocking, and we're replacing it).
|
|
54
|
+
|
|
55
|
+
## Acceptance criteria
|
|
56
|
+
|
|
57
|
+
- New spec `spec/fontisan/ufo/compile/otf_compiler/cff_builder_spec.rb` covers:
|
|
58
|
+
- Single-glyph font (`.notdef` + one outline) → valid CFF
|
|
59
|
+
- Multi-glyph font with quadratic curves → charstrings use rcurveto correctly
|
|
60
|
+
- Multi-glyph font with TrueType cubic curves → curve conversion to Type 2 cubic
|
|
61
|
+
- Charset ordering matches maxp.numGlyphs
|
|
62
|
+
- Round-trip: UFO → OTF → re-read → contours match (within Type 2 quantization tolerance)
|
|
63
|
+
- `fontisan convert font.ufo --to otf` produces a font that loads in fontTools and Chrome.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# 06 — CFF2 blend/vsindex operators
|
|
2
|
+
|
|
3
|
+
## Priority
|
|
4
|
+
P2
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
`Ufo::Compile::VariableOtf` (`lib/fontisan/ufo/compile/variable_otf.rb:12`) emits static CFF2 only:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
- CFF2 outlines (static — blend/vsindex operators are TODO 07/18)
|
|
12
|
+
TODO 07 (blend/vsindex) and TODO 18 (blend integration) to be
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Variable CFF2 fonts (e.g., variable OTF output from a UFO with `fvar` + `gvar`-equivalent masters) can't round-trip. The compiled font is static — variation deltas are dropped.
|
|
16
|
+
|
|
17
|
+
## Goal
|
|
18
|
+
|
|
19
|
+
`VariableOtf` emits CFF2 with `blend` and `vsindex` operators per CFF2 spec section 3.A.4. A variable UFO source compiles to a variable OTF that renders correctly at any chosen instance along each axis.
|
|
20
|
+
|
|
21
|
+
## Approach
|
|
22
|
+
|
|
23
|
+
Two pieces:
|
|
24
|
+
|
|
25
|
+
1. **CharString encoder** — extend the Type 2 charstring builder (from TODO 05) to emit `blend` after each variation-aware value. The master value is the default; deltas for each region are pushed on the stack then summed via `blend`.
|
|
26
|
+
|
|
27
|
+
2. **ItemVariationData emission** — translate the UFO master model (`gvar`-equivalent: per-glyph deltas keyed by region) into CFF2's `ItemVariationData` and `ItemVariationStore` structures. Reuse `Ufo::Compile::ItemVariationStore` which already handles this for the variable-TTF path.
|
|
28
|
+
|
|
29
|
+
3. **Region detection** — pre-pass over `fvar` axes + master locations to compute the set of variation regions. Same algorithm as `Ufo::Compile::Fvar`.
|
|
30
|
+
|
|
31
|
+
## Out of scope
|
|
32
|
+
|
|
33
|
+
- HVAR/VVAR/MVAR tables for variable OTF — those are outside CFF2 but already partially supported; track separately if needed.
|
|
34
|
+
- Backwards-compat with non-variable CFF fonts — separate concern.
|
|
35
|
+
- Subroutine packing of variation-aware charstrings — TODO follow-up.
|
|
36
|
+
|
|
37
|
+
## Effort
|
|
38
|
+
|
|
39
|
+
~3-5 days. CFF2 charstring semantics are intricate.
|
|
40
|
+
|
|
41
|
+
## Dependencies
|
|
42
|
+
|
|
43
|
+
- TODO 05 (OTF compiler real CFF) — must land first; the CFF2 builder extends the CFF builder.
|
|
44
|
+
|
|
45
|
+
## Acceptance criteria
|
|
46
|
+
|
|
47
|
+
- New spec covers:
|
|
48
|
+
- Single-axis variable font → CFF2 with correct `blend` operators + `ItemVariationStore`
|
|
49
|
+
- Multi-axis variable font → region table correct, no cross-axis contamination
|
|
50
|
+
- Instance generation via `Variation::InstanceGenerator` produces output identical to direct master compile (within tolerance)
|
|
51
|
+
- Round-trip: UFO with fvar + masters → variable OTF → re-read → `fvar` axes match, instances render correctly.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# 07 — CPAL v1 header fields
|
|
2
|
+
|
|
3
|
+
## Priority
|
|
4
|
+
P2
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
`Tables::Cpal` (`lib/fontisan/tables/cpal.rb:106,184`) only parses CPAL v0 fields. CPAL v1 adds:
|
|
9
|
+
|
|
10
|
+
- `numPaletteEntryLabels` (uint16) — count of palette entry labels
|
|
11
|
+
- `numPaletteLabels` (uint16) — count of palette labels
|
|
12
|
+
- `paletteEntryLabelsOffset` (uint32) — offset to label records
|
|
13
|
+
- `paletteLabelsOffset` (uint32) — offset to label records
|
|
14
|
+
|
|
15
|
+
These attach human-readable names (via the `name` table) to color palettes and entries. Fonts with CPAL v1 lose this metadata on parse.
|
|
16
|
+
|
|
17
|
+
## Goal
|
|
18
|
+
|
|
19
|
+
`Tables::Cpal` parses v1 header fields and exposes:
|
|
20
|
+
|
|
21
|
+
- `palette_labels` — array of name-record IDs, one per palette
|
|
22
|
+
- `palette_entry_labels` — array of name-record IDs, one per palette entry index
|
|
23
|
+
|
|
24
|
+
Round-trip preserves the labels.
|
|
25
|
+
|
|
26
|
+
## Approach
|
|
27
|
+
|
|
28
|
+
Extend the BinData record in `lib/fontisan/tables/cpal.rb`:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
class Cpal < Binary::BaseRecord
|
|
32
|
+
uint16 :version
|
|
33
|
+
uint16 :num_palette_entries
|
|
34
|
+
uint16 :num_palettes
|
|
35
|
+
uint16 :num_color_records
|
|
36
|
+
uint32 :color_records_offset
|
|
37
|
+
array :color_record_indices, type: :uint16, initial_length: :num_palettes
|
|
38
|
+
# v0 ends here
|
|
39
|
+
|
|
40
|
+
# v1 extension — only present when version >= 1
|
|
41
|
+
choice :v1_fields, onlyif: -> { version >= 1 } do
|
|
42
|
+
uint16 :num_palette_entry_labels
|
|
43
|
+
uint16 :num_palette_labels
|
|
44
|
+
uint32 :palette_entry_labels_offset
|
|
45
|
+
uint32 :palette_labels_offset
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
After BinData parse, lazily walk the label offsets to populate `palette_labels` and `palette_entry_labels` arrays.
|
|
51
|
+
|
|
52
|
+
For emission: if v1 fields are set, include them; else emit v0.
|
|
53
|
+
|
|
54
|
+
## Out of scope
|
|
55
|
+
|
|
56
|
+
- CPAL v2 changes (none — v1 is the latest defined).
|
|
57
|
+
- Building new palettes from scratch — just parse + emit.
|
|
58
|
+
|
|
59
|
+
## Effort
|
|
60
|
+
|
|
61
|
+
~4 hours.
|
|
62
|
+
|
|
63
|
+
## Dependencies
|
|
64
|
+
|
|
65
|
+
None.
|
|
66
|
+
|
|
67
|
+
## Acceptance criteria
|
|
68
|
+
|
|
69
|
+
- New spec covers parse of a synthetic CPAL v1 table with both label arrays.
|
|
70
|
+
- Round-trip: v1 table → read → `to_binary_s` → identical bytes.
|
|
71
|
+
- v0 tables still parse and emit unchanged.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# 08 — CFF standard string table
|
|
2
|
+
|
|
3
|
+
## Priority
|
|
4
|
+
P2
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
`Tables::Cff` (`lib/fontisan/tables/cff.rb:419`) ships only a partial standard string table (the 391 predefined Type 1 / CFF strings per spec appendix A). Glyph names that should be in the standard set are duplicated in the custom strings index, wasting space and breaking round-trip fidelity with fontTools.
|
|
9
|
+
|
|
10
|
+
## Goal
|
|
11
|
+
|
|
12
|
+
Complete the 391-string standard table per CFF spec. `Cff` parses glyph names against the standard table; only non-standard names go in the custom strings index.
|
|
13
|
+
|
|
14
|
+
## Approach
|
|
15
|
+
|
|
16
|
+
Replace the partial list with the canonical 391 strings (spec appendix A, also in fontTools' `fontTools.cffLib.strings`).
|
|
17
|
+
|
|
18
|
+
Add a class method:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
class Cff < Binary::BaseRecord
|
|
22
|
+
STANDARD_STRINGS = [...].freeze # 391 entries
|
|
23
|
+
|
|
24
|
+
def self.standard_string?(name)
|
|
25
|
+
STANDARD_STRINGS.include?(name)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.standard_string_index(name)
|
|
29
|
+
STANDARD_STRINGS.index(name)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Update the CFF builder (TODO 05's `CffBuilder`) to skip custom-string encoding when a name is standard.
|
|
35
|
+
|
|
36
|
+
## Out of scope
|
|
37
|
+
|
|
38
|
+
- CID-keyed fonts (no name strings) — unaffected.
|
|
39
|
+
- Custom charstring-internal names (operator names) — those are operators, not string IDs.
|
|
40
|
+
|
|
41
|
+
## Effort
|
|
42
|
+
|
|
43
|
+
~2 hours.
|
|
44
|
+
|
|
45
|
+
## Dependencies
|
|
46
|
+
|
|
47
|
+
- TODO 05 (OTF compiler real CFF) benefits; doesn't strictly block.
|
|
48
|
+
|
|
49
|
+
## Acceptance criteria
|
|
50
|
+
|
|
51
|
+
- Spec covers: every standard name → correct index, every non-standard name → custom index.
|
|
52
|
+
- Round-trip: CFF with mixed standard/non-standard names → read → emit → identical bytes.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# 09 — Type 1 seac expansion
|
|
2
|
+
|
|
3
|
+
## Priority
|
|
4
|
+
P2
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
`Type1::CharStringConverter` (`lib/fontisan/type1/charstring_converter.rb:161`) and `Type1::TtfToType1Converter` (`lib/fontisan/type1/ttf_to_type1_converter.rb:256`) emit seac operators as-is when converting Type 1 → TrueType. seac (Standard Encoding Accented Character) composites two glyphs into one — e.g., é = e + acute at a fixed offset. Modern renderers deprecated seac; Type 1 → TTF conversion must expand it into actual outlines.
|
|
9
|
+
|
|
10
|
+
## Goal
|
|
11
|
+
|
|
12
|
+
`TtfToType1Converter` (and the reverse path) expands seac into merged outlines:
|
|
13
|
+
|
|
14
|
+
1. Resolve the two referenced glyphs (base + accent) via the source font.
|
|
15
|
+
2. Translate them to the seac offset.
|
|
16
|
+
3. Concatenate their contours into the parent glyph.
|
|
17
|
+
|
|
18
|
+
## Approach
|
|
19
|
+
|
|
20
|
+
Add a `SeacExpander` collaborator under `lib/fontisan/type1/seac_expander.rb` that takes:
|
|
21
|
+
|
|
22
|
+
- A charstring with a seac operator
|
|
23
|
+
- A resolver: glyph_name → charstring (for the base + accent)
|
|
24
|
+
|
|
25
|
+
Returns a new charstring with the seac replaced by the merged outline operators.
|
|
26
|
+
|
|
27
|
+
Walk the source's standard-encoding lookup (Adobe Standard Encoding) to map seac's two uint8 operands to glyph names. Decode the operands:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
seac: asb base accent
|
|
31
|
+
asb — sidebearing (unused in expansion)
|
|
32
|
+
base — Adobe Standard Encoding index of the base glyph
|
|
33
|
+
accent — Adobe Standard Encoding index of the accent glyph
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The expander recursively expands seac in base/accent glyphs (seac can nest).
|
|
37
|
+
|
|
38
|
+
## Out of scope
|
|
39
|
+
|
|
40
|
+
- seac in CFF (not Type 1) — CFF deprecated seac entirely; should be a warning, not an expansion.
|
|
41
|
+
- Replacing the deprecated `seac` operator in TTF → Type 1 conversion — seac emission is already removed; we only need to handle source seac on input.
|
|
42
|
+
|
|
43
|
+
## Effort
|
|
44
|
+
|
|
45
|
+
~6 hours.
|
|
46
|
+
|
|
47
|
+
## Dependencies
|
|
48
|
+
|
|
49
|
+
None.
|
|
50
|
+
|
|
51
|
+
## Acceptance criteria
|
|
52
|
+
|
|
53
|
+
- Spec covers:
|
|
54
|
+
- Single-level seac (e.g., é) → expanded to merged outline.
|
|
55
|
+
- Nested seac (e.g., a glyph that references another seac glyph as its base) → fully expanded.
|
|
56
|
+
- seac referencing a missing glyph → raise `SeacReferenceError` with the missing name.
|
|
57
|
+
- Round-trip: Type 1 with seac → TTF → re-read → outline matches the merged shape.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# 10 — UFO image set + feature writers
|
|
2
|
+
|
|
3
|
+
## Priority
|
|
4
|
+
P2
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
Two UFO subsystems ship as stubs:
|
|
9
|
+
|
|
10
|
+
- `Ufo::ImageSet` (`lib/fontisan/ufo/image_set.rb:6`) — placeholder, "real implementation lands with TODO 02 (glyph model + images)"
|
|
11
|
+
- `Ufo::Features` (`lib/fontisan/ufo/features.rb:8`) — placeholder, "TODO 08 (feature writers)"
|
|
12
|
+
|
|
13
|
+
UFO sources with `features.fea` (OpenType layout compilation from text) or images (UFO 3 image data) lose both on round-trip.
|
|
14
|
+
|
|
15
|
+
## Goal
|
|
16
|
+
|
|
17
|
+
### ImageSet
|
|
18
|
+
- `Ufo::ImageSet` parses UFO 3 image data: reads `images/` directory, exposes `images` array of `Ufo::Image` records with filename + sha + PNG bytes.
|
|
19
|
+
- `Ufo::Glyph#image` (already modeled) reads image references; round-trips.
|
|
20
|
+
- Compile: glyphs with image references emit `CBDT`/`CBLC` color-bitmap entries (reusing the BinData models from PR #106).
|
|
21
|
+
|
|
22
|
+
### Features (feature writers)
|
|
23
|
+
- `Ufo::Features` parses `features.fea` via a new `FeatureCompiler` collaborator.
|
|
24
|
+
- Compiles OpenType layout rules (kern, liga, calt, etc.) into `GSUB`/`GPOS` tables.
|
|
25
|
+
- Output tables are mergeable into the compiled font.
|
|
26
|
+
|
|
27
|
+
## Approach
|
|
28
|
+
|
|
29
|
+
### ImageSet
|
|
30
|
+
Reuse existing `Tables::Cbdt`/`Cblc` BinData records. Convert each UFO image to a small-metrics-format PNG block. Build CBLC IndexSubTableArray pointing at each block.
|
|
31
|
+
|
|
32
|
+
For test fixtures: the existing `CbdtFixture` (PR #107) already builds valid CBDT/CBLC pairs — use as reference for the emission shape.
|
|
33
|
+
|
|
34
|
+
### Features
|
|
35
|
+
Use a fea-syntax parser. Options:
|
|
36
|
+
- **Inline parser** — non-trivial; ~5 days work, ~500 LOC.
|
|
37
|
+
- **Adapter for `fontTools.feaLib`** — only available with Python; introduces cross-language dep (violates "pure Ruby" rule).
|
|
38
|
+
- **Limited fea subset** — handle only kern/liga (most common); error on unsupported constructs.
|
|
39
|
+
|
|
40
|
+
Recommendation: **limited fea subset** initially, expanding as real-world fonts demand.
|
|
41
|
+
|
|
42
|
+
Add `FeatureCompiler` under `lib/fontisan/ufo/compile/feature_compiler.rb`. Parses `features.fea` into a tree, then emits GSUB/GPOS records. Starts with:
|
|
43
|
+
- `feature kern { pair ... } kern;` → GPOS PairPos (format 1)
|
|
44
|
+
- `feature liga { sub ... by ...; } liga;` → GSUB LigatureSubst
|
|
45
|
+
|
|
46
|
+
## Out of scope
|
|
47
|
+
|
|
48
|
+
- Adobe FEA parser completeness — full spec is 100+ pages; cover what real fonts use.
|
|
49
|
+
- AFDKO integration — out of scope.
|
|
50
|
+
|
|
51
|
+
## Effort
|
|
52
|
+
|
|
53
|
+
- ImageSet: ~1 day (BinData + IO).
|
|
54
|
+
- Features (limited subset): ~3 days (kern + liga paths).
|
|
55
|
+
- Features (full fea): weeks — explicitly out of scope.
|
|
56
|
+
|
|
57
|
+
## Dependencies
|
|
58
|
+
|
|
59
|
+
- TODO 05 (OTF compiler real CFF) — image glyphs end up in CBDT, unrelated to CFF. No dep.
|
|
60
|
+
- None blocking.
|
|
61
|
+
|
|
62
|
+
## Acceptance criteria
|
|
63
|
+
|
|
64
|
+
### ImageSet
|
|
65
|
+
- Spec covers read of UFO with one image → `glyph.image` populated, `imageset.images.first.bytes` matches PNG bytes.
|
|
66
|
+
- Compile round-trip: UFO with image glyph → TTF → re-read → CBDT/CBLC tables present, bitmap decodable.
|
|
67
|
+
|
|
68
|
+
### Features
|
|
69
|
+
- Spec covers a UFO with kern feature → GPOS table present with correct PairPos.
|
|
70
|
+
- Spec covers a UFO with liga feature → GSUB table present with correct LigatureSubst.
|
|
71
|
+
- Unsupported fea constructs raise `UnsupportedFeaError` with a clear message.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# 11 — kern groups.plist support
|
|
2
|
+
|
|
3
|
+
## Priority
|
|
4
|
+
P2
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
`Ufo::Compile::FeatureWriters::Kern` (`lib/fontisan/ufo/compile/feature_writers/kern.rb:18`) reads kerning pairs from `kerning.plist` but only partially supports `groups.plist`:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
# groups.plist data (TODO: groups.plist support is partial).
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
UFO sources that use class-based kerning (kerning classes defined in `groups.plist` instead of per-glyph) lose the class data. Compiled fonts have incomplete kern tables.
|
|
15
|
+
|
|
16
|
+
## Goal
|
|
17
|
+
|
|
18
|
+
`Kern` feature writer reads `groups.plist` class definitions, resolves them in `kerning.plist` lookups, and emits GPOS class-based PairPos records (format 2).
|
|
19
|
+
|
|
20
|
+
## Approach
|
|
21
|
+
|
|
22
|
+
Two pieces:
|
|
23
|
+
|
|
24
|
+
1. **Group resolution** — when `kerning.plist` references a group name (rather than a glyph name), expand to all group members. Track group → glyph-set mapping.
|
|
25
|
+
|
|
26
|
+
2. **Format-2 emission** — emit PairPos format 2 (class-based) when > N pairs share a class. Threshold: ~5 pairs (configurable). Below threshold, fall back to format 1 (pair-based) for size.
|
|
27
|
+
|
|
28
|
+
Add a `KerningClassResolver` collaborator under `lib/fontisan/ufo/compile/feature_writers/kern/class_resolver.rb`. Stateless given (kerning.plist, groups.plist); returns a list of resolved pairs (glyph_a, glyph_b, value).
|
|
29
|
+
|
|
30
|
+
## Out of scope
|
|
31
|
+
|
|
32
|
+
- Variable-font kerning (HVAR table) — separate concern.
|
|
33
|
+
- Contextual kerning (`kern` feature with context) — separate feature.
|
|
34
|
+
|
|
35
|
+
## Effort
|
|
36
|
+
|
|
37
|
+
~4 hours.
|
|
38
|
+
|
|
39
|
+
## Dependencies
|
|
40
|
+
|
|
41
|
+
None.
|
|
42
|
+
|
|
43
|
+
## Acceptance criteria
|
|
44
|
+
|
|
45
|
+
- Spec covers:
|
|
46
|
+
- Class-based kerning (groups.plist with two classes, kerning.plist with one pair between them).
|
|
47
|
+
- Mixed class + glyph kerning (one glyph + one class).
|
|
48
|
+
- Threshold: < 5 pairs → format 1; >= 5 pairs → format 2.
|
|
49
|
+
- Round-trip: UFO with class kerning → TTF → re-read → GPOS contains the right PairPos records.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# 12 — `cbdt_fixture.rb` full BinData conversion
|
|
2
|
+
|
|
3
|
+
## Priority
|
|
4
|
+
P3
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
`spec/support/cbdt_fixture.rb` was partially refactored in PR #107 (CBDT/CBLC use the new BinData models), but the remaining tables still hand-roll binary packing:
|
|
9
|
+
|
|
10
|
+
- `head_table` — 19-element `[...].pack("NNNNnnNNnnnnnnnnnn")`
|
|
11
|
+
- `hhea_table` — 16-element pack
|
|
12
|
+
- `os2_table` — 43-element pack
|
|
13
|
+
- `name_table` — manual record + offset arithmetic
|
|
14
|
+
- `post_table` — manual pack
|
|
15
|
+
- `hmtx_table` — `Array.new(num_glyphs) { [1000, 0] }.flatten.pack("n*")`
|
|
16
|
+
- `cmap_table` / `format4_subtable` / `format12_subtable` — full cmap builder, ~80 lines
|
|
17
|
+
|
|
18
|
+
~200 lines of pack/unpack that duplicate the layout knowledge in `Tables::*`. If any table layout changes, the fixture breaks silently — the same anti-pattern PR #107 already fixed for CBDT/CBLC.
|
|
19
|
+
|
|
20
|
+
## Goal
|
|
21
|
+
|
|
22
|
+
Every table builder in `cbdt_fixture.rb` constructs via BinData record (`Tables::Head`, `Tables::Hhea`, `Tables::Maxp`, `Tables::Os2`, `Tables::Name`, `Tables::Post`, `Tables::Hmtx`, `Tables::Cmap`). No more `[...].pack(...)` calls.
|
|
23
|
+
|
|
24
|
+
The fixture's self-test (`spec/support/cbdt_fixture_spec.rb`) catches any drift between the fixture's output and the BinData models.
|
|
25
|
+
|
|
26
|
+
## Approach
|
|
27
|
+
|
|
28
|
+
For each table, replace the hand-rolled pack with a `Tables::*` BinData construction:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# Before
|
|
32
|
+
def head_table
|
|
33
|
+
[0x00010000, 0x00005000, ...].pack("NNNNnnNNnnnnnnnnnn")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# After
|
|
37
|
+
def head_table
|
|
38
|
+
Tables::Head.new(
|
|
39
|
+
version_raw: 0x00010000,
|
|
40
|
+
font_revision: 0x00005000,
|
|
41
|
+
...
|
|
42
|
+
).to_binary_s
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For tables with field shapes that don't match the BinData record cleanly (e.g., `name_table`'s variable-length string storage), use the BinData record's nested array attributes.
|
|
47
|
+
|
|
48
|
+
For `cmap_table`: extract the format-4 / format-12 subtable builders to a reusable `Tables::Cmap.from_mappings(mappings)` class method. The same logic is duplicated in `Subset::TableStrategy::Cmap::Builder` and `Subset::TableStrategy::ColorBitmapSubsetter` — DRY win across three call sites.
|
|
49
|
+
|
|
50
|
+
## Out of scope
|
|
51
|
+
|
|
52
|
+
- Refactoring the BinData models themselves — only changes the fixture's consumers.
|
|
53
|
+
- Adding `Tables::Cmap.from_mappings` if the DRY extraction scope proves larger than the fixture alone (split into separate PR).
|
|
54
|
+
|
|
55
|
+
## Effort
|
|
56
|
+
|
|
57
|
+
~4 hours.
|
|
58
|
+
|
|
59
|
+
## Dependencies
|
|
60
|
+
|
|
61
|
+
None.
|
|
62
|
+
|
|
63
|
+
## Acceptance criteria
|
|
64
|
+
|
|
65
|
+
- `spec/support/cbdt_fixture.rb` contains zero `[...].pack` calls.
|
|
66
|
+
- `spec/support/cbdt_fixture_spec.rb` self-test still passes (it round-trips via the BinData models; the assertions don't change).
|
|
67
|
+
- All downstream specs that use `CbdtFixture` still pass.
|
|
68
|
+
- Bundle size of the fixture file drops from ~300 lines to ~150.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# 13 — Split `OctokitFetcher` out of `fixture_downloader.rb`
|
|
2
|
+
|
|
3
|
+
## Priority
|
|
4
|
+
P3
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
`spec/support/fixture_downloader.rb` (post-PR #111) is ~350 lines with two distinct concerns:
|
|
9
|
+
|
|
10
|
+
1. **Retry / routing loop** (the `Downloader` class) — backoff, Retry-After, permanent-vs-transient classification.
|
|
11
|
+
2. **Octokit URL dispatch** (the `OctokitFetcher` module) — release-asset, raw-file, contents-API routing.
|
|
12
|
+
|
|
13
|
+
These are MECE-distinct. Bundling them in one file muddies the boundary; future changes to Octokit routing shouldn't require reading the retry loop.
|
|
14
|
+
|
|
15
|
+
## Goal
|
|
16
|
+
|
|
17
|
+
`OctokitFetcher` moves to its own file `spec/support/fixture_downloader/octokit_fetcher.rb`. The `Downloader` class becomes the only public surface in `spec/support/fixture_downloader.rb` and delegates to `OctokitFetcher` when needed.
|
|
18
|
+
|
|
19
|
+
## Approach
|
|
20
|
+
|
|
21
|
+
1. Create `spec/support/fixture_downloader/` directory.
|
|
22
|
+
2. Move `OctokitFetcher` module to `spec/support/fixture_downloader/octokit_fetcher.rb`.
|
|
23
|
+
3. Move `HttpStatusIo` and `Downloader` constants stay in `spec/support/fixture_downloader.rb`.
|
|
24
|
+
4. Add a `module FixtureFonts::Downloader` autoload or simple `require_relative "fixture_downloader/octokit_fetcher"` from `fixture_downloader.rb`.
|
|
25
|
+
|
|
26
|
+
Actually — for spec/support files, `require_relative` is acceptable (CLAUDE.md rule on autoload is for `lib/`). No autoload needed; the Rakefile's `require_relative` chain handles loading.
|
|
27
|
+
|
|
28
|
+
## Out of scope
|
|
29
|
+
|
|
30
|
+
- Splitting the Downloader further — it's cohesive.
|
|
31
|
+
- Moving `fixture_downloader_spec.rb` — already sibling, fine where it is.
|
|
32
|
+
|
|
33
|
+
## Effort
|
|
34
|
+
|
|
35
|
+
~1 hour.
|
|
36
|
+
|
|
37
|
+
## Dependencies
|
|
38
|
+
|
|
39
|
+
None.
|
|
40
|
+
|
|
41
|
+
## Acceptance criteria
|
|
42
|
+
|
|
43
|
+
- `spec/support/fixture_downloader.rb` < 200 lines.
|
|
44
|
+
- `spec/support/fixture_downloader/octokit_fetcher.rb` exists and contains all Octokit-specific code.
|
|
45
|
+
- All existing specs pass unchanged.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# 14 — Rubocop baseline chip (per-namespace)
|
|
2
|
+
|
|
3
|
+
## Priority
|
|
4
|
+
P3
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
PR #111's `rubocop -A --auto-gen-config` baseline refresh tracked ~9600 remaining offenses. The `.rubocop_todo.yml` file is now ~800 lines of `Exclude:` lists. Layout/Style cops dominate (most are auto-correctable; the rest need manual fixes).
|
|
9
|
+
|
|
10
|
+
Leaving the baseline in this state makes future rubocop improvements invisible — new offenses are buried in baseline noise, and contributors can't see whether their PR is clean vs. baseline-burdened.
|
|
11
|
+
|
|
12
|
+
## Goal
|
|
13
|
+
|
|
14
|
+
Reduce baseline count to < 1000 offenses via per-namespace PRs:
|
|
15
|
+
|
|
16
|
+
| Namespace | Approximate count | Priority |
|
|
17
|
+
|-----------|-------------------|----------|
|
|
18
|
+
| `lib/fontisan/tables/` | ~3000 | High (well-understood) |
|
|
19
|
+
| `lib/fontisan/ufo/compile/` | ~2500 | High |
|
|
20
|
+
| `lib/fontisan/stitcher/` | ~800 | Medium |
|
|
21
|
+
| `lib/fontisan/converters/` | ~500 | Medium |
|
|
22
|
+
| `lib/fontisan/type1/` | ~400 | Medium |
|
|
23
|
+
| `lib/fontisan/collection/` | ~300 | Medium |
|
|
24
|
+
| `lib/fontisan/subset/` | ~300 | Medium (recent PR #106 work) |
|
|
25
|
+
| `lib/fontisan/woff2/` | ~250 | Medium |
|
|
26
|
+
| `lib/fontisan/optimizers/` | ~200 | Low |
|
|
27
|
+
| `lib/fontisan/pipeline/` | ~200 | Low |
|
|
28
|
+
| `lib/fontisan/commands/` | ~150 | Low |
|
|
29
|
+
| `lib/fontisan/validators/` | ~100 | Low |
|
|
30
|
+
| `lib/fontisan/binary/` | ~100 | Low |
|
|
31
|
+
| `spec/` | ~1500 | Low |
|
|
32
|
+
|
|
33
|
+
## Approach
|
|
34
|
+
|
|
35
|
+
One PR per namespace. Each PR:
|
|
36
|
+
|
|
37
|
+
1. Runs `rubocop -A lib/fontisan/<namespace>/` to auto-correct everything possible.
|
|
38
|
+
2. Manually fixes what auto-correct couldn't (typically: complex Layout cops, naming).
|
|
39
|
+
3. Updates `.rubocop_todo.yml` to drop the now-cleaned `Exclude:` entries.
|
|
40
|
+
4. Verifies the full test suite still passes.
|
|
41
|
+
|
|
42
|
+
Order: tables first (highest count, lowest risk — table parsing is purely structural).
|
|
43
|
+
|
|
44
|
+
## Out of scope
|
|
45
|
+
|
|
46
|
+
- Disabling cops project-wide — these are real issues, not noise.
|
|
47
|
+
- Refactoring architectural patterns rubocop flags (e.g., long methods) — separate refactors.
|
|
48
|
+
|
|
49
|
+
## Effort
|
|
50
|
+
|
|
51
|
+
~30 minutes per namespace on average. ~7 hours total.
|
|
52
|
+
|
|
53
|
+
## Dependencies
|
|
54
|
+
|
|
55
|
+
None.
|
|
56
|
+
|
|
57
|
+
## Acceptance criteria
|
|
58
|
+
|
|
59
|
+
- `.rubocop_todo.yml` < 100 lines (from ~800).
|
|
60
|
+
- `bundle exec rubocop` reports 0 offenses.
|
|
61
|
+
- All existing specs pass after each namespace PR.
|
|
62
|
+
- No behavior change (pure style/layout).
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Fontisan Improvements Backlog
|
|
2
|
+
|
|
3
|
+
This directory tracks all known improvement work, ordered by priority.
|
|
4
|
+
Each file is `NN-short-name.md` where `NN` is the priority order.
|
|
5
|
+
|
|
6
|
+
## Priorities
|
|
7
|
+
|
|
8
|
+
### P0 — Correctness gaps (open bugs / silent failures)
|
|
9
|
+
- [01 — CBDT/CBLC GID-stable propagation](01-cbdt-cblc-gid-stable-propagation.md)
|
|
10
|
+
- [02 — Collection-mode outline-priority regression](02-collection-outline-priority.md)
|
|
11
|
+
|
|
12
|
+
### P1 — High-value features
|
|
13
|
+
- [03 — `fontisan audit` command (identity+style+features lens)](03-fontisan-audit-command.md)
|
|
14
|
+
- [04 — UFO composite glyph encoding](04-ufo-composite-glyph-encoding.md)
|
|
15
|
+
- [05 — OTF compiler real CFF charstrings](05-otf-compiler-real-cff.md)
|
|
16
|
+
|
|
17
|
+
### P2 — Specialist feature parity
|
|
18
|
+
- [x] ~~[07 — CPAL v1 header fields](07-cpal-v1-header-fields.md)~~ ✓ Done
|
|
19
|
+
- [06 — CFF2 blend/vsindex operators](06-cff2-blend-vsindex-operators.md)
|
|
20
|
+
- [08 — CFF standard string table](08-cff-standard-string-table.md)
|
|
21
|
+
- [09 — Type 1 seac expansion](09-type1-seac-expansion.md)
|
|
22
|
+
- [10 — UFO image set + feature writers](10-ufo-image-set-feature-writers.md)
|
|
23
|
+
- [11 — kern groups.plist support](11-kern-groups-plist.md)
|
|
24
|
+
|
|
25
|
+
### P3 — Code-quality cleanup
|
|
26
|
+
- [x] ~~[13 — Split `OctokitFetcher` out of `fixture_downloader.rb`](13-split-octokit-fetcher.md)~~ ✓ Done (commit `4c68f2a`)
|
|
27
|
+
- [12 — `cbdt_fixture.rb` full BinData conversion](12-cbdt-fixture-bindata-conversion.md)
|
|
28
|
+
- [14 — Rubocop baseline chip (per-namespace)](14-rubocop-baseline-chip.md)
|
|
29
|
+
|
|
30
|
+
## Convention
|
|
31
|
+
|
|
32
|
+
Each TODO file follows this template:
|
|
33
|
+
|
|
34
|
+
```markdown
|
|
35
|
+
# NN — Title
|
|
36
|
+
|
|
37
|
+
## Priority
|
|
38
|
+
P0 / P1 / P2 / P3
|
|
39
|
+
|
|
40
|
+
## Problem
|
|
41
|
+
What's broken or missing. Concrete examples.
|
|
42
|
+
|
|
43
|
+
## Goal
|
|
44
|
+
What good looks like. Acceptance criteria.
|
|
45
|
+
|
|
46
|
+
## Approach
|
|
47
|
+
Architecture-level sketch. Files to add/change. Constraints.
|
|
48
|
+
|
|
49
|
+
## Out of scope
|
|
50
|
+
Explicit non-goals.
|
|
51
|
+
|
|
52
|
+
## Effort
|
|
53
|
+
Rough estimate (hours/days).
|
|
54
|
+
|
|
55
|
+
## Dependencies
|
|
56
|
+
Other TODOs that must land first.
|
|
57
|
+
```
|
data/lib/fontisan/tables/cpal.rb
CHANGED
|
@@ -27,7 +27,10 @@ module Fontisan
|
|
|
27
27
|
# - numColorRecords (uint16): Total number of color records
|
|
28
28
|
# - colorRecordsArrayOffset (uint32): Offset to color records array
|
|
29
29
|
#
|
|
30
|
-
# Version 1
|
|
30
|
+
# Version 1 Header (24 bytes) adds:
|
|
31
|
+
# - paletteTypesArrayOffset (uint32): Offset to palette types array
|
|
32
|
+
# - paletteLabelsArrayOffset (uint32): Offset to palette labels array
|
|
33
|
+
# - paletteEntryLabelsArrayOffset (uint32): Offset to palette entry labels
|
|
31
34
|
#
|
|
32
35
|
# Color Record Structure (4 bytes, BGRA format):
|
|
33
36
|
# - blue (uint8)
|
|
@@ -47,6 +50,13 @@ module Fontisan
|
|
|
47
50
|
# OpenType table tag for CPAL
|
|
48
51
|
TAG = "CPAL"
|
|
49
52
|
|
|
53
|
+
# Palette type bit flags (CPAL v1, paletteTypesArray entries).
|
|
54
|
+
PALETTE_TYPE_LIGHT_ON_DARK = 0x01
|
|
55
|
+
PALETTE_TYPE_DARK_ON_LIGHT = 0x02
|
|
56
|
+
|
|
57
|
+
# Offset that means "no table present" in CPAL v1.
|
|
58
|
+
NULL_OFFSET = 0
|
|
59
|
+
|
|
50
60
|
# @return [Integer] CPAL version (0 or 1)
|
|
51
61
|
attr_reader :version
|
|
52
62
|
|
|
@@ -71,6 +81,19 @@ module Fontisan
|
|
|
71
81
|
# @return [Array<Hash>] Parsed color records (RGBA hashes)
|
|
72
82
|
attr_reader :color_records
|
|
73
83
|
|
|
84
|
+
# @return [Array<Integer>, nil] Palette type bit flags, one per
|
|
85
|
+
# palette. nil for CPAL v0 or when v1 has no palette types array.
|
|
86
|
+
attr_reader :palette_types
|
|
87
|
+
|
|
88
|
+
# @return [Array<Integer>, nil] Name-record IDs (name table) for
|
|
89
|
+
# each palette. nil for CPAL v0 or when v1 has no labels array.
|
|
90
|
+
attr_reader :palette_labels
|
|
91
|
+
|
|
92
|
+
# @return [Array<Integer>, nil] Name-record IDs (name table) for
|
|
93
|
+
# each palette entry index (shared across palettes). nil for
|
|
94
|
+
# CPAL v0 or when v1 has no entry-labels array.
|
|
95
|
+
attr_reader :palette_entry_labels
|
|
96
|
+
|
|
74
97
|
# Override read to parse CPAL structure
|
|
75
98
|
#
|
|
76
99
|
# @param io [IO, String] Binary data to read
|
|
@@ -102,8 +125,8 @@ module Fontisan
|
|
|
102
125
|
# Parse color records
|
|
103
126
|
parse_color_records(io)
|
|
104
127
|
|
|
105
|
-
# Version 1
|
|
106
|
-
|
|
128
|
+
# Version 1: palette types + labels (lazily walked via offsets)
|
|
129
|
+
parse_v1_metadata if version == 1
|
|
107
130
|
rescue StandardError => e
|
|
108
131
|
raise CorruptedTableError, "Failed to parse CPAL table: #{e.message}"
|
|
109
132
|
end
|
|
@@ -152,6 +175,44 @@ module Fontisan
|
|
|
152
175
|
color_record ? color_to_hex(color_record) : nil
|
|
153
176
|
end
|
|
154
177
|
|
|
178
|
+
# Get the name-record ID for a palette's human-readable label.
|
|
179
|
+
#
|
|
180
|
+
# @param palette_index [Integer]
|
|
181
|
+
# @return [Integer, nil] nameID for the palette, or nil if v0 / no labels
|
|
182
|
+
def palette_label(palette_index)
|
|
183
|
+
return nil unless palette_labels
|
|
184
|
+
return nil if palette_index.negative? || palette_index >= palette_labels.length
|
|
185
|
+
|
|
186
|
+
palette_labels[palette_index]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Get the name-record ID for a palette entry's human-readable label.
|
|
190
|
+
#
|
|
191
|
+
# @param entry_index [Integer]
|
|
192
|
+
# @return [Integer, nil] nameID for the entry, or nil if v0 / no labels
|
|
193
|
+
def palette_entry_label(entry_index)
|
|
194
|
+
return nil unless palette_entry_labels
|
|
195
|
+
return nil if entry_index.negative? || entry_index >= palette_entry_labels.length
|
|
196
|
+
|
|
197
|
+
palette_entry_labels[entry_index]
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Whether a palette is intended for light-on-dark use (CPAL v1).
|
|
201
|
+
#
|
|
202
|
+
# @param palette_index [Integer]
|
|
203
|
+
# @return [Boolean] true if the palette type bit is set; false otherwise
|
|
204
|
+
def light_on_dark?(palette_index)
|
|
205
|
+
palette_type_set?(palette_index, PALETTE_TYPE_LIGHT_ON_DARK)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Whether a palette is intended for dark-on-light use (CPAL v1).
|
|
209
|
+
#
|
|
210
|
+
# @param palette_index [Integer]
|
|
211
|
+
# @return [Boolean] true if the palette type bit is set; false otherwise
|
|
212
|
+
def dark_on_light?(palette_index)
|
|
213
|
+
palette_type_set?(palette_index, PALETTE_TYPE_DARK_ON_LIGHT)
|
|
214
|
+
end
|
|
215
|
+
|
|
155
216
|
# Validate the CPAL table structure
|
|
156
217
|
#
|
|
157
218
|
# @return [Boolean] True if valid
|
|
@@ -169,7 +230,7 @@ module Fontisan
|
|
|
169
230
|
|
|
170
231
|
private
|
|
171
232
|
|
|
172
|
-
# Parse CPAL header (12 bytes for
|
|
233
|
+
# Parse CPAL header (12 bytes for v0, 24 bytes for v1)
|
|
173
234
|
#
|
|
174
235
|
# @param io [StringIO] Input stream
|
|
175
236
|
def parse_header(io)
|
|
@@ -179,12 +240,14 @@ module Fontisan
|
|
|
179
240
|
@num_color_records = io.read(2).unpack1("n")
|
|
180
241
|
@color_records_array_offset = io.read(4).unpack1("N")
|
|
181
242
|
|
|
182
|
-
# Version 1 has additional
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
243
|
+
# Version 1 has three additional uint32 offset fields. Offsets
|
|
244
|
+
# are read here but the arrays themselves are walked lazily
|
|
245
|
+
# from parse_v1_metadata so the v0 path (most common) doesn't
|
|
246
|
+
# pay for the seeks.
|
|
247
|
+
if @version == 1
|
|
248
|
+
@palette_types_array_offset = io.read(4).unpack1("N")
|
|
249
|
+
@palette_labels_array_offset = io.read(4).unpack1("N")
|
|
250
|
+
@palette_entry_labels_array_offset = io.read(4).unpack1("N")
|
|
188
251
|
end
|
|
189
252
|
end
|
|
190
253
|
|
|
@@ -228,7 +291,7 @@ module Fontisan
|
|
|
228
291
|
@palette_indices = []
|
|
229
292
|
return if num_palettes.zero?
|
|
230
293
|
|
|
231
|
-
# Palette indices immediately follow header (at offset 12 for v0,
|
|
294
|
+
# Palette indices immediately follow header (at offset 12 for v0, 24 for v1)
|
|
232
295
|
# Each index is uint16 (2 bytes)
|
|
233
296
|
num_palettes.times do
|
|
234
297
|
index = io.read(2).unpack1("n")
|
|
@@ -262,6 +325,44 @@ module Fontisan
|
|
|
262
325
|
end
|
|
263
326
|
end
|
|
264
327
|
|
|
328
|
+
# CPAL v1 metadata: palette types + labels + entry labels.
|
|
329
|
+
# Walked lazily via the offsets parsed in parse_header. Any
|
|
330
|
+
# offset of 0 means "no table present" — leave the accessor nil
|
|
331
|
+
# rather than raise.
|
|
332
|
+
def parse_v1_metadata
|
|
333
|
+
@palette_types = walk_uint32_array(@palette_types_array_offset, num_palettes)
|
|
334
|
+
@palette_labels = walk_uint16_array(@palette_labels_array_offset, num_palettes)
|
|
335
|
+
@palette_entry_labels = walk_uint16_array(@palette_entry_labels_array_offset,
|
|
336
|
+
num_palette_entries)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def walk_uint32_array(offset, count)
|
|
340
|
+
return nil if offset.nil? || offset == NULL_OFFSET
|
|
341
|
+
return [] if count.zero?
|
|
342
|
+
|
|
343
|
+
bytes = raw_data[offset, count * 4]
|
|
344
|
+
return nil unless bytes && bytes.bytesize == count * 4
|
|
345
|
+
|
|
346
|
+
bytes.unpack("N*")
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def walk_uint16_array(offset, count)
|
|
350
|
+
return nil if offset.nil? || offset == NULL_OFFSET
|
|
351
|
+
return [] if count.zero?
|
|
352
|
+
|
|
353
|
+
bytes = raw_data[offset, count * 2]
|
|
354
|
+
return nil unless bytes && bytes.bytesize == count * 2
|
|
355
|
+
|
|
356
|
+
bytes.unpack("n*")
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def palette_type_set?(palette_index, bit)
|
|
360
|
+
return false unless palette_types
|
|
361
|
+
return false if palette_index.negative? || palette_index >= palette_types.length
|
|
362
|
+
|
|
363
|
+
(palette_types[palette_index] & bit).nonzero? ? true : false
|
|
364
|
+
end
|
|
365
|
+
|
|
265
366
|
# Convert color record to hex string
|
|
266
367
|
#
|
|
267
368
|
# @param color [Hash] Color hash with :red, :green, :blue, :alpha keys
|
data/lib/fontisan/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fontisan
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.26
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -133,6 +133,21 @@ files:
|
|
|
133
133
|
- LICENSE
|
|
134
134
|
- README.adoc
|
|
135
135
|
- Rakefile
|
|
136
|
+
- TODO.improvements/01-cbdt-cblc-gid-stable-propagation.md
|
|
137
|
+
- TODO.improvements/02-collection-outline-priority.md
|
|
138
|
+
- TODO.improvements/03-fontisan-audit-command.md
|
|
139
|
+
- TODO.improvements/04-ufo-composite-glyph-encoding.md
|
|
140
|
+
- TODO.improvements/05-otf-compiler-real-cff.md
|
|
141
|
+
- TODO.improvements/06-cff2-blend-vsindex-operators.md
|
|
142
|
+
- TODO.improvements/07-cpal-v1-header-fields.md
|
|
143
|
+
- TODO.improvements/08-cff-standard-string-table.md
|
|
144
|
+
- TODO.improvements/09-type1-seac-expansion.md
|
|
145
|
+
- TODO.improvements/10-ufo-image-set-feature-writers.md
|
|
146
|
+
- TODO.improvements/11-kern-groups-plist.md
|
|
147
|
+
- TODO.improvements/12-cbdt-fixture-bindata-conversion.md
|
|
148
|
+
- TODO.improvements/13-split-octokit-fetcher.md
|
|
149
|
+
- TODO.improvements/14-rubocop-baseline-chip.md
|
|
150
|
+
- TODO.improvements/README.md
|
|
136
151
|
- benchmark/compile_benchmark.rb
|
|
137
152
|
- benchmark/variation_quick_bench.rb
|
|
138
153
|
- docs/.gitignore
|