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.
- checksums.yaml +4 -4
- data/BUG-stitcher-drops-isolated-cps.md +58 -0
- data/BUG-stitcher-drops-plane1-codepoints.md +310 -0
- data/BUG-stitcher-gid-cap-65535.md +110 -0
- data/CHANGELOG.md +119 -0
- data/README.adoc +121 -68
- data/benchmark/compile_benchmark.rb +70 -0
- data/docs/.vitepress/config.ts +0 -7
- data/docs/CFF2_SUPPORT.adoc +184 -0
- data/docs/STITCHER_GUIDE.adoc +151 -0
- data/docs/SVG_TO_GLYF.adoc +118 -0
- data/docs/UFO_COMPILATION.adoc +119 -0
- data/docs/cli/index.md +5 -28
- data/docs/index.md +0 -2
- data/lib/fontisan/cli.rb +29 -8
- data/lib/fontisan/collection/reader/stats.rb +23 -0
- data/lib/fontisan/collection/reader.rb +90 -0
- data/lib/fontisan/collection/writer.rb +5 -6
- data/lib/fontisan/collection.rb +1 -0
- data/lib/fontisan/commands/convert_command.rb +96 -18
- data/lib/fontisan/commands/multi_format_output.rb +59 -0
- data/lib/fontisan/commands/validate_collection_command.rb +121 -0
- data/lib/fontisan/commands.rb +2 -0
- data/lib/fontisan/error.rb +56 -0
- data/lib/fontisan/models.rb +0 -1
- data/lib/fontisan/stitcher/collection_result.rb +18 -0
- data/lib/fontisan/stitcher/deduplicator.rb +47 -0
- data/lib/fontisan/stitcher/glyph_limit.rb +53 -0
- data/lib/fontisan/stitcher/glyph_signature.rb +51 -0
- data/lib/fontisan/stitcher/partition_strategy/base.rb +23 -0
- data/lib/fontisan/stitcher/partition_strategy/blueprint.rb +24 -0
- data/lib/fontisan/stitcher/partition_strategy/by_plane.rb +131 -0
- data/lib/fontisan/stitcher/partition_strategy/partition.rb +24 -0
- data/lib/fontisan/stitcher/partition_strategy.rb +22 -0
- data/lib/fontisan/stitcher.rb +223 -168
- data/lib/fontisan/svg_to_glyf/assembler.rb +132 -0
- data/lib/fontisan/svg_to_glyf/document.rb +83 -0
- data/lib/fontisan/svg_to_glyf/geometry/affine_transform.rb +112 -0
- data/lib/fontisan/svg_to_glyf/geometry/normalizer.rb +45 -0
- data/lib/fontisan/svg_to_glyf/geometry/transform_parser.rb +91 -0
- data/lib/fontisan/svg_to_glyf/geometry.rb +13 -0
- data/lib/fontisan/svg_to_glyf/path/command.rb +18 -0
- data/lib/fontisan/svg_to_glyf/path/contour_builder.rb +140 -0
- data/lib/fontisan/svg_to_glyf/path/parser.rb +98 -0
- data/lib/fontisan/svg_to_glyf/path/state.rb +79 -0
- data/lib/fontisan/svg_to_glyf/path.rb +14 -0
- data/lib/fontisan/svg_to_glyf.rb +62 -0
- data/lib/fontisan/tables/cff/cff2_charstring_builder.rb +216 -0
- data/lib/fontisan/tables/cff.rb +1 -0
- data/lib/fontisan/tables/cff2/dict_encoder.rb +94 -0
- data/lib/fontisan/tables/cff2/fd_select.rb +69 -0
- data/lib/fontisan/tables/cff2/header.rb +34 -0
- data/lib/fontisan/tables/cff2/index_builder.rb +79 -0
- data/lib/fontisan/tables/cff2.rb +4 -0
- data/lib/fontisan/ufo/compile/cbdt_cblc.rb +103 -0
- data/lib/fontisan/ufo/compile/cff2.rb +181 -0
- data/lib/fontisan/ufo/compile/cff2_subroutines.rb +39 -0
- data/lib/fontisan/ufo/compile/colr.rb +80 -0
- data/lib/fontisan/ufo/compile/cpal.rb +61 -0
- data/lib/fontisan/ufo/compile/math.rb +143 -0
- data/lib/fontisan/ufo/compile/meta.rb +51 -0
- data/lib/fontisan/ufo/compile/name.rb +2 -2
- data/lib/fontisan/ufo/compile/otf2_compiler.rb +46 -0
- data/lib/fontisan/ufo/compile/sbix.rb +99 -0
- data/lib/fontisan/ufo/compile/svg_table.rb +60 -0
- data/lib/fontisan/ufo/compile/variable_otf.rb +75 -0
- data/lib/fontisan/ufo/compile.rb +11 -0
- data/lib/fontisan/ufo/info.rb +48 -0
- data/lib/fontisan/unicode/plane.rb +56 -0
- data/lib/fontisan/unicode.rb +17 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +5 -2
- metadata +53 -19
- data/docs/cli/audit.md +0 -337
- data/lib/fontisan/cldr/aggregator.rb +0 -33
- data/lib/fontisan/cldr/cache_manager.rb +0 -110
- data/lib/fontisan/cldr/config.rb +0 -59
- data/lib/fontisan/cldr/download_error.rb +0 -9
- data/lib/fontisan/cldr/downloader.rb +0 -79
- data/lib/fontisan/cldr/error.rb +0 -8
- data/lib/fontisan/cldr/index.rb +0 -64
- data/lib/fontisan/cldr/index_builder.rb +0 -72
- data/lib/fontisan/cldr/unicode_set_parser.rb +0 -189
- data/lib/fontisan/cldr/unknown_version_error.rb +0 -9
- data/lib/fontisan/cldr/version_resolver.rb +0 -91
- data/lib/fontisan/cldr.rb +0 -23
- data/lib/fontisan/cli/cldr_cli.rb +0 -85
- data/lib/fontisan/config/cldr.yml +0 -22
- data/lib/fontisan/models/cldr/language_coverage.rb +0 -31
- 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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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: :
|
|
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,
|
|
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
|
data/lib/fontisan/collection.rb
CHANGED
|
@@ -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"
|