fontisan 0.4.7 → 0.4.9

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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.adoc +116 -2
  4. data/docs/.vitepress/config.ts +1 -7
  5. data/docs/COLLECTION_VALIDATION.adoc +61 -0
  6. data/docs/STITCHER_GUIDE.adoc +138 -1
  7. data/docs/cli/convert.md +44 -2
  8. data/docs/cli/index.md +21 -28
  9. data/docs/cli/validate-collection.md +95 -0
  10. data/docs/cli/validate.md +8 -0
  11. data/docs/index.md +0 -2
  12. data/lib/fontisan/cli.rb +29 -8
  13. data/lib/fontisan/collection/reader/stats.rb +23 -0
  14. data/lib/fontisan/collection/reader.rb +90 -0
  15. data/lib/fontisan/collection.rb +1 -0
  16. data/lib/fontisan/commands/convert_command.rb +96 -18
  17. data/lib/fontisan/commands/multi_format_output.rb +59 -0
  18. data/lib/fontisan/commands/validate_collection_command.rb +121 -0
  19. data/lib/fontisan/commands.rb +2 -0
  20. data/lib/fontisan/error.rb +25 -0
  21. data/lib/fontisan/models.rb +0 -1
  22. data/lib/fontisan/stitcher/collection_result.rb +18 -0
  23. data/lib/fontisan/stitcher/partition_strategy/base.rb +23 -0
  24. data/lib/fontisan/stitcher/partition_strategy/blueprint.rb +24 -0
  25. data/lib/fontisan/stitcher/partition_strategy/by_plane.rb +131 -0
  26. data/lib/fontisan/stitcher/partition_strategy/partition.rb +24 -0
  27. data/lib/fontisan/stitcher/partition_strategy.rb +22 -0
  28. data/lib/fontisan/stitcher.rb +44 -10
  29. data/lib/fontisan/ufo/compile/name.rb +2 -2
  30. data/lib/fontisan/ufo/info.rb +48 -0
  31. data/lib/fontisan/unicode/plane.rb +56 -0
  32. data/lib/fontisan/unicode.rb +17 -0
  33. data/lib/fontisan/version.rb +1 -1
  34. data/lib/fontisan.rb +2 -2
  35. metadata +14 -18
  36. data/docs/cli/audit.md +0 -337
  37. data/lib/fontisan/cldr/aggregator.rb +0 -33
  38. data/lib/fontisan/cldr/cache_manager.rb +0 -110
  39. data/lib/fontisan/cldr/config.rb +0 -59
  40. data/lib/fontisan/cldr/download_error.rb +0 -9
  41. data/lib/fontisan/cldr/downloader.rb +0 -79
  42. data/lib/fontisan/cldr/error.rb +0 -8
  43. data/lib/fontisan/cldr/index.rb +0 -64
  44. data/lib/fontisan/cldr/index_builder.rb +0 -72
  45. data/lib/fontisan/cldr/unicode_set_parser.rb +0 -189
  46. data/lib/fontisan/cldr/unknown_version_error.rb +0 -9
  47. data/lib/fontisan/cldr/version_resolver.rb +0 -91
  48. data/lib/fontisan/cldr.rb +0 -23
  49. data/lib/fontisan/cli/cldr_cli.rb +0 -85
  50. data/lib/fontisan/config/cldr.yml +0 -22
  51. data/lib/fontisan/models/cldr/language_coverage.rb +0 -31
  52. data/lib/fontisan/models/cldr.rb +0 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9957716f86dad58d8ff9b5f0bc49bc7044ab20f073bd26b5b87f09d8a5aa1cb1
4
- data.tar.gz: 5ac5083c8db47532bc1331c82209c02bc6d8b7095b8f2c27c41fecced8230b32
3
+ metadata.gz: 38b49343b74d1876d57ec13992fc49fdc2879a44d1f0fe3c6529bd3a2f8f7a75
4
+ data.tar.gz: cc69156566e86294177d9046f39ebde8334f87807da38a7a5dd83ddd2eea8247
5
5
  SHA512:
