fontisan 0.4.23 → 0.4.25
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/.rubocop_todo.yml +240 -29
- data/Gemfile +6 -0
- data/Rakefile +9 -6
- 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 +189 -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/benchmark/compile_benchmark.rb +13 -5
- data/docs/.vitepress/config.ts +1 -0
- data/docs/STITCHER_GUIDE.adoc +47 -3
- data/docs/api/layer.md +89 -0
- data/docs/cli/subset.md +25 -6
- data/docs/guide/cli/subset.md +31 -0
- data/docs/guide/color-fonts/bitmaps.md +17 -0
- data/lib/fontisan/cli.rb +2 -1
- data/lib/fontisan/collection/writer.rb +2 -1
- data/lib/fontisan/commands/validate_collection_command.rb +7 -1
- data/lib/fontisan/commands.rb +2 -1
- data/lib/fontisan/converters/collection_converter.rb +6 -1
- data/lib/fontisan/converters/svg_generator.rb +3 -1
- data/lib/fontisan/stitcher/collection_result.rb +2 -1
- data/lib/fontisan/stitcher/glyph_copier.rb +2 -1
- data/lib/fontisan/stitcher/partition_strategy/by_block.rb +3 -1
- data/lib/fontisan/stitcher/partition_strategy/by_script.rb +6 -2
- data/lib/fontisan/stitcher/source.rb +2 -1
- data/lib/fontisan/stitcher.rb +2 -1
- data/lib/fontisan/subset/table_strategy/glyf_loca_builder.rb +2 -1
- data/lib/fontisan/svg/standalone_glyph.rb +4 -1
- data/lib/fontisan/svg_to_glyf/assembler.rb +6 -3
- data/lib/fontisan/svg_to_glyf/geometry/transform_parser.rb +11 -4
- data/lib/fontisan/svg_to_glyf/geometry.rb +4 -2
- data/lib/fontisan/tables/cff/cff2_charstring_builder.rb +11 -5
- data/lib/fontisan/tables/cff.rb +2 -1
- data/lib/fontisan/tables/cff2/index_builder.rb +2 -1
- data/lib/fontisan/tables/cpal.rb +112 -11
- data/lib/fontisan/tables.rb +2 -1
- data/lib/fontisan/ufo/cli.rb +4 -1
- data/lib/fontisan/ufo/compile/cbdt_cblc.rb +2 -1
- data/lib/fontisan/ufo/compile/cff.rb +6 -2
- data/lib/fontisan/ufo/compile/cmap.rb +11 -5
- data/lib/fontisan/ufo/compile/cpal.rb +2 -1
- data/lib/fontisan/ufo/compile/feature_writers/gdef.rb +3 -1
- data/lib/fontisan/ufo/compile/feature_writers/mark_family_base.rb +3 -1
- data/lib/fontisan/ufo/compile/feature_writers.rb +2 -1
- data/lib/fontisan/ufo/compile/filters/cubic_to_quadratic.rb +4 -2
- data/lib/fontisan/ufo/compile/filters/remove_overlaps.rb +2 -1
- data/lib/fontisan/ufo/compile/fvar.rb +7 -3
- data/lib/fontisan/ufo/compile/glyf_loca.rb +4 -2
- data/lib/fontisan/ufo/compile/gvar.rb +5 -2
- data/lib/fontisan/ufo/compile/head.rb +2 -1
- data/lib/fontisan/ufo/compile/item_variation_store.rb +2 -1
- data/lib/fontisan/ufo/compile/name.rb +3 -1
- data/lib/fontisan/ufo/compile/sbix.rb +2 -1
- data/lib/fontisan/ufo/compile/stat.rb +6 -3
- data/lib/fontisan/ufo/convert/from_bin_data.rb +6 -2
- data/lib/fontisan/ufo/convert/to_dfont.rb +6 -2
- data/lib/fontisan/ufo/convert/to_otc.rb +4 -1
- data/lib/fontisan/ufo/convert/to_postscript.rb +8 -3
- data/lib/fontisan/ufo/convert/to_woff.rb +8 -3
- data/lib/fontisan/ufo/convert/to_woff2.rb +8 -3
- data/lib/fontisan/ufo/convert.rb +4 -1
- data/lib/fontisan/ufo/info.rb +4 -2
- data/lib/fontisan/ufo/plist.rb +7 -2
- data/lib/fontisan/ufo/reader.rb +3 -1
- data/lib/fontisan/ufo/writer.rb +13 -5
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/collection_decoder.rb +4 -1
- data/lib/fontisan/woff2/collection_encoder.rb +4 -1
- data/lib/fontisan/woff2/glyf_canonicalizer.rb +2 -1
- data/lib/fontisan/woff2/glyf_loca_reconstruct.rb +5 -2
- data/lib/fontisan/woff2/sfnt_checksum.rb +2 -1
- data/lib/fontisan.rb +0 -1
- metadata +18 -4
- data/lib/fontisan/tasks/fixture_downloader.rb +0 -162
- data/lib/fontisan/tasks.rb +0 -11
|
@@ -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
|
+
```
|
|
@@ -24,10 +24,17 @@ module FontisanBench
|
|
|
24
24
|
g.width = 500
|
|
25
25
|
g.add_unicode(0xE000 + i) if i < 0x1000
|
|
26
26
|
g.add_contour(Fontisan::Ufo::Contour.new([
|
|
27
|
-
Fontisan::Ufo::Point.new(x: 0,
|
|
28
|
-
|
|
29
|
-
Fontisan::Ufo::Point.new(
|
|
30
|
-
|
|
27
|
+
Fontisan::Ufo::Point.new(x: 0,
|
|
28
|
+
y: 0, type: "line"),
|
|
29
|
+
Fontisan::Ufo::Point.new(
|
|
30
|
+
x: 100, y: 0, type: "line",
|
|
31
|
+
),
|
|
32
|
+
Fontisan::Ufo::Point.new(
|
|
33
|
+
x: 100, y: 100, type: "offcurve",
|
|
34
|
+
),
|
|
35
|
+
Fontisan::Ufo::Point.new(
|
|
36
|
+
x: 50, y: 150, type: "curve",
|
|
37
|
+
),
|
|
31
38
|
]))
|
|
32
39
|
font.glyphs["g#{i}"] = g
|
|
33
40
|
end
|
|
@@ -38,7 +45,8 @@ module FontisanBench
|
|
|
38
45
|
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
39
46
|
yield
|
|
40
47
|
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
41
|
-
puts format("%-40<label>s %8.<elapsed>.2f ms", label: label,
|
|
48
|
+
puts format("%-40<label>s %8.<elapsed>.2f ms", label: label,
|
|
49
|
+
elapsed: elapsed * 1000)
|
|
42
50
|
elapsed
|
|
43
51
|
end
|
|
44
52
|
end
|
data/docs/.vitepress/config.ts
CHANGED
|
@@ -322,6 +322,7 @@ export default defineConfig({
|
|
|
322
322
|
{ text: "ConversionOptions", link: "/api/conversion-options" },
|
|
323
323
|
{ text: "SfntFont", link: "/api/sfnt-font" },
|
|
324
324
|
{ text: "Type1Font", link: "/api/type1-font" },
|
|
325
|
+
{ text: "Ufo::Layer", link: "/api/layer" },
|
|
325
326
|
],
|
|
326
327
|
},
|
|
327
328
|
{
|
data/docs/STITCHER_GUIDE.adoc
CHANGED
|
@@ -364,12 +364,56 @@ stitcher.write_to("out.ttf", format: :ttf, subfont: :main)
|
|
|
364
364
|
# → out.ttf has glyf + CBDT + CBLC, emoji renders correctly
|
|
365
365
|
----
|
|
366
366
|
|
|
367
|
-
|
|
367
|
+
=== Outline-first cmap priority
|
|
368
|
+
|
|
369
|
+
When a codepoint is covered by BOTH an outline donor and the CBDT
|
|
370
|
+
donor, the cmap maps it to the outline glyph — never to the CBDT
|
|
371
|
+
placeholder. The Cmap compiler follows first-wins semantics, and
|
|
372
|
+
`GlyphCopier` runs before `CbdtPropagator` so the outline GID is
|
|
373
|
+
registered first. This was the fix for the Emoticons cmap-loss bug
|
|
374
|
+
where shared codepoints silently dropped out of the cmap.
|
|
375
|
+
|
|
376
|
+
=== Placeholder name deconfliction
|
|
377
|
+
|
|
378
|
+
CBDT placeholders are named after the source's glyph IDs
|
|
379
|
+
(`"gid1"`, `"gid2"`, …), which is the SAME naming scheme
|
|
380
|
+
`Source#extract_truetype_glyph` uses for outline glyphs from TTF
|
|
381
|
+
donors. Without protection, a placeholder at `"gid110"` would
|
|
382
|
+
silently overwrite an outline glyph at `"gid110"` from a different
|
|
383
|
+
donor via `Layer#add` — the original cmap-loss vector for the
|
|
384
|
+
Essenfont CJK Ext G regression.
|
|
385
|
+
|
|
386
|
+
The Stitcher resolves this with two pieces:
|
|
387
|
+
|
|
388
|
+
* `Ufo::Layer#add` RAISES `Ufo::GlyphExistsError` on name conflict.
|
|
389
|
+
Silent overwrites are no longer possible. Callers that want to
|
|
390
|
+
replace an existing glyph use the explicit `Ufo::Layer#put`.
|
|
391
|
+
* `Stitcher::UniqueGlyphName.in(target, base)` allocates a
|
|
392
|
+
non-colliding name (`"gid110"`, then `"gid110.1"`, `"gid110.2"`,
|
|
393
|
+
…). `GlyphCopier` and `CbdtPropagator` both call it before every
|
|
394
|
+
`Layer#add`, so each glyph lands at its own GID slot without
|
|
395
|
+
touching the others.
|
|
396
|
+
|
|
397
|
+
=== Limitations
|
|
368
398
|
|
|
369
399
|
* Only ONE CBDT source per Stitcher. Multiple CBDT sources raise
|
|
370
400
|
`Fontisan::MultipleCbdtSourcesError`.
|
|
371
|
-
*
|
|
372
|
-
|
|
401
|
+
* **GID stability for overlapping codepoint ranges.** The raw-bytes
|
|
402
|
+
CBDT/CBLC propagation (`CbdtPropagator#propagate_tables_into`)
|
|
403
|
+
copies the source's CBLC verbatim. CBLC indexes glyphs by SOURCE
|
|
404
|
+
GID. After placeholder renaming (`"gid110"` → `"gid110.1"`), the
|
|
405
|
+
compiled GID no longer matches the source GID, so CBLC's source-
|
|
406
|
+
GID-indexed bitmaps can dangle. This is masked in the typical
|
|
407
|
+
Essenfont case (CBDT donor NotoColorEmoji and outline donors
|
|
408
|
+
cover disjoint codepoint ranges) but would produce wrong bitmaps
|
|
409
|
+
for any stitch where CBDT and outline sources share codepoint
|
|
410
|
+
ranges. A proper fix requires a CBLC rebuild pass that walks the
|
|
411
|
+
compiled cmap to remap source GIDs to compiled GIDs — tracked as
|
|
412
|
+
follow-up.
|
|
413
|
+
* **Collection-mode single-face coverage.** Each face is compiled
|
|
414
|
+
independently; `CbdtPropagator` currently requires at least one
|
|
415
|
+
outline codepoint per face. Collection-mode coverage of the
|
|
416
|
+
outline-first cmap-priority invariant is tracked as follow-up.
|
|
373
417
|
|
|
374
418
|
== CLI
|
|
375
419
|
|
data/docs/api/layer.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Ufo::Layer
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# `Ufo::Layer`
|
|
6
|
+
|
|
7
|
+
A single layer in a UFO source. A `Layer` holds a set of glyphs keyed by name. The default layer is `public.default` per UFO 3.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
layer = Fontisan::Ufo::Layer.new # => "public.default"
|
|
11
|
+
layer.add(Fontisan::Ufo::Glyph.new(name: "A"))
|
|
12
|
+
layer["A"] # => the glyph
|
|
13
|
+
layer.size # => 1
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Naming contract
|
|
17
|
+
|
|
18
|
+
Glyph names are the layer's primary key. Two distinct glyphs with the same name cannot coexist — adding the second would silently destroy the first, a class of bug that has historically cost real data (e.g. the CJK Ext G cmap loss when CBDT placeholders collided with outline glyphs sharing `"gid{N}"` names).
|
|
19
|
+
|
|
20
|
+
To make that contract unbreakable, the API exposes two methods with named semantics:
|
|
21
|
+
|
|
22
|
+
| Method | Behavior on conflict |
|
|
23
|
+
|--------|----------------------|
|
|
24
|
+
| `Layer#add(glyph)` | **Raises** `Ufo::GlyphExistsError` (the safe default) |
|
|
25
|
+
| `Layer#put(glyph)` | Overwrites the existing glyph (explicit replace) |
|
|
26
|
+
|
|
27
|
+
Callers that need auto-renaming — insert without giving up on a collision — call `Stitcher::UniqueGlyphName.in(target, base)` first to allocate a free name, then `Layer#add` the glyph under that name.
|
|
28
|
+
|
|
29
|
+
## API
|
|
30
|
+
|
|
31
|
+
### `Layer.new(name = "public.default")`
|
|
32
|
+
|
|
33
|
+
Initialize an empty layer.
|
|
34
|
+
|
|
35
|
+
### `#add(glyph) -> Glyph`
|
|
36
|
+
|
|
37
|
+
Insert `glyph`. Raises `Ufo::GlyphExistsError` if a glyph with the same name is already present.
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
layer = Fontisan::Ufo::Layer.new
|
|
41
|
+
layer.add(Fontisan::Ufo::Glyph.new(name: "A"))
|
|
42
|
+
|
|
43
|
+
layer.add(Fontisan::Ufo::Glyph.new(name: "A"))
|
|
44
|
+
# => raises Fontisan::Ufo::GlyphExistsError: a glyph named "A" already exists
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The original glyph is preserved when the second `add` raises — the layer is never partially mutated.
|
|
48
|
+
|
|
49
|
+
### `#put(glyph) -> Glyph`
|
|
50
|
+
|
|
51
|
+
Insert `glyph`, replacing any existing glyph with the same name. Use this when the caller has positively decided the previous glyph (if any) should be discarded.
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
layer.put(Fontisan::Ufo::Glyph.new(name: "A")) # overwrites the prior "A"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### `#[](glyph_name) -> Glyph, nil`
|
|
58
|
+
|
|
59
|
+
Look up a glyph by name.
|
|
60
|
+
|
|
61
|
+
### `#each { |glyph| ... }`
|
|
62
|
+
|
|
63
|
+
Iterate over glyphs (no ordering guarantee).
|
|
64
|
+
|
|
65
|
+
### `#size -> Integer`
|
|
66
|
+
|
|
67
|
+
Number of glyphs in the layer.
|
|
68
|
+
|
|
69
|
+
## `Ufo::GlyphExistsError`
|
|
70
|
+
|
|
71
|
+
Raised by `Layer#add` on a name conflict. Carries the colliding name so callers can branch programmatically:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
begin
|
|
75
|
+
layer.add(glyph)
|
|
76
|
+
rescue Fontisan::Ufo::GlyphExistsError => e
|
|
77
|
+
puts "name #{e.name.inspect} already taken"
|
|
78
|
+
# Decide: layer.put(glyph) to overwrite, or
|
|
79
|
+
# unique = Fontisan::Stitcher::UniqueGlyphName.in(target, e.name)
|
|
80
|
+
# layer.add(glyph.dup_as(name: unique))
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The error class is a sibling of `Layer` (not nested inside it) so the namespace stays flat — both `Ufo::Layer` and `Ufo::GlyphExistsError` live directly under `Fontisan::Ufo`.
|
|
85
|
+
|
|
86
|
+
## See also
|
|
87
|
+
|
|
88
|
+
- `docs/STITCHER_GUIDE.adoc` — `UniqueGlyphName`, `GlyphCopier`, and `CbdtPropagator` all build on the Layer naming contract.
|
|
89
|
+
- `docs/UFO_COMPILATION.adoc` — how layers feed the TTF/OTF/CFF2 compilers.
|
data/docs/cli/subset.md
CHANGED
|
@@ -18,8 +18,23 @@ fontisan subset <font> [options]
|
|
|
18
18
|
|--------|-------------|
|
|
19
19
|
| `--chars TEXT` | Characters to include |
|
|
20
20
|
| `--file FILE` | File containing characters |
|
|
21
|
+
| `--unicodes RANGE` | Unicode ranges (`"U+0000-007F"`) |
|
|
22
|
+
| `--glyphs LIST` | Glyph names/IDs |
|
|
21
23
|
| `--output FILE` | Output file path |
|
|
22
|
-
| `--format FORMAT` | Output format |
|
|
24
|
+
| `--format FORMAT` | Output format (`ttf`, `woff`, `woff2`, …) |
|
|
25
|
+
| `--profile PROFILE` | Subsetting profile (default: `pdf`) |
|
|
26
|
+
| `--retain-gids` | Preserve original glyph IDs |
|
|
27
|
+
|
|
28
|
+
## Profiles
|
|
29
|
+
|
|
30
|
+
| Profile | Tables included | Use case |
|
|
31
|
+
|---------|-----------------|----------|
|
|
32
|
+
| `pdf` | core + glyf/loca | PDF embedding (smallest) |
|
|
33
|
+
| `web` | core + glyf/loca + GSUB/GPOS + **CBDT/CBLC** | Web fonts that retain color-emoji bitmaps |
|
|
34
|
+
| `minimal` | core only | Smallest valid font |
|
|
35
|
+
| `full` | every standard table | Lossless re-export |
|
|
36
|
+
|
|
37
|
+
The `web` profile is the one to reach for when the subset must still render color emoji in a browser: it preserves the CBDT (bitmap data) and CBLC (bitmap location) tables, and the paired subsetter rewrites them to keep only the bitmaps for the retained glyphs.
|
|
23
38
|
|
|
24
39
|
## Examples
|
|
25
40
|
|
|
@@ -27,13 +42,17 @@ fontisan subset <font> [options]
|
|
|
27
42
|
# Subset to specific characters
|
|
28
43
|
fontisan subset font.ttf --chars "ABCDEF" --output subset.ttf
|
|
29
44
|
|
|
30
|
-
#
|
|
31
|
-
fontisan subset
|
|
45
|
+
# Web subset that retains color-emoji bitmaps
|
|
46
|
+
fontisan subset NotoColorEmoji.ttf \
|
|
47
|
+
--chars "😀😁😂🤣😊" \
|
|
48
|
+
--profile web \
|
|
49
|
+
--format woff2 \
|
|
50
|
+
--output emoji.woff2
|
|
32
51
|
|
|
33
|
-
#
|
|
34
|
-
fontisan subset font.ttf --chars "
|
|
52
|
+
# PDF embedding profile (drops CBDT/CBLC, smaller output)
|
|
53
|
+
fontisan subset font.ttf --chars "ABC" --profile pdf --output pdf-subset.ttf
|
|
35
54
|
```
|
|
36
55
|
|
|
37
56
|
## Detailed Documentation
|
|
38
57
|
|
|
39
|
-
For comprehensive documentation including unicode ranges and
|
|
58
|
+
For comprehensive documentation including unicode ranges, profile selection, and CBDT/CBLC behavior, see the [subset command guide](/guide/cli/subset).
|
data/docs/guide/cli/subset.md
CHANGED
|
@@ -28,6 +28,7 @@ fontisan subset FONT [options]
|
|
|
28
28
|
| `--glyphs LIST` | Glyph names/IDs |
|
|
29
29
|
| `--output PATH` | Output file |
|
|
30
30
|
| `--output-format FORMAT` | Output format |
|
|
31
|
+
| `--profile PROFILE` | Subsetting profile (`pdf`, `web`, `minimal`, `full`; default `pdf`) |
|
|
31
32
|
| `--retain-gids` | Retain glyph IDs |
|
|
32
33
|
|
|
33
34
|
## Examples
|
|
@@ -86,6 +87,36 @@ fontisan subset font.ttf --chars "ABC" --output-format woff2 --output subset.wof
|
|
|
86
87
|
|
|
87
88
|
## Subsetting Strategies
|
|
88
89
|
|
|
90
|
+
### Profile selection
|
|
91
|
+
|
|
92
|
+
The `--profile` option controls which font tables the subset retains. The right choice depends on what the subset is for:
|
|
93
|
+
|
|
94
|
+
| Profile | When to use | Notable tables dropped |
|
|
95
|
+
|---------|-------------|------------------------|
|
|
96
|
+
| `pdf` (default) | PDF embedding, smallest valid output | GSUB, GPOS, CBDT, CBLC |
|
|
97
|
+
| `web` | Browser/web font, **retains color-emoji bitmaps** | nothing that browsers need |
|
|
98
|
+
| `minimal` | Absolute smallest core that still renders | glyf/loca, CBDT/CBLC, GSUB/GPOS |
|
|
99
|
+
| `full` | Lossless re-export | (none) |
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Web subset that keeps color-emoji bitmaps so 😀 renders in color
|
|
103
|
+
fontisan subset NotoColorEmoji.ttf \
|
|
104
|
+
--chars "😀😁😂🤣😊" \
|
|
105
|
+
--profile web \
|
|
106
|
+
--output-format woff2 \
|
|
107
|
+
--output emoji.woff2
|
|
108
|
+
|
|
109
|
+
# PDF embedding — drops color bitmaps for size
|
|
110
|
+
fontisan subset NotoColorEmoji.ttf \
|
|
111
|
+
--chars "😀" \
|
|
112
|
+
--profile pdf \
|
|
113
|
+
--output emoji-pdf.ttf
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### Web profile and CBDT/CBLC
|
|
117
|
+
|
|
118
|
+
The `web` profile is the only built-in profile that retains CBDT (Color Bitmap Data) and CBLC (Color Bitmap Location) tables. The subsetter walks CBLC to find each retained glyph's bitmap block, rewrites CBDT to keep only those blocks, and rebuilds CBLC's IndexSubTableArray to point at the new offsets. Glyph IDs are remapped through the subset's `GlyphMapping`, so the output CBLC references subset GIDs, not source GIDs.
|
|
119
|
+
|
|
89
120
|
### Web Font Optimization
|
|
90
121
|
|
|
91
122
|
```bash
|
|
@@ -144,6 +144,23 @@ fontisan convert bitmap-font.ttf --to ttf --output bitmap-font.ttf
|
|
|
144
144
|
fontisan convert bitmap-font.ttf --to ttf --no-bitmaps --output outline-only.ttf
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
### Subsetting color-emoji fonts
|
|
148
|
+
|
|
149
|
+
The `web` subsetting profile retains CBDT and CBLC tables and rewrites them to keep only the bitmaps for retained glyphs. Use it when subsetting a color-emoji source for browser delivery:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# Keep only 😀 + 😁 bitmaps; output WOFF2 for web
|
|
153
|
+
fontisan subset NotoColorEmoji.ttf \
|
|
154
|
+
--chars "😀😁" \
|
|
155
|
+
--profile web \
|
|
156
|
+
--output-format woff2 \
|
|
157
|
+
--output emoji-subset.woff2
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Other profiles (`pdf`, `minimal`, `full`) drop CBDT/CBLC — pick them when the subset doesn't need color-emoji rendering (e.g. PDF embedding where the subsetter is just selecting glyphs for outlines).
|
|
161
|
+
|
|
162
|
+
See the [subset CLI docs](/cli/subset) and `docs/STITCHER_GUIDE.adoc` (CBDT/CBLC passthrough section) for the full story, including the GID-stability caveat when CBDT and outline donors cover overlapping codepoint ranges in stitch mode.
|
|
163
|
+
|
|
147
164
|
## Comparison
|
|
148
165
|
|
|
149
166
|
| Feature | sbix | CBDT/CBLC |
|
data/lib/fontisan/cli.rb
CHANGED
|
@@ -360,7 +360,8 @@ module Fontisan
|
|
|
360
360
|
|
|
361
361
|
# Handle --show-options
|
|
362
362
|
if options[:show_options]
|
|
363
|
-
show_recommended_options(source_format,
|
|
363
|
+
show_recommended_options(source_format,
|
|
364
|
+
options[:to].is_a?(Array) ? options[:to].first : options[:to])
|
|
364
365
|
return
|
|
365
366
|
end
|
|
366
367
|
|
|
@@ -95,7 +95,8 @@ module Fontisan
|
|
|
95
95
|
#
|
|
96
96
|
# @return [String] TTC header binary
|
|
97
97
|
def write_ttc_header
|
|
98
|
-
[TTC_TAG, VERSION_1_0_MAJOR, VERSION_1_0_MINOR,
|
|
98
|
+
[TTC_TAG, VERSION_1_0_MAJOR, VERSION_1_0_MINOR,
|
|
99
|
+
@fonts.size].pack("a4nnN")
|
|
99
100
|
end
|
|
100
101
|
|
|
101
102
|
# Write offset table
|
|
@@ -81,7 +81,13 @@ module Fontisan
|
|
|
81
81
|
Check.new(
|
|
82
82
|
name: :glyph_cap,
|
|
83
83
|
passed: over.empty?,
|
|
84
|
-
message: over.empty?
|
|
84
|
+
message: if over.empty?
|
|
85
|
+
nil
|
|
86
|
+
else
|
|
87
|
+
"faces over cap: #{over.map do |s|
|
|
88
|
+
"##{s.index}=#{s.glyph_count}"
|
|
89
|
+
end.join(', ')}"
|
|
90
|
+
end,
|
|
85
91
|
)
|
|
86
92
|
end
|
|
87
93
|
|
data/lib/fontisan/commands.rb
CHANGED
|
@@ -21,7 +21,8 @@ module Fontisan
|
|
|
21
21
|
autoload :TablesCommand, "fontisan/commands/tables_command"
|
|
22
22
|
autoload :UnicodeCommand, "fontisan/commands/unicode_command"
|
|
23
23
|
autoload :UnpackCommand, "fontisan/commands/unpack_command"
|
|
24
|
-
autoload :ValidateCollectionCommand,
|
|
24
|
+
autoload :ValidateCollectionCommand,
|
|
25
|
+
"fontisan/commands/validate_collection_command"
|
|
25
26
|
autoload :ValidateCommand, "fontisan/commands/validate_command"
|
|
26
27
|
autoload :VariableCommand, "fontisan/commands/variable_command"
|
|
27
28
|
end
|
|
@@ -173,7 +173,12 @@ module Fontisan
|
|
|
173
173
|
io.read(8) # signature + flavor
|
|
174
174
|
io.rewind
|
|
175
175
|
flavor = File.binread(path, 4, 4)&.unpack1("N")
|
|
176
|
-
flavor == Woff2::CollectionDecoder::TTC_FLAVOR
|
|
176
|
+
if flavor == Woff2::CollectionDecoder::TTC_FLAVOR
|
|
177
|
+
:woff2_collection
|
|
178
|
+
else
|
|
179
|
+
(raise Error,
|
|
180
|
+
"Not a WOFF2 collection: #{path}")
|
|
181
|
+
end
|
|
177
182
|
elsif Parsers::DfontParser.dfont?(io)
|
|
178
183
|
:dfont
|
|
179
184
|
else
|
|
@@ -192,7 +192,9 @@ palette_index: 0)
|
|
|
192
192
|
def build_glyph_to_codepoints(cmap)
|
|
193
193
|
return {} unless cmap
|
|
194
194
|
|
|
195
|
-
cmap.unicode_mappings.each_with_object(Hash.new
|
|
195
|
+
cmap.unicode_mappings.each_with_object(Hash.new do |h, k|
|
|
196
|
+
h[k] = []
|
|
197
|
+
end) do |(cp, gid), h|
|
|
196
198
|
h[gid] << cp
|
|
197
199
|
end
|
|
198
200
|
rescue StandardError => e
|
|
@@ -9,7 +9,8 @@ module Fontisan
|
|
|
9
9
|
|
|
10
10
|
# Return value of {Stitcher#write_collection}. Carries the output path,
|
|
11
11
|
# total bytes, and one {SubfontStats} per declared subfont.
|
|
12
|
-
CollectionResult = Struct.new(:path, :bytes, :subfonts,
|
|
12
|
+
CollectionResult = Struct.new(:path, :bytes, :subfonts,
|
|
13
|
+
keyword_init: true) do
|
|
13
14
|
def face_count
|
|
14
15
|
subfonts.size
|
|
15
16
|
end
|
|
@@ -89,7 +89,8 @@ module Fontisan
|
|
|
89
89
|
bindings.sort_by { |b| [b[:codepoint] || Float::INFINITY, b[:donor_gid]] }
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
-
def copy_glyph_into(target_font, name:, source:, donor_gid:,
|
|
92
|
+
def copy_glyph_into(target_font, name:, source:, donor_gid:,
|
|
93
|
+
codepoint: nil)
|
|
93
94
|
original = source.glyph_for_gid(donor_gid)
|
|
94
95
|
return unless original
|
|
95
96
|
|
|
@@ -403,7 +403,9 @@ module Fontisan
|
|
|
403
403
|
private
|
|
404
404
|
|
|
405
405
|
def group_by_block(cp_map)
|
|
406
|
-
cp_map.each_with_object(Hash.new
|
|
406
|
+
cp_map.each_with_object(Hash.new do |h, k|
|
|
407
|
+
h[k] = []
|
|
408
|
+
end) do |(cp, label), h|
|
|
407
409
|
block_label = find_block_label(cp) || :other
|
|
408
410
|
h[block_label] << [cp, label]
|
|
409
411
|
end
|