fontisan 0.4.6 → 0.4.8

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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/BUG-stitcher-drops-isolated-cps.md +58 -0
  3. data/BUG-stitcher-drops-plane1-codepoints.md +310 -0
  4. data/BUG-stitcher-gid-cap-65535.md +110 -0
  5. data/CHANGELOG.md +119 -0
  6. data/README.adoc +121 -68
  7. data/benchmark/compile_benchmark.rb +70 -0
  8. data/docs/.vitepress/config.ts +0 -7
  9. data/docs/CFF2_SUPPORT.adoc +184 -0
  10. data/docs/STITCHER_GUIDE.adoc +151 -0
  11. data/docs/SVG_TO_GLYF.adoc +118 -0
  12. data/docs/UFO_COMPILATION.adoc +119 -0
  13. data/docs/cli/index.md +5 -28
  14. data/docs/index.md +0 -2
  15. data/lib/fontisan/cli.rb +29 -8
  16. data/lib/fontisan/collection/reader/stats.rb +23 -0
  17. data/lib/fontisan/collection/reader.rb +90 -0
  18. data/lib/fontisan/collection/writer.rb +5 -6
  19. data/lib/fontisan/collection.rb +1 -0
  20. data/lib/fontisan/commands/convert_command.rb +96 -18
  21. data/lib/fontisan/commands/multi_format_output.rb +59 -0
  22. data/lib/fontisan/commands/validate_collection_command.rb +121 -0
  23. data/lib/fontisan/commands.rb +2 -0
  24. data/lib/fontisan/error.rb +56 -0
  25. data/lib/fontisan/models.rb +0 -1
  26. data/lib/fontisan/stitcher/collection_result.rb +18 -0
  27. data/lib/fontisan/stitcher/deduplicator.rb +47 -0
  28. data/lib/fontisan/stitcher/glyph_limit.rb +53 -0
  29. data/lib/fontisan/stitcher/glyph_signature.rb +51 -0
  30. data/lib/fontisan/stitcher/partition_strategy/base.rb +23 -0
  31. data/lib/fontisan/stitcher/partition_strategy/blueprint.rb +24 -0
  32. data/lib/fontisan/stitcher/partition_strategy/by_plane.rb +131 -0
  33. data/lib/fontisan/stitcher/partition_strategy/partition.rb +24 -0
  34. data/lib/fontisan/stitcher/partition_strategy.rb +22 -0
  35. data/lib/fontisan/stitcher.rb +223 -168
  36. data/lib/fontisan/svg_to_glyf/assembler.rb +132 -0
  37. data/lib/fontisan/svg_to_glyf/document.rb +83 -0
  38. data/lib/fontisan/svg_to_glyf/geometry/affine_transform.rb +112 -0
  39. data/lib/fontisan/svg_to_glyf/geometry/normalizer.rb +45 -0
  40. data/lib/fontisan/svg_to_glyf/geometry/transform_parser.rb +91 -0
  41. data/lib/fontisan/svg_to_glyf/geometry.rb +13 -0
  42. data/lib/fontisan/svg_to_glyf/path/command.rb +18 -0
  43. data/lib/fontisan/svg_to_glyf/path/contour_builder.rb +140 -0
  44. data/lib/fontisan/svg_to_glyf/path/parser.rb +98 -0
  45. data/lib/fontisan/svg_to_glyf/path/state.rb +79 -0
  46. data/lib/fontisan/svg_to_glyf/path.rb +14 -0
  47. data/lib/fontisan/svg_to_glyf.rb +62 -0
  48. data/lib/fontisan/tables/cff/cff2_charstring_builder.rb +216 -0
  49. data/lib/fontisan/tables/cff.rb +1 -0
  50. data/lib/fontisan/tables/cff2/dict_encoder.rb +94 -0
  51. data/lib/fontisan/tables/cff2/fd_select.rb +69 -0
  52. data/lib/fontisan/tables/cff2/header.rb +34 -0
  53. data/lib/fontisan/tables/cff2/index_builder.rb +79 -0
  54. data/lib/fontisan/tables/cff2.rb +4 -0
  55. data/lib/fontisan/ufo/compile/cbdt_cblc.rb +103 -0
  56. data/lib/fontisan/ufo/compile/cff2.rb +181 -0
  57. data/lib/fontisan/ufo/compile/cff2_subroutines.rb +39 -0
  58. data/lib/fontisan/ufo/compile/colr.rb +80 -0
  59. data/lib/fontisan/ufo/compile/cpal.rb +61 -0
  60. data/lib/fontisan/ufo/compile/math.rb +143 -0
  61. data/lib/fontisan/ufo/compile/meta.rb +51 -0
  62. data/lib/fontisan/ufo/compile/name.rb +2 -2
  63. data/lib/fontisan/ufo/compile/otf2_compiler.rb +46 -0
  64. data/lib/fontisan/ufo/compile/sbix.rb +99 -0
  65. data/lib/fontisan/ufo/compile/svg_table.rb +60 -0
  66. data/lib/fontisan/ufo/compile/variable_otf.rb +75 -0
  67. data/lib/fontisan/ufo/compile.rb +11 -0
  68. data/lib/fontisan/ufo/info.rb +48 -0
  69. data/lib/fontisan/unicode/plane.rb +56 -0
  70. data/lib/fontisan/unicode.rb +17 -0
  71. data/lib/fontisan/version.rb +1 -1
  72. data/lib/fontisan.rb +5 -2
  73. metadata +53 -19
  74. data/docs/cli/audit.md +0 -337
  75. data/lib/fontisan/cldr/aggregator.rb +0 -33
  76. data/lib/fontisan/cldr/cache_manager.rb +0 -110
  77. data/lib/fontisan/cldr/config.rb +0 -59
  78. data/lib/fontisan/cldr/download_error.rb +0 -9
  79. data/lib/fontisan/cldr/downloader.rb +0 -79
  80. data/lib/fontisan/cldr/error.rb +0 -8
  81. data/lib/fontisan/cldr/index.rb +0 -64
  82. data/lib/fontisan/cldr/index_builder.rb +0 -72
  83. data/lib/fontisan/cldr/unicode_set_parser.rb +0 -189
  84. data/lib/fontisan/cldr/unknown_version_error.rb +0 -9
  85. data/lib/fontisan/cldr/version_resolver.rb +0 -91
  86. data/lib/fontisan/cldr.rb +0 -23
  87. data/lib/fontisan/cli/cldr_cli.rb +0 -85
  88. data/lib/fontisan/config/cldr.yml +0 -22
  89. data/lib/fontisan/models/cldr/language_coverage.rb +0 -31
  90. data/lib/fontisan/models/cldr.rb +0 -12