6
- metadata.gz: e1fa7608f49e922735d6b677456cbf4864ced9a32e3e771960229f685208c67be6079f9b1bfcfc6f393c83bf8d5b7e0e158c397b618080229a14dd274718724e
7
- data.tar.gz: 730148059f3819b6dd49c1f8b86ab9d2e4d8508671b54b59cc240e638472b0da0dd5e5b7187f30b09bbd12d8e149a93903097068a45ac281224f2482464fa2ea
6
+ metadata.gz: 2822ae02861c4183bbd42ded8005fa17d28d59175155767413f94b4400faaf28fb61dad38736f3fcd11235746e1b254151c8e8918480b5ccb146882742c3ec99
7
+ data.tar.gz: eefb0dbf8e3360e7b8aeb89dfcfbf62425c255075ee1a3d2c030cd2551f90f08d980f7ea7b8c56c382b0a1cb69933654a57fc4e57d069ebe6809ace84e7d0b42
data/CHANGELOG.md CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Removed
11
+
12
+ - Audit/UCD documentation residue from the v0.3.0 audit/UCD subsystem
13
+ removal: deleted `docs/cli/audit.md`, dropped the "🔍 Font Audit"
14
+ feature card and "Font Audit" sidebar group, removed audit entries
15
+ from `docs/cli/index.md`. No behavior change.
16
+ - CLDR subsystem (`Fontisan::Cldr`, `Models::Cldr::LanguageCoverage`,
17
+ `lib/fontisan/config/cldr.yml`) and the `fontisan cldr` CLI
18
+ subcommand. CLDR was added in TODO 21 as infrastructure for
19
+ `fontisan audit`'s per-language coverage extractor; with the audit
20
+ subsystem moved to ucode (v0.3.0), CLDR had no remaining consumer
21
+ in fontisan. Not released in a published gem version.
22
+
10
23
  ### Added
11
24
 
12
25
  - `Stitcher` explicit subfont declaration model: every `include_*`
data/README.adoc CHANGED
@@ -1260,6 +1260,42 @@ Status: INVALID
1260
1260
  ----
1261
1261
  ====
1262
1262
 
1263
+ === Collection structural validation (`validate-collection`)
1264
+
1265
+ For collection-level structural checks (face count, per-face glyph cap,
1266
+ optional cmap-union size) without running the full per-face profile
1267
+ machinery, use the dedicated `validate-collection` command:
1268
+
1269
+ [source,shell]
1270
+ ----
1271
+ # Glyph-cap-only check (default)
1272
+ fontisan validate-collection family.ttc
1273
+
1274
+ # Require exactly 5 faces
1275
+ fontisan validate-collection family.ttc --expected-faces 5
1276
+
1277
+ # Tighter per-face glyph cap + minimum cmap coverage
1278
+ fontisan validate-collection family.ttc \
1279
+ --max-glyphs 60000 \
1280
+ --expected-cmap-union 100000
1281
+ ----
1282
+
1283
+ Exit code is `0` when every requested check passes, `1` when any
1284
+ check fails. With no options, only the per-face glyph cap is checked
1285
+ (default `65,535`).
1286
+
1287
+ This command is intentionally separate from `fontisan validate PATH`
1288
+ (MECE): `validate` runs profile-based per-face checks;
1289
+ `validate-collection` runs collection-level structural checks. They
1290
+ do not share a subcommand tree.
1291
+
1292
+ For programmatic access to the same metadata, see
1293
+ `Fontisan::Collection::Reader` — the read-only counterpart to
1294
+ `Collection::Builder`. It opens a TTC/OTC/dfont and exposes
1295
+ `face_count`, per-face `stats` (glyph/codepoint counts), and the
1296
+ cmap union across all faces, without hand-rolling the TTC header
1297
+ bytes.
1298
+
1263
1299
  === Ruby API usage
1264
1300
 
1265
1301
  ==== Architecture
@@ -2187,6 +2223,29 @@ fontisan convert font.ttf --to woff2 --output font.woff2 \
2187
2223
  fontisan convert font.ttf --to woff --output font.woff --zlib-level 9
2188
2224
  ----
2189
2225
 
2226
+ `--to` accepts multiple targets so a single invocation produces N
2227
+ output files from one input font. Pass a comma-separated list
2228
+ (`--to woff,woff2`) or repeat the flag (`--to woff --to woff2`).
2229
+ Duplicates are deduplicated.
2230
+
2231
+ [source,shell]
2232
+ ----
2233
+ # Emit both WOFF and WOFF2 in one invocation
2234
+ fontisan convert font.ttf --to woff,woff2 --output font
2235
+ # → font.woff, font.woff2
2236
+ ----
2237
+
2238
+ Output-path rules for multi-format:
2239
+
2240
+ * `--output out` (no extension) + N formats → append `.<format>` per
2241
+ target (`out.woff`, `out.woff2`).
2242
+ * `--output out.ttf` (extension present) + N formats → exits 1
2243
+ (ambiguous which format gets the extension).
2244
+ * `--output out.ttf` + one format → use as given (existing behaviour).
2245
+
2246
+ Multi-format is single-font → single-font only. Combining multi-format
2247
+ with collection input (TTC/OTC/dfont) exits 1.
2248
+
2190
2249
  For detailed information on all available options and conversion scenarios,
2191
2250
  see the link:docs/CONVERSION_GUIDE.adoc[Conversion Guide].
