fontisan 0.4.8 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a519f7a53629decc2b968e9c99b5979459fe33c49750aa851818a330e6fd6af
4
- data.tar.gz: 805f840f58f042288c34610fa355b1be74b428a8669abc15188c08e635494148
3
+ metadata.gz: 38b49343b74d1876d57ec13992fc49fdc2879a44d1f0fe3c6529bd3a2f8f7a75
4
+ data.tar.gz: cc69156566e86294177d9046f39ebde8334f87807da38a7a5dd83ddd2eea8247
5
5
  SHA512:
6
- metadata.gz: 06edc2ca7cf97a919095afee9b5bc7327d0f01f4ce304c537f1ac4a920dd7950cce67beaba34d0bf93aa99d6cb21c385ad5238c8077d7aebd87545a8c35437eb
7
- data.tar.gz: 8a43a243ad4aeae715f63bf94c5e907d07b2330f59b4b37f3bfafa1ee8af063796c8ab84547f92b674f46340bfb5c96686e7b25b88f490f18ff77848c7c30d6a
6
+ metadata.gz: 2822ae02861c4183bbd42ded8005fa17d28d59175155767413f94b4400faaf28fb61dad38736f3fcd11235746e1b254151c8e8918480b5ccb146882742c3ec99
7
+ data.tar.gz: eefb0dbf8e3360e7b8aeb89dfcfbf62425c255075ee1a3d2c030cd2551f90f08d980f7ea7b8c56c382b0a1cb69933654a57fc4e57d069ebe6809ace84e7d0b42
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)
@@ -285,6 +285,7 @@ export default defineConfig({
285
285
  { text: "convert", link: "/cli/convert" },
286
286
  { text: "subset", link: "/cli/subset" },
287
287
  { text: "validate", link: "/cli/validate" },
288
+ { text: "validate-collection", link: "/cli/validate-collection" },
288
289
  { text: "instance", link: "/cli/instance" },
289
290
  { text: "export", link: "/cli/export" },
290
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
@@ -40,7 +40,8 @@ Always check your font's End User License Agreement (EULA) before processing. Ma
40
40
  |---------|-------------|---------|
41
41
  | `convert` | Convert between formats | `fontisan convert input.ttf --to otf` |
42
42
  | `subset` | Subset fonts | `fontisan subset font.ttf --chars "ABC"` |
43
- | `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` |
44
45
  | `instance` | Generate variable font instances | `fontisan instance var.ttf --wght 700` |
45
46
  | `dump-table` | Extract raw table data | `fontisan dump-table font.ttf head` |
46
47
 
@@ -120,6 +121,10 @@ fontisan unicode font.ttf
120
121
  ```bash
121
122
  # Convert single font to WOFF2
122
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
123
128
  ```
124
129
 
125
130
  ### Work with Variable Fonts
@@ -165,6 +170,16 @@ fontisan validate font.ttf --profile google_fonts
165
170
  fontisan validate font.ttf --profile production
166
171
  ```
167
172
 
173
+ ### Validate Collection Structure
174
+
175
+ ```bash
176
+ # Glyph-cap-only check (default behaviour)
177
+ fontisan validate-collection family.ttc
178
+
179
+ # Require exactly 5 faces, max 60,000 glyphs per face
180
+ fontisan validate-collection family.ttc --expected-faces 5 --max-glyphs 60000
181
+ ```
182
+
168
183
  ### Audit a Font (or Library)
169
184
 
170
185
  The `audit` and `ucd` commands have been removed from fontisan. The
@@ -202,9 +217,10 @@ Detailed documentation for each command:
202
217
  - [optical-size](/cli/optical-size) — Display optical size information
203
218
 
204
219
  ### Font Operations
205
- - [convert](/cli/convert) — Format conversion (TTF, OTF, WOFF, WOFF2)
220
+ - [convert](/cli/convert) — Format conversion (TTF, OTF, WOFF, WOFF2); supports multi-format output
206
221
  - [subset](/cli/subset) — Create character subsets
207
- - [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)
208
224
  - [instance](/cli/instance) — Generate static instances from variable fonts
209
225
  - [export](/cli/export) — Export to TTX, YAML, JSON
210
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
data/docs/cli/validate.md CHANGED
@@ -46,3 +46,11 @@ fontisan validate font.ttf --profile opentype --format json
46
46
  ## Detailed Documentation
47
47
 
48
48
  For comprehensive documentation including profile details and validation helpers, see the [validate command guide](/guide/cli/validate).
49
+
50
+ ## See also
51
+
52
+ For collection-level structural checks (face count, per-face glyph cap,
53
+ cmap-union size) on a TTC/OTC/dfont, see
54
+ [validate-collection](/cli/validate-collection). The two commands are
55
+ complementary: `validate` runs profile-based per-face checks;
56
+ `validate-collection` runs collection-level structural checks.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.4.8"
4
+ VERSION = "0.4.9"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fontisan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.8
4
+ version: 0.4.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -188,6 +188,7 @@ files:
188
188
  - docs/cli/subset.md
189
189
  - docs/cli/tables.md
190
190
  - docs/cli/unicode.md
191
+ - docs/cli/validate-collection.md
191
192
  - docs/cli/validate.md
192
193
  - docs/cli/variable.md
193
194
  - docs/cli/version.md