@@ -0,0 +1,118 @@
1
+ = SVG to Glyph Guide
2
+
3
+ == General
4
+
5
+ `Fontisan::SvgToGlyf` converts SVG path data — as produced by ucode's
6
+ code-chart extraction — into `Ufo::Glyph` objects that feed directly
7
+ into the UFO → TTF/OTF compile pipeline.
8
+
9
+ SvgToGlyf emits cubic Bezier contours (matching UFO/PostScript
10
+ conventions). The cubic-to-quadratic conversion and winding-order
11
+ correction happen later, in `Ufo::Compile::Filters::CubicToQuadratic`
12
+ and `ReverseContourDirection`, when the glyph is compiled to TTF.
13
+
14
+ == API
15
+
16
+ [cols="2,3", options="header"]
17
+ |===
18
+ | Method | Purpose
19
+
20
+ | `SvgToGlyf.convert(path_data, upm:, codepoint:, name:, viewbox:, transform:)`
21
+ | Convert a single SVG path `d` string into a `Ufo::Glyph`
22
+
23
+ | `SvgToGlyf.from_svg_file(file_path, upm:, codepoint:)`
24
+ | Convert one `.svg` file into a `Ufo::Glyph`
25
+
26
+ | `SvgToGlyf.from_directory(dir, upm:)`
27
+ | Convert a directory of `.svg` files into a `Ufo::Font` (one glyph
28
+ per file)
29
+ |===
30
+
31
+ The default `upm` is 1000.
32
+
33
+ == Single path
34
+
35
+ [source,ruby]
36
+ ----
37
+ glyph = Fontisan::SvgToGlyf.convert(
38
+ "M 0 0 L 500 0 L 500 700 L 0 700 Z",
39
+ upm: 1000,
40
+ codepoint: 0x10940,
41
+ )
42
+ # → #<Fontisan::Ufo::Glyph name="uni10940" ...>
43
+ ----
44
+
45
+ == Single file
46
+
47
+ [source,ruby]
48
+ ----
49
+ glyph = Fontisan::SvgToGlyf.from_svg_file("U+10940.svg", upm: 1000)
50
+ ----
51
+
52
+ If the codepoint is not supplied, it is derived from the filename when
53
+ it matches `U+XXXX.svg` or `hexcode.svg`.
54
+
55
+ == Directory → Font
56
+
57
+ For batch conversion of a code-chart directory (one SVG per
58
+ codepoint):
59
+
60
+ [source,ruby]
61
+ ----
62
+ font = Fontisan::SvgToGlyf.from_directory(
63
+ "/tmp/chart-svg/Khitan",
64
+ upm: 1000,
65
+ )
66
+ Fontisan::Ufo::Compile::TtfCompiler.new(font)
67
+ .compile(output_path: "Khitan.ttf")
68
+ ----
69
+
70
+ Or feed it directly to the Stitcher:
71
+
72
+ [source,ruby]
73
+ ----
74
+ stitcher = Fontisan::Stitcher.new
75
+ stitcher.add_source(:khitan, font)
76
+ stitcher.include_range(0x18B00..0x18CFF, from: :khitan, into: :main)
77
+ stitcher.write_to("Khitan.ttf", format: :ttf, subfont: :main)
78
+ ----
79
+
80
+ == SVG subset supported
81
+
82
+ SvgToGlyf handles the subset of SVG that real chart extractions use:
83
+
84
+ * Path commands: `M`, `L`, `H`, `V`, `C`, `S`, `Q`, `T`, `A`, `Z`
85
+ (absolute and lowercase relative variants)
86
+ * `<svg>` `viewBox` and `width`/`height` for coordinate scaling
87
+ * `<g transform="...">` group transforms (matrix, translate, scale,
88
+ rotate)
89
+ * `<path d="...">` elements
90
+
91
+ Unsupported SVG features (filters, masks, embedded raster images) are
92
+ ignored. SvgToGlyf is a vector-only converter — for raster emoji, use
93
+ the CBDT/CBLC passthrough path (see link:STITCHER_GUIDE.adoc[Stitcher Guide]).
94
+
95
+ == Coordinate system
96
+
97
+ SVG uses a y-down coordinate system; UFO uses y-up. SvgToGlyf flips
98
+ the y-axis using the SVG `viewBox` height so glyphs render right-way-up
99
+ in font rendering contexts. When no `viewBox` is present, the
100
+ `<svg>` `width`/`height` attributes are used as a fallback.
101
+
102
+ == Namespace layout
103
+
104
+ [source]
105
+ ----
106
+ Fontisan::SvgToGlyf
107
+ ├── Path # SVG path parser → contour builder
108
+ ├── Geometry # AffineTransform, vector math
109
+ ├── Document # <svg> document parsing, viewBox handling
110
+ └── Assembler # top-level glue: builds Ufo::Glyph / Ufo::Font
111
+ ----
112
+
113
+ == See also
114
+
115
+ * link:UFO_COMPILATION.adoc[UFO Compilation Guide] — compile converted
116
+ glyphs to binary fonts
117
+ * link:STITCHER_GUIDE.adoc[Stitcher Guide] — combine converted fonts
118
+ with other donors
@@ -0,0 +1,119 @@
1
+ = UFO Compilation Guide
2
+
3
+ == General
4
+
5
+ Fontisan compiles UFO (Unified Font Object) v2/v3 sources into binary
6
+ OpenType fonts — TTF, OTF (CFF1), and OTF2 (CFF2) — entirely in pure
7
+ Ruby. No AFDKO or Python fonttools required.
8
+
9
+ All compilers live under `Fontisan::Ufo::Compile` and share a common
10
+ `BaseCompiler` orchestrator that emits the required set of OpenType
11
+ tables (head, hhea, maxp, OS/2, name, post, hmtx, cmap, plus an
12
+ optional GPOS kern table when the source has kerning data). Subclasses
13
+ add format-specific outline tables.
14
+
15
+ == Compilers
16
+
17
+ [cols="1,1,3", options="header"]
18
+ |===
19
+ | Class | Output | Outline format
20
+
21
+ | `Ufo::Compile::TtfCompiler`
22
+ | `.ttf`
23
+ | Quadratic Bezier curves in the `glyf` table
24
+
25
+ | `Ufo::Compile::OtfCompiler`
26
+ | `.otf` (CFF1)
27
+ | Cubic Bezier curves in the `CFF ` table using Type 2 charstrings
28
+
29
+ | `Ufo::Compile::Otf2Compiler`
30
+ | `.otf` (CFF2)
31
+ | Cubic Bezier curves in the `CFF2` table — modern format, supports
32
+ variable fonts via blend/vsindex operators
33
+ |===
34
+
35
+ == Building a single font
36
+
37
+ [source,ruby]
38
+ ----
39
+ require "fontisan"
40
+
41
+ font = Fontisan::Ufo::Font.open("MyFont.ufo")
42
+
43
+ # TrueType
44
+ Fontisan::Ufo::Compile::TtfCompiler.new(font)
45
+ .compile(output_path: "MyFont.ttf")
46
+
47
+ # OpenType / CFF1
48
+ Fontisan::Ufo::Compile::OtfCompiler.new(font)
49
+ .compile(output_path: "MyFont.otf")
50
+
51
+ # OpenType / CFF2 (modern, supports variable fonts)
52
+ Fontisan::Ufo::Compile::Otf2Compiler.new(font)
53
+ .compile(output_path: "MyFont-CFF2.otf")
54
+ ----
55
+
56
+ == TTF filters
57
+
58
+ The TTF compiler applies two UFO glyph filters automatically:
59
+
60
+ 1. `CubicToQuadratic` — converts cubic Bezier control points to
61
+ quadratic. TTF `glyf` only supports quadratic curves; UFO sources
62
+ typically use cubics (matching PostScript/CFF conventions).
63
+ 2. `ReverseContourDirection` — reverses winding order. CFF and TTF
64
+ use opposite fill conventions (CFF: non-zero wind; TTF: even-odd by
65
+ default), so contour direction must be flipped.
66
+
67
+ These filters live under `Ufo::Compile::Filters` and are applied to
68
+ each glyph before serialization to `glyf`.
69
+
70
+ == Tables emitted
71
+
72
+ Every compiler emits this shared set:
73
+
74
+ [cols="1,3", options="header"]
75
+ |===
76
+ | Tag | Purpose
77
+
78
+ | `head` | Font header (units per em, bounding box, timestamps)
79
+ | `hhea` | Horizontal header (ascender, descender, line gap)
80
+ | `maxp` | Maximum profile (glyph count, point/contour limits)
81
+ | `OS/2` | OS/2 and Windows-specific metrics
82
+ | `name` | Name records (family, subfamily, copyright, etc.)
83
+ | `post` | PostScript name table (glyph names)
84
+ | `hmtx` | Horizontal metrics (advance width, left side bearing)
85
+ | `cmap` | Character to glyph mapping
86
+ | `GPOS` | Optional — present only when the UFO has kerning data
87
+ |===
88
+
89
+ Format-specific outline tables:
90
+
91
+ * TTF → `glyf` + `loca`
92
+ * OTF (CFF1) → `CFF `
93
+ * OTF2 (CFF2) → `CFF2`
94
+
95
+ == GPOS kerning
96
+
97
+ If the UFO source includes kerning data (`kerning.plist`), the compiler
98
+ emits a `GPOS` table with a pair-positioning lookup (feature `kern`).
99
+ When no kerning is present, no `GPOS` table is written — the file stays
100
+ smaller.
101
+
102
+ == Round-tripping binary ↔ UFO
103
+
104
+ To go the other way — extract a UFO from an existing binary font:
105
+
106
+ [source,ruby]
107
+ ----
108
+ ttf = Fontisan::FontLoader.load("MyFont.ttf")
109
+ ufo = Fontisan::Ufo::Convert::FromBinData.convert(ttf)
110
+ Fontisan::Ufo::Writer.new(ufo).write("MyFont.ufo")
111
+ ----
112
+
113
+ == See also
114
+
115
+ * link:CFF2_SUPPORT.adoc[CFF2 Support Guide] — variable-font blend
116
+ operators, FDSelect, subroutines, VariationStore
117
+ * link:VARIABLE_FONT_OPERATIONS.adoc[Variable Font Operations] —
118
+ reading and modifying existing variable fonts
119
+ - link:STITCHER_GUIDE.adoc[Stitcher Guide] — multi-source assembly
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 |
@@ -179,19 +167,11 @@ fontisan validate font.ttf --profile production
179
167
 