2192
2251
 
@@ -2499,8 +2558,63 @@ stitcher.add_source(:noto_cjk, Fontisan::FontLoader.load("NotoSansCJK.ttf"))
2499
2558
  stitcher.include_range(0x41..0x5A, from: :noto_sans, into: :latin)
2500
2559
  stitcher.include_range(0x4E00..0x9FFF, from: :noto_cjk, into: :cjk)
2501
2560
 
2502
- # Write as OTC with CFF2 subfonts and table deduplication
2503
- stitcher.write_collection("out.otc", format: :otf2)
2561
+ # Write as OTC with CFF2 subfonts and table deduplication.
2562
+ # Returns a Stitcher::CollectionResult (path, bytes, per-subfont stats).
2563
+ result = stitcher.write_collection("out.otc", format: :otf2)
2564
+ result.face_count # => 2
2565
+ result.subfonts.map(&:name) # => [:latin, :cjk]
2566
+ result.subfonts.first.glyph_count # maxp.num_glyphs of face 0
2567
+ ----
2568
+
2569
+ === Donor maps and partitioning
2570
+
2571
+ `include_codepoints_map(cp_map, into:)` takes a `{codepoint => donor}`
2572
+ map and groups internally — one call handles N codepoints from M
2573
+ donors:
2574
+
2575
+ [source,ruby]
2576
+ ----
2577
+ stitcher.include_codepoints_map(
2578
+ { 0x41 => :noto_sans, 0x4E00 => :noto_cjk },
2579
+ into: :main,
2580
+ )
2581
+ ----
2582
+
2583
+ For automatic plane-aware splitting, use
2584
+ `Stitcher::PartitionStrategy::ByPlane`. It groups codepoints by
2585
+ Unicode plane and sub-splits planes that overflow the cap along the
2586
+ large CJK-extension block boundaries:
2587
+
2588
+ [source,ruby]
2589
+ ----
2590
+ require "fontisan/stitcher/partition_strategy"
2591
+
2592
+ blueprint = Fontisan::Stitcher::PartitionStrategy::ByPlane.new
2593
+ .call(cp_map, cap: 65_484)
2594
+ blueprint.apply_to(stitcher) # declares one subfont per partition
2595
+ ----
2596
+
2597
+ If a single CJK extension block alone exceeds the cap, ByPlane raises
2598
+ `Fontisan::PartitionCapExceededError`. ByBlock/ByScript partitioners
2599
+ (via Unicode Blocks.txt / Scripts.txt) are tracked as a follow-up —
2600
+ the framework is open for them.
2601
+
2602
+ === Per-subfont metadata
2603
+
2604
+ `Ufo::Info.for_subfont(family:, subfont:, version:, ...)` builds the
2605
+ standard name-table fields for one subfont of a collection. The
2606
+ family name embeds the subfont (`"essenfont SIP"`), the PostScript
2607
+ name uses the hyphenated form (`"essenfont-SIP"`), and the version is
2608
+ parsed into `version_major` / `version_minor` per the UFO
2609
+ major.minor shape.
2610
+
2611
+ [source,ruby]
2612
+ ----
2613
+ stitcher.set_info(
2614
+ Fontisan::Ufo::Info.for_subfont(
2615
+ family: "essenfont", subfont: :SIP, version: "0.1"
2616
+ ).to_plist,
2617
+ )
2504
2618
  ----
2505
2619
 
2506
2620
  === CBDT/CBLC passthrough (color emoji)
@@ -263,13 +263,6 @@ export default defineConfig({
263
263
  { text: "Overview", link: "/cli/" },
264
264
  ],
265
265
  },