180
168
  ### Audit a Font (or Library)
181
169
 
182
- ```bash
183
- # Full per-face audit
184
- fontisan audit font.ttf
185
-
186
- # Diff two fonts (or saved reports)
187
- fontisan audit --compare baseline.yaml new.ttf
188
-
189
- # Whole-library summary
190
- fontisan audit lib/ --recursive --summary
191
-
192
- # Fast inventory pass
193
- fontisan audit lib/ --recursive --summary --brief
194
- ```
170
+ The `audit` and `ucd` commands have been removed from fontisan. The
171
+ audit/UCD subsystem moved to the
172
+ [ucode](https://github.com/fontist/ucode) gem. For per-face font
173
+ metadata, use [`info`](/cli/info); for pass/fail quality checks, use
174
+ [`validate`](/cli/validate).
195
175
 
196
176
  ### Export Font Data
197
177
 
@@ -210,9 +190,6 @@ fontisan export font.ttf --format ttx --tables head,name,cmap
210
190
 
211
191
  Detailed documentation for each command:
212
192
 
213
- ### Font Audit
214
- - [audit](/cli/audit) — Comprehensive per-face audit (replaces `otfinfo`); supports collections, compare, library summary
215
-
216
193
  ### Font Information
217
194
  - [info](/cli/info) — Extract font metadata and properties (includes brief mode)
218
195
  - [ls](/cli/ls) — List fonts in collections
data/docs/index.md CHANGED
@@ -18,8 +18,6 @@ hero:
18
18
  link: /guide/migrations/fonttools
19
19
 
20
20
  features:
21
- - title: "🔍 Font Audit"
22
- details: Comprehensive per-face audit (replaces otfinfo) — identity, style, metrics, coverage, licensing, hinting, color, variation, OpenType layout, UCD/CLDR aggregation, compare, and library-summary modes.
23
21
  - title: "🔄 Font Conversion"
24
22
  details: Convert between TTF, OTF, WOFF, WOFF2, Type 1 (PFB/PFA), and SVG formats with curve conversion and optimization.
25
23
  - title: "✅ Font Validation"
data/lib/fontisan/cli.rb CHANGED
@@ -25,9 +25,6 @@ module Fontisan
25
25
  desc: "Suppress non-error output",
26
26
  aliases: "-q"
27
27
 
28
- desc "cldr", "Manage local CLDR cache (subcommands)", hide: true
29
- subcommand "cldr", CldrCli
30
-
31
28
  desc "ufo", "UFO source operations (build, convert, validate)"
32
29
  subcommand "ufo", Fontisan::Ufo::Cli
33
30
 
@@ -211,10 +208,12 @@ module Fontisan
211
208
  end
212
209
 
213
210
  desc "convert FONT_FILE", "Convert font to different format"
214
- option :to, type: :string, required: true,
215
- desc: "Target format: ttf, otf, type1, t1, ttc, otc, dfont, svg, " \
211
+ option :to, type: :array, required: true,
212
+ desc: "Target format(s): ttf, otf, type1, t1, ttc, otc, dfont, svg, " \
216
213
  "woff (zlib — works on all browsers incl. legacy), " \
217
- "woff2 (Brotli — ~30% smaller, modern browsers only)",
214
+ "woff2 (Brotli — ~30% smaller, modern browsers only). " \
215
+ "Pass multiple times (--to woff --to woff2) or comma-separated " \
216
+ "(--to woff,woff2) for multi-format output.",
218
217
  aliases: "-t"
219
218
  option :output, type: :string,
220
219
  desc: "Output file path (required unless --show-options)",
@@ -361,7 +360,7 @@ module Fontisan
361
360
 
362
361
  # Handle --show-options
363
362
  if options[:show_options]
364
- show_recommended_options(source_format, options[:to])
363
+ show_recommended_options(source_format, options[:to].is_a?(Array) ? options[:to].first : options[:to])
365
364
  return
366
365
  end
367
366
 
@@ -371,7 +370,8 @@ module Fontisan
371
370
  end
372
371
 
373
372
  # Build ConversionOptions
374
- conv_options = build_conversion_options(source_format, options[:to],
373
+ conv_options = build_conversion_options(source_format,
374
+ Array(options[:to]).first,
375
375
  options)
376
376
 
377
377
  # Build instance coordinates from axis options
@@ -573,6 +573,27 @@ module Fontisan
573
573
  exit command.run
574
574
  end
575
575
 
576
+ desc "validate-collection PATH",
577
+ "Validate a TTC/OTC/dfont (face count, glyph cap, cmap union)"
578
+ option :expected_faces, type: :numeric,
579
+ desc: "Required face count"
580
+ option :max_glyphs, type: :numeric, default: Fontisan::Commands::ValidateCollectionCommand::DEFAULT_MAX_GLYPHS,
581
+ desc: "Per-face glyph cap"
582
+ option :expected_cmap_union, type: :numeric,
583
+ desc: "Minimum cmap union size across all faces"
584
+ def validate_collection(path)
585
+ cmd = Commands::ValidateCollectionCommand.new(
586
+ input: path,
587
+ expected_faces: options[:expected_faces],
588
+ max_glyphs: options[:max_glyphs],
589
+ expected_cmap_union: options[:expected_cmap_union],
590
+ )
591
+ exit cmd.run
592
+ rescue ArgumentError => e
593
+ warn "ERROR: #{e.message}"
594
+ exit 1
595
+ end
596
+
576
597
  desc "version", "Display version information"
577
598
  # Display the Fontisan version.
578
599
  def version
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Collection
5
+ class Reader
6
+ # Snapshot of one face's headline metrics. Pure value object —
7
+ # no methods beyond accessors. Separate file from {Reader} so
8
+ # the Stats struct can be referenced without pulling in the
9
+ # full Reader (and its FontLoader dependency).
10
+ #
11
+ # @!attribute [r] index
12
+ # @return [Integer] 0-based face index inside the collection
13
+ # @!attribute [r] glyph_count
14
+ # @return [Integer] face's maxp.numGlyphs
15
+ # @!attribute [r] codepoint_count
16
+ # @return [Integer] count of keys in face's cmap unicode_mappings
17
+ # @!attribute [r] sfnt_version
18
+ # @return [Integer] face sfnt version (0x00010000 for TTF, 0x4F54544F for OTF)
19
+ Stats = Struct.new(:index, :glyph_count, :codepoint_count, :sfnt_version,
20
+ keyword_init: true)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Collection
5
+ # Read-only counterpart to {Builder}. Opens an existing TTC/OTC/dfont
6
+ # and exposes per-face metadata (glyph count, codepoint count, sfnt
7
+ # version) and the cmap union across all faces.
8
+ #
9
+ # Delegates header parsing to {FontLoader} — never hand-rolls the
10
+ # TTC header bytes (that would duplicate BaseCollection.from_file
11
+ # and only handle TTC/OTC, not dfont).
12
+ #
13
+ # @example Per-face glyph counts
14
+ # reader = Collection::Reader.open("family.ttc")
15
+ # reader.stats.map { |s| [s.index, s.glyph_count] }
16
+ #
17
+ # @example Cmap union
18
+ # Collection::Reader.open("family.otc").cmap_union.size
19
+ class Reader
20
+ autoload :Stats, "fontisan/collection/reader/stats"
21
+
22
+ # @!attribute [r] path
23
+ # @return [String] the file path this reader was opened with
24
+ attr_reader :path
25
+
26
+ # Collection formats this reader accepts. Single source of truth —
27
+ # matches {FontLoader::COLLECTION_CLASSES}.
28
+ FORMATS = %i[ttc otc dfont].freeze
29
+
30
+ # @param path [String] path to a TTC/OTC/dfont file
31
+ # @raise [ArgumentError] if the file is not a recognized collection
32
+ def initialize(path)
33
+ @path = path
34
+ detected = FontLoader.detect_format(path)
35
+ unless FORMATS.include?(detected)
36
+ raise ArgumentError,
37
+ "#{path} is not a TTC/OTC/dfont (detected: #{detected.inspect})"
38
+ end
39
+
40
+ @collection = FontLoader.load_collection(path)
41
+ end
42
+
43
+ # Convenience constructor.
44
+ # @param path [String]
45
+ # @return [Reader]
46
+ def self.open(path)
47
+ new(path)
48
+ end
49
+
50
+ # @return [Integer] number of faces in the collection
51
+ def face_count
52
+ @collection.num_fonts
53
+ end
54
+
55
+ # Yields each face as a loaded TTF/OTF font. Without a block,
56
+ # returns an Enumerator (so callers can chain +each_with_index+).
57
+ # The Enumerator's size is set to +face_count+ so size-based
58
+ # assertions work without consuming it.
59
+ #
60
+ # @yieldparam face [TrueTypeFont, OpenTypeFont]
61
+ # @return [Enumerator, void]
62
+ def each_face
63
+ return enum_for(:each_face) { face_count } unless block_given?
64
+
65
+ face_count.times { |i| yield FontLoader.load(@path, font_index: i) }
66
+ end
67
+
68
+ # @return [Array<Stats>] one Stats per face, in face-index order
69
+ def stats
70
+ each_face.map.with_index do |face, i|
71
+ Stats.new(
72
+ index: i,
73
+ glyph_count: face.table("maxp")&.num_glyphs || 0,
74
+ codepoint_count: (face.table("cmap")&.unicode_mappings || {}).size,
75
+ sfnt_version: face.header.sfnt_version,
76
+ )
77
+ end
78
+ end
79
+
80
+ # Union of every face's cmap keys.
81
+ #
82
+ # @return [Set<Integer>]
83
+ def cmap_union
84
+ each_face.with_object(Set.new) do |face, union|
85
+ union.merge((face.table("cmap")&.unicode_mappings || {}).keys)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -19,6 +19,10 @@ module Fontisan
19
19
  VERSION_1_0_MAJOR = 1
20
20
  VERSION_1_0_MINOR = 0
21
21
 
22
+ # TTC version 2.0 (major=2, minor=0) — supports DSIG reference
23
+ VERSION_2_0_MAJOR = 2
24
+ VERSION_2_0_MINOR = 0
25
+
22
26
  # Initialize writer
23
27
  #
24
28
  # @param fonts [Array<TrueTypeFont, OpenTypeFont>] Source fonts
@@ -91,12 +95,7 @@ module Fontisan
91
95
  #
92
96
  # @return [String] TTC header binary
93
97
  def write_ttc_header
94
- [
95
- TTC_TAG, # char[4] - tag
96
- VERSION_1_0_MAJOR, # uint16 - major version
97
- VERSION_1_0_MINOR, # uint16 - minor version
98
- @fonts.size, # uint32 - number of fonts
99
- ].pack("a4 n n N")
98
+ [TTC_TAG, VERSION_1_0_MAJOR, VERSION_1_0_MINOR, @fonts.size].pack("a4nnN")
100
99
  end
101
100
 
102
101
  # Write offset table
@@ -7,6 +7,7 @@ module Fontisan
7
7
  autoload :Builder, "fontisan/collection/builder"
8
8
  autoload :DfontBuilder, "fontisan/collection/dfont_builder"
9
9
  autoload :OffsetCalculator, "fontisan/collection/offset_calculator"
10
+ autoload :Reader, "fontisan/collection/reader"
10
11
  autoload :SharedLogic, "fontisan/collection/shared_logic"
11
12
  autoload :TableAnalyzer, "fontisan/collection/table_analyzer"
12
13
  autoload :TableDeduplicator, "fontisan/collection/table_deduplicator"