266
- {
267
- text: "Font Audit",
268
- collapsed: true,
269
- items: [
270
- { text: "audit", link: "/cli/audit" },
271
- ],
272
- },
273
266
  {
274
267
  text: "Font Information",
275
268
  collapsed: true,
@@ -292,6 +285,7 @@ export default defineConfig({
292
285
  { text: "convert", link: "/cli/convert" },
293
286
  { text: "subset", link: "/cli/subset" },
294
287
  { text: "validate", link: "/cli/validate" },
288
+ { text: "validate-collection", link: "/cli/validate-collection" },
295
289
  { text: "instance", link: "/cli/instance" },
296
290
  { text: "export", link: "/cli/export" },
297
291
  { text: "dump-table", link: "/cli/dump-table" },
@@ -141,3 +141,64 @@ puts "Valid fonts: #{valid_count}"
141
141
  puts "Invalid fonts: #{invalid_count}"
142
142
  ----
143
143
  ====
144
+
145
+ == Fast structural checks: `validate-collection`
146
+
147
+ For collection-level structural checks (face count, per-face glyph
148
+ cap, optional cmap-union size) without running the full per-face
149
+ profile machinery, use the dedicated `validate-collection` command:
150
+
151
+ [source,shell]
152
+ ----
153
+ # Glyph-cap-only check (default)
154
+ fontisan validate-collection family.ttc
155
+
156
+ # Require exactly 5 faces
157
+ fontisan validate-collection family.ttc --expected-faces 5
158
+
159
+ # Tighter per-face glyph cap + minimum cmap coverage
160
+ fontisan validate-collection family.ttc \
161
+ --max-glyphs 60000 \
162
+ --expected-cmap-union 100000
163
+ ----
164
+
165
+ Exit code is `0` when every requested check passes, `1` when any
166
+ check fails. With no options, only the per-face glyph cap is checked
167
+ (default `65,535`).
168
+
169
+ This command is intentionally separate from `fontisan validate PATH`
170
+ (MECE): the two cover different validator categories — `validate`
171
+ runs profile-based per-face checks, `validate-collection` runs
172
+ collection-level structural checks. They don't share a subcommand tree.
173
+
174
+ == Reading collection metadata: `Collection::Reader`
175
+
176
+ `Fontisan::Collection::Reader` is the read-only counterpart to
177
+ `Collection::Builder`. It opens an existing TTC/OTC/dfont and
178
+ exposes per-face metadata plus the cmap union across all faces,
179
+ without hand-rolling the TTC header bytes.
180
+
181
+ [source,ruby]
182
+ ----
183
+ reader = Fontisan::Collection::Reader.open("family.ttc")
184
+
185
+ reader.face_count # => 3
186
+ reader.path # => "family.ttc"
187
+
188
+ reader.each_face do |face|
189
+ puts face.table("maxp").num_glyphs
190
+ end
191
+
192
+ # One Stats struct per face (index, glyph_count, codepoint_count, sfnt_version)
193
+ reader.stats.map(&:glyph_count) # => [3541, 21048, 991]
194
+
195
+ # Union of every face's cmap keys
196
+ reader.cmap_union.size # => 24580
197
+ reader.cmap_union.is_a?(Set) # => true
198
+ ----
199
+
200
+ `Reader` delegates header parsing to `FontLoader` (which already
201
+ handles TTC, OTC, and dfont via `BinData`). Constructing a `Reader`
202
+ on a non-collection file raises `ArgumentError`. This is the API the
203
+ `validate-collection` command uses internally — reach for it directly
204
+ when you want collection metadata without invoking the CLI.
@@ -25,15 +25,19 @@ There are no defaults and no after-the-fact splitting.
25
25
 
26
26
  | `include_codepoints(cps, from:, into:)` | Add an explicit Array of codepoints
27
27
 
28
+ | `include_codepoints_map(cp_map, into:)` | Add `{codepoint => donor}` for many codepoints and donors in one call
29
+
28
30
  | `include_gid(donor_gid, from:, into:)` | Add a specific GID from the source
29
31
 
30
32
  | `include_notdef(from:, into:)` | Convenience for `include_gid(0, ...)`
31
33
 
32
34
  | `set_info(hash)` | Override font info (family name, etc.)
33
35
 
36
+ | `subfont_names` | List declared subfont names (insertion order)
37
+
34
38
  | `write_to(path, format:, subfont:)` | Compile one subfont to a single file
35
39
 
36
- | `write_collection(path, format:)` | Compile all declared subfonts into a TTC/OTC
40
+ | `write_collection(path, format:)` | Compile all declared subfonts into a TTC/OTC; returns a `CollectionResult`
37
41
  |===
38
42
 
39
43
  `format:` is one of `:ttf`, `:otf`, `:otf2`. For collections, `:ttf`
@@ -77,6 +81,139 @@ Each subfont is compiled independently, then packed by
77
81
  (head, name, OS/2, ...) are stored once and referenced from each
78
82
  subfont's offset table.
79
83
 
84
+ `write_collection` returns a `Stitcher::CollectionResult` Struct with
85
+ the output `path`, total `bytes`, and one `Stitcher::SubfontStats`
86
+ per declared subfont (`name`, `glyph_count`, `codepoint_count`). Stats
87
+ are read from the compiled on-disk face so they reflect what was
88
+ actually written — including glyphs the compiler adds (e.g. `.notdef`).
89
+
90
+ [source,ruby]
91
+ ----
92
+ result = stitcher.write_collection("out.otc", format: :otf2)
93
+ result.path # => "out.otc"
94
+ result.bytes # => File.size("out.otc")
95
+ result.face_count # => result.subfonts.size
96
+ result.subfonts.first.glyph_count # maxp.num_glyphs of face 0
97
+ result.subfonts.first.codepoint_count # cmap size of face 0
98
+ ----
99
+
100
+ == Donor maps (many codepoints, many donors, one call)
101
+
102
+ `include_codepoints_map(cp_map, into:)` takes a `{codepoint => donor}`
103
+ map and forwards each donor group to `include_codepoints` after sorting
104
+ the codepoints (reproducible GID assignment). Useful when the cp →
105
+ donor routing comes from an external plan (Unicode block table,
106
+ partitioner output, coverage analysis).
107
+
108
+ [source,ruby]
109
+ ----
110
+ cp_map = {
111
+ 0x41 => :latin,
112
+ 0x42 => :latin,
113
+ 0x4E00 => :cjk,
114
+ 0x4E01 => :cjk,
115
+ }
116
+ stitcher.include_codepoints_map(cp_map, into: :main)
117
+ # equivalent to:
118
+ # stitcher.include_codepoints([0x41, 0x42], from: :latin, into: :main)
119
+ # stitcher.include_codepoints([0x4E00, 0x4E01], from: :cjk, into: :main)
120
+ ----
121
+
122
+ == Codepoint partitioning (PartitionStrategy)
123
+
124
+ When the codepoint set is too large for one subfont (or when you want
125
+ to split by Unicode plane / block / script for organizational reasons),
126
+ `Stitcher::PartitionStrategy` provides ready-to-use partitioner classes
127
+ that produce a `Blueprint` (an ordered list of `Partition`s). Each
128
+ partition names a target subfont and carries its codepoints and
129
+ donor map.
130
+
131
+ The framework is open for new partitioners — adding one is a new file
132
+ under `lib/fontisan/stitcher/partition_strategy/` plus an autoload
133
+ entry. No edits to existing partitioners required (OCP).
134
+
135
+ === ByPlane
136
+
137
+ Groups codepoints by Unicode plane (`cp >> 16`). Each plane that fits
138
+ under the cap becomes one partition named `:plane_<n>` (e.g.
139
+ `:plane_0` for BMP, `:plane_2` for SIP). When a plane overflows the
140
+ cap, ByPlane sub-splits along the large CJK-extension block
141
+ boundaries (`CJK_Ext_B` through `CJK_Ext_F`), naming partitions
142
+ `:plane_<n>_a`, `:plane_<n>_b`, etc.
143
+
144
+ [source,ruby]
145
+ ----
146
+ require "fontisan/stitcher/partition_strategy"
147
+
148
+ cp_map = build_coverage_map # { codepoint => donor_label, ... }
149
+ blueprint = Fontisan::Stitcher::PartitionStrategy::ByPlane.new
150
+ .call(cp_map, cap: 65_484)
151
+
152
+ blueprint.names # => [:plane_0, :plane_2, ...]
153
+ blueprint.partitions # => [#<Partition name=:plane_0 cps=[...] ...>, ...]
154
+
155
+ # Push every partition's bindings into the Stitcher in one go:
156
+ declared = blueprint.apply_to(stitcher)
157
+ # declared == blueprint.names
158
+ ----
159
+
160
+ If a single CJK extension block alone exceeds `cap` (e.g. `CJK_Ext_B`
161
+ has ~6,592 codepoints and the cap is set to 5,000), ByPlane raises
162
+ `Fontisan::PartitionCapExceededError` — its codepoints are contiguous
163
+ and ByPlane has no finer-grained boundary to sub-split on. The caller
164
+ must either raise the cap, drop codepoints, or switch to a format
165
+ with a higher glyph limit.
166
+
167
+ The Unicode plane metadata used by ByPlane lives in
168
+ `Fontisan::Unicode::Plane` (`Plane.of(cp)`, `Plane.label(n)`,
169
+ `Plane::LARGE_CJK_BLOCKS`).
170
+
171
+ === Partition / Blueprint
172
+
173
+ A `Partition` is a `Struct.new(:name, :cps, :donor_map, keyword_init: true)`.
174
+ `Partition#apply_to(stitcher)` pushes its bindings via
175
+ `include_codepoints_map`.
176
+
177
+ A `Blueprint` is a `Struct.new(:partitions, keyword_init: true)`;
178
+ `Blueprint#apply_to(stitcher)` calls `apply_to` on each partition in
179
+ order and returns the list of declared subfont names.
180
+
181
+ === Future: ByBlock, ByScript
182
+
183
+ ByBlock (Unicode Blocks.txt, ~350 entries) and ByScript (Scripts.txt)
184
+ are tracked as a follow-up. The framework is already open for them:
185
+ add `by_block.rb` / `by_script.rb` under
186
+ `lib/fontisan/stitcher/partition_strategy/` and an autoload entry in
187
+ `partition_strategy.rb`.
188
+
189
+ == Per-subfont metadata (Ufo::Info.for_subfont)
190
+
191
+ `Fontisan::Ufo::Info.for_subfont(family:, subfont:, version:, ...)`
192
+ builds the standard name-table fields for one subfont of a collection.
193
+ The family name embeds the subfont name (e.g. `"MyFont CJK"`), and
194
+ the PostScript name uses the hyphenated form (e.g. `"MyFont-CJK"`).
195
+ `version` is parsed into `version_major` / `version_minor` per the
196
+ UFO major.minor shape (semver patch is dropped).
197
+
198
+ [source,ruby]
199
+ ----
200
+ info = Fontisan::Ufo::Info.for_subfont(
201
+ family: "essenfont",
202
+ subfont: :SIP,
203
+ version: "0.1",
204
+ )
205
+ info.family_name # => "essenfont SIP"
206
+ info.postscript_font_name # => "essenfont-SIP"
207
+ info.postscript_full_name # => "essenfont SIP"
208
+ info.version_major # => 0
209
+ info.version_minor # => 1
210
+ ----
211
+
212
+ Optional kwargs: `subfamily:` (default `"Regular"`), `copyright:`,
213
+ `trademark:`. `trademark` lands in `extras["openTypeNameTrademark"]`
214
+ because it is not in `Ufo::Info::STANDARD_FIELDS` yet — round-trip
215
+ still works through the extras channel.
216
+
80
217
  == Glyph deduplication
81
218
 
82
219
  By default the Stitcher deduplicates glyphs across all sources using
data/docs/cli/convert.md CHANGED
@@ -32,8 +32,8 @@ These are individual font formats that can be converted:
32
32
 
33
33
  | Option | Description |
34
34
  |--------|-------------|
35
- | `--to FORMAT` | Target format (ttf, otf, woff, woff2) |
36
- | `--output FILE` | Output file path |
35
+ | `--to FORMAT[,FORMAT...]` | Target format(s): `ttf`, `otf`, `woff`, `woff2`, `type1`/`t1`, `ttc`, `otc`, `dfont`, `svg`. Pass once with comma-separated values (`--to woff,woff2`) or multiple times (`--to woff --to woff2`) for multi-format output. |
36
+ | `--output FILE` | Output file path (see "Output path rules" below) |
37
37
  | `--optimize` | Enable outline optimization |
38
38
  | `--flatten` | Flatten composite glyphs |
39
39
  | `--zlib-level=N` | WOFF only: zlib compression level (0–9, default 6) |
@@ -46,6 +46,37 @@ The format you pick (`--to woff` vs `--to woff2`) **is** the algorithm
46
46
  choice — WOFF mandates zlib, WOFF2 mandates Brotli. Passing a WOFF knob
47
47
  to a WOFF2 target (or vice versa) exits 1 with a clear error.
48
48
 
49
+ ## Multi-format output
50
+
51
+ `--to` accepts multiple targets so a single invocation produces N
52
+ output files from one input font. Both spellings work and produce
53
+ identical results:
54
+
55
+ ```bash
56
+ # Comma-separated (one --to flag)
57
+ fontisan convert font.ttf --to woff,woff2 --output font
58
+
59
+ # Repeated flag (Thor array form)
60
+ fontisan convert font.ttf --to woff --to woff2 --output font
61
+ ```
62
+
63
+ Duplicates are deduplicated, so `--to woff,woff2,woff` is equivalent to
64
+ `--to woff,woff2`.
65
+
66
+ ### Output path rules
67
+
68
+ | `--output` shape | `--to` shape | Behaviour |
69
+ |------------------|--------------|-----------|
70
+ | `out.ttf` (has extension) | one format | Use as given |
71
+ | `out` (no extension) | one format | Append `.<format>` → `out.ttf` |
72
+ | `out` (no extension) | many formats | Append `.<format>` per target → `out.woff`, `out.woff2` |
73
+ | `out.ttf` (has extension) | many formats | **Error**: ambiguous which format gets the extension |
74
+
75
+ Multi-format is single-font → single-font only. Combining multi-format
76
+ with collection input (TTC/OTC/dfont) exits 1 — collections pack
77
+ multiple faces and N formats × M faces explodes output count. Convert
78
+ collections to one target format at a time.
79
+
49
80
  ## Common Workflows
50
81
 
51
82
  ### Convert for Web
@@ -85,6 +116,17 @@ fontisan convert font.otf --to ttf --output font.ttf
85
116
  fontisan convert font.pfb --to otf --output font.otf
86
117
  ```
87
118
 
119
+ ### Emit Multiple Web Formats in One Invocation
120
+
121
+ ```bash
122
+ # Both WOFF (legacy IE9+) and WOFF2 (modern browsers) from one input
123
+ fontisan convert font.ttf --to woff,woff2 --output font
124
+ # → font.woff, font.woff2
125
+
126
+ # Same result with the repeated-flag form
127
+ fontisan convert font.ttf --to woff --to woff2 --output font
128
+ ```
129
+
88
130
  ## Working with Collections
89
131
 
90
132
  Collection formats (TTC, OTC, dfont) contain multiple fonts. To convert fonts from a collection:
data/docs/cli/index.md CHANGED
@@ -20,18 +20,6 @@ Always check your font's End User License Agreement (EULA) before processing. Ma
20
20
 
21
21
  ## Quick Reference
22
22
 
23
- ### Font Audit
24
-
25
- | Command | Description | Example |
26
- |---------|-------------|---------|
27
- | `audit` | Comprehensive per-face audit (replaces `otfinfo`) | `fontisan audit font.ttf` |
28
-
29
- The `audit` command covers identity, style, metrics, codepoint coverage,
30
- licensing, hinting, color capabilities, variable-font detail, and
31
- OpenType layout — and supports collections, `--compare` mode, and
32
- whole-library `--recursive --summary` mode. See
33
- [audit](/cli/audit) for details.
34
-
35
23
  ### Font Information Commands
36
24
 
37
25
  | Command | Description | Example |
@@ -52,7 +40,8 @@ whole-library `--recursive --summary` mode. See
52
40
  |---------|-------------|---------|
53
41
  | `convert` | Convert between formats | `fontisan convert input.ttf --to otf` |
54
42
  | `subset` | Subset fonts | `fontisan subset font.ttf --chars "ABC"` |
55
- | `validate` | Validate fonts | `fontisan validate font.ttf` |
43
+ | `validate` | Validate single-font or per-face quality | `fontisan validate font.ttf` |
44
+ | `validate-collection` | Structural checks on a TTC/OTC/dfont | `fontisan validate-collection fonts.ttc --expected-faces 5` |
56
45
  | `instance` | Generate variable font instances | `fontisan instance var.ttf --wght 700` |
57
46
  | `dump-table` | Extract raw table data | `fontisan dump-table font.ttf head` |
58
47
 
@@ -132,6 +121,10 @@ fontisan unicode font.ttf
132
121
  ```bash
133
122
  # Convert single font to WOFF2
134
123
  fontisan convert font.ttf --to woff2 --output font.woff2
124
+
125
+ # Emit both WOFF and WOFF2 in one invocation
126
+ fontisan convert font.ttf --to woff,woff2 --output font
127
+ # → font.woff, font.woff2
135
128
  ```
136
129
 
137
130
  ### Work with Variable Fonts
@@ -177,21 +170,23 @@ fontisan validate font.ttf --profile google_fonts
177
170
  fontisan validate font.ttf --profile production
178
171
  ```
179
172
 
180
- ### Audit a Font (or Library)
173
+ ### Validate Collection Structure
181
174
 
182
175
  ```bash
183
- # Full per-face audit
184
- fontisan audit font.ttf
176
+ # Glyph-cap-only check (default behaviour)
177
+ fontisan validate-collection family.ttc
185
178
 
186
- # Diff two fonts (or saved reports)
187
- fontisan audit --compare baseline.yaml new.ttf
179
+ # Require exactly 5 faces, max 60,000 glyphs per face
180
+ fontisan validate-collection family.ttc --expected-faces 5 --max-glyphs 60000
181
+ ```
188
182
 
189
- # Whole-library summary
190
- fontisan audit lib/ --recursive --summary
183
+ ### Audit a Font (or Library)
191
184
 
192
- # Fast inventory pass
193
- fontisan audit lib/ --recursive --summary --brief
194
- ```
185
+ The `audit` and `ucd` commands have been removed from fontisan. The
186
+ audit/UCD subsystem moved to the
187
+ [ucode](https://github.com/fontist/ucode) gem. For per-face font
188
+ metadata, use [`info`](/cli/info); for pass/fail quality checks, use
189
+ [`validate`](/cli/validate).
195
190
 
196
191
  ### Export Font Data
197
192
 
@@ -210,9 +205,6 @@ fontisan export font.ttf --format ttx --tables head,name,cmap
210
205
 
211
206
  Detailed documentation for each command:
212
207
 
213
- ### Font Audit
214
- - [audit](/cli/audit) — Comprehensive per-face audit (replaces `otfinfo`); supports collections, compare, library summary
215
-
216
208
  ### Font Information
217
209
  - [info](/cli/info) — Extract font metadata and properties (includes brief mode)
218
210
  - [ls](/cli/ls) — List fonts in collections
@@ -225,9 +217,10 @@ Detailed documentation for each command:
225
217
  - [optical-size](/cli/optical-size) — Display optical size information
226
218
 
227
219
  ### Font Operations
228
- - [convert](/cli/convert) — Format conversion (TTF, OTF, WOFF, WOFF2)
220
+ - [convert](/cli/convert) — Format conversion (TTF, OTF, WOFF, WOFF2); supports multi-format output
229
221
  - [subset](/cli/subset) — Create character subsets
230
- - [validate](/cli/validate) — Validate fonts against profiles
222
+ - [validate](/cli/validate) — Per-face profile validation
223
+ - [validate-collection](/cli/validate-collection) — Structural checks on TTC/OTC/dfont (face count, glyph cap, cmap union)
231
224
  - [instance](/cli/instance) — Generate static instances from variable fonts
232
225
  - [export](/cli/export) — Export to TTX, YAML, JSON
233
226
  - [dump-table](/cli/dump-table) — Extract raw binary table data
@@ -0,0 +1,95 @@
1
+ ---
2
+ title: validate-collection
3
+ ---
4
+
5
+ # validate-collection
6
+
7
+ Validate the structural integrity of a TTC / OTC / dfont collection:
8
+ face count, per-face glyph cap, and optional cmap-union size.
9
+
10
+ This is a collection-level counterpart to
11
+ [validate](/cli/validate), which runs profile-based per-face quality
12
+ checks (OpenType compliance, hint validity, etc.). Use
13
+ `validate-collection` when you care about the *shape* of the
14
+ collection (number of faces, glyph-cap overflows, codepoint coverage)
15
+ rather than per-face quality.
16
+
17
+ ## Quick Reference
18
+
19
+ ```bash
20
+ fontisan validate-collection <path> [options]
21
+ ```
22
+
23
+ The command exits `0` when every requested check passes and `1` when
24
+ any check fails. With no options, only the per-face glyph cap is
25
+ checked (default `65,535`).
26
+
27
+ ## Options
28
+
29
+ | Option | Default | Description |
30
+ |--------|---------|-------------|
31
+ | `--expected-faces N` | (none) | Require exactly `N` faces in the collection. |
32
+ | `--max-glyphs N` | `65535` | Per-face glyph cap (`maxp.numGlyphs`). Faces over this cap fail. |
33
+ | `--expected-cmap-union N` | (none) | Require the union of cmap codepoints across all faces to be at least `N`. |
34
+
35
+ ## Examples
36
+
37
+ ```bash
38
+ # Glyph-cap-only check (default behaviour, no optional checks)
39
+ fontisan validate-collection family.ttc
40
+
41
+ # Require exactly 5 faces
42
+ fontisan validate-collection family.ttc --expected-faces 5
43
+
44
+ # Tighter per-face glyph cap
45
+ fontisan validate-collection family.ttc --max-glyphs 60000
46
+
47
+ # Require at least 100,000 codepoints covered across all faces
48
+ fontisan validate-collection family.ttc --expected-cmap-union 100000
49
+ ```
50
+
51
+ ## Sample output
52
+
53
+ ```
54
+ face 0: 3541 glyphs ✓
55
+ face 1: 21048 glyphs ✓
56
+ face 2: 991 glyphs ✓
57
+ all 3 faces within 65535-glyph cap ✓
58
+ face_count: ✓
59
+ cmap union: 24580 cps ✓
60
+ ```
61
+
62
+ When a check fails, the marker becomes `✗` and a message is printed
63
+ in parentheses; the exit code is `1`.
64
+
65
+ ## Programmatic access
66
+
67
+ The command is a thin wrapper around
68
+ `Fontisan::Collection::Reader`, which exposes per-face metadata
69
+ directly:
70
+
71
+ ```ruby
72
+ reader = Fontisan::Collection::Reader.open("family.ttc")
73
+ reader.face_count # => 3
74
+ reader.stats.map(&:glyph_count) # => [3541, 21048, 991]
75
+ reader.cmap_union.size # => 24580
76
+ ```
77
+
78
+ Use the command-class directly when you want structured results
79
+ without stdout:
80
+
81
+ ```ruby
82
+ cmd = Fontisan::Commands::ValidateCollectionCommand.new(
83
+ input: "family.ttc",
84
+ expected_faces: 3,
85
+ )
86
+ exit_code = cmd.run # 0 or 1
87
+ cmd.checks # => [#<Check name=:face_count passed=true ...>, ...]
88
+ ```
89
+
90
+ ## See also
91
+
92
+ - [validate](/cli/validate) — per-face profile-based checks (OpenType
93
+ compliance, hint validity, etc.)
94
+ - [pack](/cli/pack) / [unpack](/cli/pack) — create and extract
95
+ collections