fontisan 0.4.6 → 0.4.7
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 +106 -0
- data/README.adoc +121 -68
- data/benchmark/compile_benchmark.rb +70 -0
- 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/lib/fontisan/collection/writer.rb +5 -6
- data/lib/fontisan/error.rb +31 -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.rb +188 -167
- 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/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/version.rb +1 -1
- data/lib/fontisan.rb +3 -0
- metadata +41 -2
data/README.adoc
CHANGED
|
@@ -56,9 +56,19 @@ gem install fontisan
|
|
|
56
56
|
|
|
57
57
|
== Features
|
|
58
58
|
|
|
59
|
+
Font compilation and assembly::
|
|
60
|
+
* UFO → TTF compilation with cubic-to-quadratic conversion and winding-order correction (see link:docs/UFO_COMPILATION.adoc[UFO Compilation Guide])
|
|
61
|
+
* UFO → OTF (CFF1) compilation with Type 2 charstring encoding
|
|
62
|
+
* UFO → OTF (CFF2) compilation with variable-font blend/vsindex operators (see link:docs/CFF2_SUPPORT.adoc[CFF2 Support Guide])
|
|
63
|
+
* Multi-source font stitching with explicit subfont declaration and TTC/OTC collection output (see link:docs/STITCHER_GUIDE.adoc[Stitcher Guide])
|
|
64
|
+
* SVG path → UFO glyph conversion for chart-extracted glyphs (see link:docs/SVG_TO_GLYF.adoc[SVG to Glyph Guide])
|
|
65
|
+
* Variable font compilation: fvar, gvar, HVAR, MVAR, avar, STAT tables
|
|
66
|
+
* Signature-based glyph deduplication with 65,535 glyph cap enforcement
|
|
67
|
+
* Compound (composite) TrueType glyph flattening
|
|
68
|
+
|
|
59
69
|
Color fonts and collections::
|
|
60
|
-
* Color font
|
|
61
|
-
* Font collection
|
|
70
|
+
* Color font table builders: COLR v0, CPAL, SVG table, sbix, CBDT/CBLC (see link:docs/COLOR_FONTS.adoc[Color Fonts Guide])
|
|
71
|
+
* Font collection management: pack/unpack TTC/OTC with table deduplication (see link:docs/COLLECTION_VALIDATION.adoc[Collection Validation Guide])
|
|
62
72
|
|
|
63
73
|
Font analysis and information::
|
|
64
74
|
* Comprehensive per-face font metadata extraction (replaces `otfinfo`) covering identity, style, metrics, codepoint coverage, licensing, hinting, color capabilities, variable-font detail, and OpenType layout features
|
|
@@ -81,21 +91,26 @@ Font operations::
|
|
|
81
91
|
* Collection management (pack/unpack TTC/OTC/dfont files with table deduplication)
|
|
82
92
|
|
|
83
93
|
Font format support::
|
|
84
|
-
* TTF, OTF, TTC, OTC font formats (production ready)
|
|
94
|
+
* TTF, OTF (CFF1), OTF2 (CFF2), TTC, OTC font formats (production ready)
|
|
95
|
+
* CFF2 with variable-font blend/vsindex operators, FDSelect, subroutines
|
|
85
96
|
* Adobe Type 1 fonts (PFB/PFA) with bidirectional conversion (see link:docs/TYPE1_FONTS.adoc[Type 1 Fonts Guide])
|
|
86
97
|
* WOFF/WOFF2 format support with reading, writing, and conversion (see link:docs/WOFF_WOFF2_FORMATS.adoc[WOFF/WOFF2 Guide])
|
|
87
98
|
* Apple legacy font support: 'true' signature TrueType fonts and dfont format (see link:docs/APPLE_LEGACY_FONTS.adoc[Apple Legacy Fonts Guide])
|
|
88
|
-
* SVG font generation (complete)
|
|
99
|
+
* SVG font generation and SVG-to-glyph conversion (complete)
|
|
89
100
|
|
|
90
101
|
Font engineering::
|
|
91
102
|
* Universal outline model for format-agnostic glyph representation
|
|
92
|
-
* CFF CharString encoding/decoding
|
|
93
|
-
* CFF INDEX structure building (
|
|
94
|
-
* CFF DICT structure building
|
|
103
|
+
* CFF/CFF2 CharString encoding/decoding with blend operator support
|
|
104
|
+
* CFF/CFF2 INDEX structure building (uint16 for CFF1, uint32 for CFF2)
|
|
105
|
+
* CFF/CFF2 DICT structure building
|
|
95
106
|
* TrueType curve converter for bi-directional quadratic/cubic conversion (complete)
|
|
96
|
-
* Compound glyph decomposition with transformation
|
|
97
|
-
*
|
|
107
|
+
* Compound glyph decomposition with affine transformation flattening (complete)
|
|
108
|
+
* CFF2 subroutine bias calculation and INDEX management
|
|
109
|
+
* FDSelect (format 0, 3) for CID-keyed CFF2 fonts
|
|
98
110
|
* Bidirectional hint conversion (TrueType ↔ PostScript) with validation (complete)
|
|
111
|
+
* MATH table builder for mathematical formula layout
|
|
112
|
+
* Meta table builder for tagged metadata
|
|
113
|
+
* Glyph signature-based deduplication across donor fonts
|
|
99
114
|
* CFF2 variable font support for PostScript hint conversion (complete)
|
|
100
115
|
|
|
101
116
|
Export and interfaces::
|
|
@@ -2379,8 +2394,8 @@ Fontisan can:
|
|
|
2379
2394
|
== UFO source operations
|
|
2380
2395
|
|
|
2381
2396
|
fontisan reads, writes, and compiles UFO (Unified Font Object) v2/v3
|
|
2382
|
-
font sources into binary font formats (TTF, OTF). No AFDKO
|
|
2383
|
-
fonttools needed.
|
|
2397
|
+
font sources into binary font formats (TTF, OTF, OTF2/CFF2). No AFDKO
|
|
2398
|
+
or Python fonttools needed.
|
|
2384
2399
|
|
|
2385
2400
|
=== Build a UFO to binary
|
|
2386
2401
|
|
|
@@ -2397,43 +2412,23 @@ The TTF compiler automatically applies:
|
|
|
2397
2412
|
- `cubic_to_quadratic` filter (UFO cubic Beziers -> TTF quadratic)
|
|
2398
2413
|
- `reverse_contour_direction` filter (TTF winding convention)
|
|
2399
2414
|
|
|
2400
|
-
===
|
|
2401
|
-
|
|
2402
|
-
[source,bash]
|
|
2403
|
-
----
|
|
2404
|
-
# UFO -> binary
|
|
2405
|
-
fontisan ufo convert font.ufo out.ttf --to ttf
|
|
2406
|
-
|
|
2407
|
-
# Binary -> UFO (reverse direction)
|
|
2408
|
-
fontisan ufo convert font.ttf font.ufo/
|
|
2409
|
-
----
|
|
2410
|
-
|
|
2411
|
-
=== Validate a UFO source
|
|
2412
|
-
|
|
2413
|
-
[source,bash]
|
|
2414
|
-
----
|
|
2415
|
-
fontisan ufo validate font.ufo
|
|
2416
|
-
----
|
|
2417
|
-
|
|
2418
|
-
Checks: glyphs present, family name set, unitsPerEm set, .notdef exists.
|
|
2419
|
-
|
|
2420
|
-
=== Ruby API
|
|
2415
|
+
=== Ruby API — compile to TTF, OTF, or CFF2
|
|
2421
2416
|
|
|
2422
2417
|
[source,ruby]
|
|
2423
2418
|
----
|
|
2424
2419
|
require "fontisan"
|
|
2425
2420
|
|
|
2426
|
-
# Read a UFO
|
|
2427
2421
|
font = Fontisan::Ufo::Font.open("font.ufo")
|
|
2428
|
-
puts font.family_name
|
|
2429
|
-
puts font.glyphs.size
|
|
2430
2422
|
|
|
2431
|
-
#
|
|
2423
|
+
# TrueType (.ttf) — quadratic outlines
|
|
2432
2424
|
Fontisan::Ufo::Compile::TtfCompiler.new(font).compile(output_path: "out.ttf")
|
|
2433
2425
|
|
|
2434
|
-
#
|
|
2426
|
+
# OpenType/CFF1 (.otf) — cubic outlines
|
|
2435
2427
|
Fontisan::Ufo::Compile::OtfCompiler.new(font).compile(output_path: "out.otf")
|
|
2436
2428
|
|
|
2429
|
+
# OpenType/CFF2 (.otf) — modern format, supports variable fonts
|
|
2430
|
+
Fontisan::Ufo::Compile::Otf2Compiler.new(font).compile(output_path: "out.otf")
|
|
2431
|
+
|
|
2437
2432
|
# Write back to UFO (round-trip)
|
|
2438
2433
|
Fontisan::Ufo::Writer.new(font).write("out.ufo")
|
|
2439
2434
|
|
|
@@ -2442,12 +2437,39 @@ ttf = Fontisan::FontLoader.load("font.ttf")
|
|
|
2442
2437
|
ufo = Fontisan::Ufo::Convert::FromBinData.convert(ttf)
|
|
2443
2438
|
----
|
|
2444
2439
|
|
|
2440
|
+
=== Variable font compilation
|
|
2441
|
+
|
|
2442
|
+
[source,ruby]
|
|
2443
|
+
----
|
|
2444
|
+
# Variable TTF (glyf outlines)
|
|
2445
|
+
orchestrator = Fontisan::Ufo::Compile::VariableTtf.new(
|
|
2446
|
+
font: default_font,
|
|
2447
|
+
axes: [{ tag: "wght", min: 100, default: 400, max: 900 }],
|
|
2448
|
+
masters: [{ font: bold_font, axes: { "wght" => 1.0 } }],
|
|
2449
|
+
)
|
|
2450
|
+
orchestrator.compile(output_path: "variable.ttf")
|
|
2451
|
+
|
|
2452
|
+
# Variable OTF (CFF2 outlines)
|
|
2453
|
+
orchestrator = Fontisan::Ufo::Compile::VariableOtf.new(
|
|
2454
|
+
default_font,
|
|
2455
|
+
axes: [{ tag: "wght", min: 100, default: 400, max: 900 }],
|
|
2456
|
+
)
|
|
2457
|
+
orchestrator.compile(output_path: "variable.otf")
|
|
2458
|
+
----
|
|
2459
|
+
|
|
2460
|
+
See link:docs/CFF2_SUPPORT.adoc[CFF2 Support Guide] for CFF2-specific
|
|
2461
|
+
features (blend operators, FDSelect, subroutines, VariationStore).
|
|
2462
|
+
|
|
2445
2463
|
== Font stitching
|
|
2446
2464
|
|
|
2447
|
-
The Stitcher combines glyphs from multiple source fonts into one
|
|
2448
|
-
|
|
2465
|
+
The Stitcher combines glyphs from multiple source fonts into one or
|
|
2466
|
+
more new fonts. Sources can be UFO directories or loaded TTF/OTF files.
|
|
2449
2467
|
|
|
2450
|
-
|
|
2468
|
+
Every `include_*` method requires an `into:` keyword naming the target
|
|
2469
|
+
subfont. `write_to` requires a `subfont:` name. No defaults — the user
|
|
2470
|
+
controls the collection structure explicitly.
|
|
2471
|
+
|
|
2472
|
+
=== Single font
|
|
2451
2473
|
|
|
2452
2474
|
[source,ruby]
|
|
2453
2475
|
----
|
|
@@ -2455,32 +2477,37 @@ stitcher = Fontisan::Stitcher.new
|
|
|
2455
2477
|
stitcher.add_source(:latin, Fontisan::FontLoader.load("NotoSans.ttf"))
|
|
2456
2478
|
stitcher.add_source(:cjk, Fontisan::FontLoader.load("FSung-m.ttf"))
|
|
2457
2479
|
|
|
2458
|
-
stitcher.
|
|
2459
|
-
stitcher.include_range(
|
|
2460
|
-
stitcher.
|
|
2480
|
+
stitcher.include_notdef(from: :latin, into: :main)
|
|
2481
|
+
stitcher.include_range(0x41..0x5A, from: :latin, into: :main) # A-Z
|
|
2482
|
+
stitcher.include_range(0x4E00..0x9FFF, from: :cjk, into: :main) # CJK
|
|
2461
2483
|
|
|
2462
|
-
stitcher.write_to("stitched.ttf", format: :ttf)
|
|
2484
|
+
stitcher.write_to("stitched.ttf", format: :ttf, subfont: :main)
|
|
2463
2485
|
----
|
|
2464
2486
|
|
|
2465
|
-
===
|
|
2487
|
+
=== Collection (TTC/OTC) with explicit subfonts
|
|
2466
2488
|
|
|
2467
|
-
|
|
2489
|
+
When the glyph count exceeds 65,535 (the OpenType cap), split across
|
|
2490
|
+
named subfonts and write a collection:
|
|
2491
|
+
|
|
2492
|
+
[source,ruby]
|
|
2468
2493
|
----
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2494
|
+
stitcher = Fontisan::Stitcher.new
|
|
2495
|
+
stitcher.add_source(:noto_sans, Fontisan::FontLoader.load("NotoSans.ttf"))
|
|
2496
|
+
stitcher.add_source(:noto_cjk, Fontisan::FontLoader.load("NotoSansCJK.ttf"))
|
|
2497
|
+
|
|
2498
|
+
# Explicitly assign codepoints to named subfonts
|
|
2499
|
+
stitcher.include_range(0x41..0x5A, from: :noto_sans, into: :latin)
|
|
2500
|
+
stitcher.include_range(0x4E00..0x9FFF, from: :noto_cjk, into: :cjk)
|
|
2501
|
+
|
|
2502
|
+
# Write as OTC with CFF2 subfonts and table deduplication
|
|
2503
|
+
stitcher.write_collection("out.otc", format: :otf2)
|
|
2476
2504
|
----
|
|
2477
2505
|
|
|
2478
|
-
===
|
|
2506
|
+
=== CBDT/CBLC passthrough (color emoji)
|
|
2479
2507
|
|
|
2480
2508
|
Sources with CBDT/CBLC tables (e.g. NotoColorEmoji) can be used as
|
|
2481
2509
|
Stitcher sources. The bitmap data is propagated byte-for-byte from the
|
|
2482
|
-
source into the output.
|
|
2483
|
-
storage mode via `bitmap_mode`:
|
|
2510
|
+
source into the output.
|
|
2484
2511
|
|
|
2485
2512
|
[source,ruby]
|
|
2486
2513
|
----
|
|
@@ -2488,22 +2515,48 @@ stitcher = Fontisan::Stitcher.new
|
|
|
2488
2515
|
stitcher.add_source(:text, Fontisan::FontLoader.load("NotoSans.ttf"))
|
|
2489
2516
|
stitcher.add_source(:emoji, Fontisan::FontLoader.load("NotoColorEmoji.ttf"))
|
|
2490
2517
|
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
stitcher.include_range(
|
|
2494
|
-
stitcher.include_range(0x1F600..0x1F64F, from: :emoji)
|
|
2518
|
+
stitcher.include_notdef(from: :text, into: :main)
|
|
2519
|
+
stitcher.include_range(0x41..0x5A, from: :text, into: :main)
|
|
2520
|
+
stitcher.include_range(0x1F600..0x1F64F, from: :emoji, into: :main)
|
|
2495
2521
|
|
|
2496
|
-
stitcher.write_to("out.ttf", format: :ttf)
|
|
2522
|
+
stitcher.write_to("out.ttf", format: :ttf, subfont: :main)
|
|
2497
2523
|
# → out.ttf has glyf + CBDT + CBLC, emoji renders correctly
|
|
2498
2524
|
----
|
|
2499
2525
|
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2526
|
+
=== SVG path → glyph conversion
|
|
2527
|
+
|
|
2528
|
+
Convert SVG path data (from ucode code-chart extraction) into UFO glyphs:
|
|
2529
|
+
|
|
2530
|
+
[source,ruby]
|
|
2531
|
+
----
|
|
2532
|
+
# Single path → glyph
|
|
2533
|
+
glyph = Fontisan::SvgToGlyf.convert(
|
|
2534
|
+
"M 0 0 L 500 0 L 500 700 Z",
|
|
2535
|
+
upm: 1000,
|
|
2536
|
+
codepoint: 0x10940,
|
|
2537
|
+
)
|
|
2538
|
+
|
|
2539
|
+
# SVG file → glyph
|
|
2540
|
+
glyph = Fontisan::SvgToGlyf.from_svg_file("U+10940.svg", upm: 1000)
|
|
2541
|
+
|
|
2542
|
+
# Directory of SVGs → font (for Stitcher#add_source)
|
|
2543
|
+
font = Fontisan::SvgToGlyf.from_directory("/tmp/chart-svg/Khitan", upm: 1000)
|
|
2544
|
+
----
|
|
2545
|
+
|
|
2546
|
+
See link:docs/SVG_TO_GLYF.adoc[SVG to Glyph Guide] for details.
|
|
2547
|
+
|
|
2548
|
+
=== CLI
|
|
2549
|
+
|
|
2550
|
+
[source,bash]
|
|
2551
|
+
----
|
|
2552
|
+
fontisan stitch \
|
|
2553
|
+
--source latin=NotoSans.ttf \
|
|
2554
|
+
--source cjk=FSung-m.ttf \
|
|
2555
|
+
--include-range latin=0x41-0x5A \
|
|
2556
|
+
--include-range cjk=0x4E00-0x9FFF \
|
|
2557
|
+
--notdef-from latin \
|
|
2558
|
+
--output stitched.ttf
|
|
2559
|
+
----
|
|
2507
2560
|
|
|
2508
2561
|
|
|
2509
2562
|
== Testing
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Benchmark: large font compilation performance.
|
|
4
|
+
#
|
|
5
|
+
# Measures the time taken to compile a synthetic large UFO font into
|
|
6
|
+
# TTF, OTF, and CFF2 OTF. Catches regressions in the charstring
|
|
7
|
+
# pipeline and the table builders.
|
|
8
|
+
|
|
9
|
+
require "tmpdir"
|
|
10
|
+
require "fontisan"
|
|
11
|
+
require "fontisan/ufo/compile"
|
|
12
|
+
|
|
13
|
+
module FontisanBench
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def build_synthetic_font(num_glyphs)
|
|
17
|
+
font = Fontisan::Ufo::Font.new
|
|
18
|
+
font.info.family_name = "Bench"
|
|
19
|
+
font.info.units_per_em = 1000
|
|
20
|
+
font.glyphs[".notdef"] = Fontisan::Ufo::Glyph.new(name: ".notdef")
|
|
21
|
+
|
|
22
|
+
num_glyphs.times do |i|
|
|
23
|
+
g = Fontisan::Ufo::Glyph.new(name: "g#{i}")
|
|
24
|
+
g.width = 500
|
|
25
|
+
g.add_unicode(0xE000 + i) if i < 0x1000
|
|
26
|
+
g.add_contour(Fontisan::Ufo::Contour.new([
|
|
27
|
+
Fontisan::Ufo::Point.new(x: 0, y: 0, type: "line"),
|
|
28
|
+
Fontisan::Ufo::Point.new(x: 100, y: 0, type: "line"),
|
|
29
|
+
Fontisan::Ufo::Point.new(x: 100, y: 100, type: "offcurve"),
|
|
30
|
+
Fontisan::Ufo::Point.new(x: 50, y: 150, type: "curve"),
|
|
31
|
+
]))
|
|
32
|
+
font.glyphs["g#{i}"] = g
|
|
33
|
+
end
|
|
34
|
+
font
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def measure(label)
|
|
38
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
39
|
+
yield
|
|
40
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
41
|
+
puts format("%-40<label>s %8.<elapsed>.2f ms", label: label, elapsed: elapsed * 1000)
|
|
42
|
+
elapsed
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if __FILE__ == $PROGRAM_NAME
|
|
47
|
+
ufo = FontisanBench.build_synthetic_font(1000)
|
|
48
|
+
|
|
49
|
+
Dir.mktmpdir do |dir|
|
|
50
|
+
FontisanBench.measure("TTF compile (1000 glyphs)") do
|
|
51
|
+
Fontisan::Ufo::Compile::TtfCompiler.new(ufo).compile(output_path: "#{dir}/a.ttf")
|
|
52
|
+
end
|
|
53
|
+
FontisanBench.measure("OTF compile (1000 glyphs)") do
|
|
54
|
+
Fontisan::Ufo::Compile::OtfCompiler.new(ufo).compile(output_path: "#{dir}/a.otf")
|
|
55
|
+
end
|
|
56
|
+
FontisanBench.measure("CFF2 OTF compile (1000 glyphs)") do
|
|
57
|
+
Fontisan::Ufo::Compile::Otf2Compiler.new(ufo).compile(output_path: "#{dir}/a-cff2.otf")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
Dir.mktmpdir do |dir|
|
|
62
|
+
ufo_big = FontisanBench.build_synthetic_font(10_000)
|
|
63
|
+
FontisanBench.measure("TTF compile (10k glyphs)") do
|
|
64
|
+
Fontisan::Ufo::Compile::TtfCompiler.new(ufo_big).compile(output_path: "#{dir}/a.ttf")
|
|
65
|
+
end
|
|
66
|
+
FontisanBench.measure("CFF2 OTF compile (10k glyphs)") do
|
|
67
|
+
Fontisan::Ufo::Compile::Otf2Compiler.new(ufo_big).compile(output_path: "#{dir}/a-cff2.otf")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
= CFF2 Support Guide
|
|
2
|
+
|
|
3
|
+
== General
|
|
4
|
+
|
|
5
|
+
CFF2 (Compact Font Format version 2) is the modern PostScript outline
|
|
6
|
+
format defined in OpenType 1.9+. It is required for variable OTF fonts
|
|
7
|
+
that use PostScript outlines, and it replaces the legacy CFF (CFF1)
|
|
8
|
+
format's Name INDEX, String INDEX, Encoding, and Charset with a
|
|
9
|
+
simpler, variation-aware structure.
|
|
10
|
+
|
|
11
|
+
Fontisan implements CFF2 entirely in pure Ruby. The pieces:
|
|
12
|
+
|
|
13
|
+
* `Tables::Cff2::Header` — 5-byte header (major, minor, headerSize, topDictLength)
|
|
14
|
+
* `Tables::Cff2::IndexBuilder` — uint32-count INDEX (CFF1 uses uint16)
|
|
15
|
+
* `Tables::Cff2::DictEncoder` — DICT operand/operator encoding
|
|
16
|
+
* `Tables::Cff2::FdSelect` — format 0 and format 3 FDSelect for CID-keyed fonts
|
|
17
|
+
* `Tables::Cff::Cff2CharStringBuilder` — charstring builder with
|
|
18
|
+
vsindex (operator 22, replacing CFF1's hmoveto) and blend (operator 23)
|
|
19
|
+
* `Ufo::Compile::Cff2` — assembles the CFF2 table from UFO glyphs
|
|
20
|
+
* `Ufo::Compile::Cff2Subrs` — bias calculation + GlobalSubr/LocalSubr INDEXes
|
|
21
|
+
|
|
22
|
+
== Building a CFF2 table directly
|
|
23
|
+
|
|
24
|
+
[source,ruby]
|
|
25
|
+
----
|
|
26
|
+
glyphs = font.glyphs.values
|
|
27
|
+
cff2_bytes = Fontisan::Ufo::Compile::Cff2.build(font, glyphs: glyphs)
|
|
28
|
+
----
|
|
29
|
+
|
|
30
|
+
For variable CFF2, pass an `ItemVariationStore`:
|
|
31
|
+
|
|
32
|
+
[source,ruby]
|
|
33
|
+
----
|
|
34
|
+
vs_bytes = Fontisan::Ufo::Compile::ItemVariationStore.build(...)
|
|
35
|
+
cff2_bytes = Fontisan::Ufo::Compile::Cff2.build(
|
|
36
|
+
font, glyphs: glyphs, variation_store: vs_bytes
|
|
37
|
+
)
|
|
38
|
+
----
|
|
39
|
+
|
|
40
|
+
When `variation_store:` is present, it is placed between the GlobalSubr
|
|
41
|
+
INDEX and CharStrings INDEX, and the Top DICT records its offset via
|
|
42
|
+
operator 24.
|
|
43
|
+
|
|
44
|
+
== CFF2 table layout
|
|
45
|
+
|
|
46
|
+
[cols="1,3", options="header"]
|
|
47
|
+
|===
|
|
48
|
+
| Section | Notes
|
|
49
|
+
|
|
50
|
+
| Header (5 bytes) | major=2, minor=0, headerSize=5, topDictLength=N
|
|
51
|
+
|
|
52
|
+
| Top DICT | Offsets to CharStrings (op 17), Font DICT INDEX (op [12,36]),
|
|
53
|
+
VariationStore (op 24, variable fonts only), Private
|
|
54
|
+
(op 18)
|
|
55
|
+
|
|
56
|
+
| GlobalSubr INDEX | uint32 count — empty by default
|
|
57
|
+
|
|
58
|
+
| VariationStore | Present only for variable CFF2 fonts
|
|
59
|
+
|
|
60
|
+
| CharStrings INDEX | uint32 count, one entry per glyph
|
|
61
|
+
|
|
62
|
+
| Font DICT INDEX | uint32 count, wraps one or more Font DICTs
|
|
63
|
+
|
|
64
|
+
| Font DICT | DICT with Private operator `[size, offset]`
|
|
65
|
+
|===
|
|
66
|
+
|
|
67
|
+
The Top DICT's size determines where GlobalSubr INDEX starts, which
|
|
68
|
+
cascades to all subsequent offsets — a circular dependency that
|
|
69
|
+
`Ufo::Compile::Cff2` resolves with fixed-point iteration (typically
|
|
70
|
+
converges in 2-3 passes).
|
|
71
|
+
|
|
72
|
+
== Variable charstrings: blend and vsindex
|
|
73
|
+
|
|
74
|
+
CFF2 introduces two new charstring operators:
|
|
75
|
+
|
|
76
|
+
[cols="1,1,3", options="header"]
|
|
77
|
+
|===
|
|
78
|
+
| Operator | Code | Purpose
|
|
79
|
+
|
|
80
|
+
| `vsindex` | 22 | Selects the variation region scalars (replaces CFF1 `hmoveto`)
|
|
81
|
+
|
|
82
|
+
| `blend` | 23 | Pops n+1 numbers per operand, blends using region scalars
|
|
83
|
+
|===
|
|
84
|
+
|
|
85
|
+
The blend protocol:
|
|
86
|
+
|
|
87
|
+
stack before: [base_value, delta_r0, delta_r1, ..., delta_r(n-1), n, blend]
|
|
88
|
+
stack after: [blended_value]
|
|
89
|
+
|
|
90
|
+
The blended value is `base_value + Σ(delta_i × scalar_i)` for the
|
|
91
|
+
currently selected regions.
|
|
92
|
+
|
|
93
|
+
`Tables::Cff::Cff2CharStringBuilder.build_variable` encodes variable
|
|
94
|
+
outlines directly:
|
|
95
|
+
|
|
96
|
+
[source,ruby]
|
|
97
|
+
----
|
|
98
|
+
charstring = Fontisan::Tables::Cff::Cff2CharStringBuilder.build_variable(
|
|
99
|
+
default_outline,
|
|
100
|
+
master_outlines: [bold_outline, light_outline],
|
|
101
|
+
num_regions: 2,
|
|
102
|
+
width: 600,
|
|
103
|
+
)
|
|
104
|
+
# → charstring bytes with base values + per-region deltas + blend
|
|
105
|
+
----
|
|
106
|
+
|
|
107
|
+
The builder tracks per-master position via
|
|
108
|
+
`MasterState = Struct.new(:current_x, :current_y)` to compute relative
|
|
109
|
+
deltas as the command sequence is walked. All masters must share the
|
|
110
|
+
same command structure as the default outline.
|
|
111
|
+
|
|
112
|
+
When all deltas for a coordinate are zero, the blend operator is
|
|
113
|
+
omitted for that coordinate — visually identical output, smaller file.
|
|
114
|
+
|
|
115
|
+
== FDSelect
|
|
116
|
+
|
|
117
|
+
CID-keyed CFF2 fonts use FDSelect to map each GID to a Font DICT.
|
|
118
|
+
`Tables::Cff2::FdSelect` supports both formats:
|
|
119
|
+
|
|
120
|
+
* **Format 0** — one byte per glyph (small fonts)
|
|
121
|
+
* **Format 3** — run-length encoded (large fonts with contiguous FD
|
|
122
|
+
assignments)
|
|
123
|
+
|
|
124
|
+
[source,ruby]
|
|
125
|
+
----
|
|
126
|
+
fdselect_bytes = Fontisan::Tables::Cff2::FdSelect.build(
|
|
127
|
+
format: 3,
|
|
128
|
+
fd_indices: [0, 0, 1, 1, 1, 2]
|
|
129
|
+
)
|
|
130
|
+
----
|
|
131
|
+
|
|
132
|
+
== Subroutines
|
|
133
|
+
|
|
134
|
+
`Ufo::Compile::Cff2Subrs` provides:
|
|
135
|
+
|
|
136
|
+
* `bias(count)` — subroutine bias per spec:
|
|
137
|
+
** 107 when count ≤ 1240
|
|
138
|
+
** 1131 when count ≤ 33800
|
|
139
|
+
** 32768 above
|
|
140
|
+
* `build_index(subrs)` — uint32 INDEX builder for both GlobalSubr and
|
|
141
|
+
LocalSubr (same structure — DRY)
|
|
142
|
+
|
|
143
|
+
By default the GlobalSubr INDEX is empty (count=0). Per-FontDICT
|
|
144
|
+
LocalSubr INDEXes are also empty in the static-font case.
|
|
145
|
+
|
|
146
|
+
== Compiling to a CFF2 OTF
|
|
147
|
+
|
|
148
|
+
The high-level path is `Ufo::Compile::Otf2Compiler`:
|
|
149
|
+
|
|
150
|
+
[source,ruby]
|
|
151
|
+
----
|
|
152
|
+
Fontisan::Ufo::Compile::Otf2Compiler.new(font)
|
|
153
|
+
.compile(output_path: "Modern.otf")
|
|
154
|
+
----
|
|
155
|
+
|
|
156
|
+
For variable CFF2:
|
|
157
|
+
|
|
158
|
+
[source,ruby]
|
|
159
|
+
----
|
|
160
|
+
orchestrator = Fontisan::Ufo::Compile::VariableOtf.new(
|
|
161
|
+
default_font,
|
|
162
|
+
master_fonts: [bold_font, light_font],
|
|
163
|
+
axes: [{ tag: "wght", min: 100, default: 400, max: 900 }],
|
|
164
|
+
)
|
|
165
|
+
orchestrator.compile(output_path: "Variable.otf")
|
|
166
|
+
----
|
|
167
|
+
|
|
168
|
+
NOTE: The current `VariableOtf` orchestrator emits a static CFF2 table
|
|
169
|
+
with `fvar`, `STAT`, and `avar` for axes — the blend/vsindex operators
|
|
170
|
+
exist in `Cff2CharStringBuilder` but are not yet wired through the
|
|
171
|
+
default compile path. Until that integration lands, instances
|
|
172
|
+
generated from the variable OTF will all render the default master.
|
|
173
|
+
|
|
174
|
+
== Spec reference
|
|
175
|
+
|
|
176
|
+
* https://learn.microsoft.com/en-us/typography/opentype/spec/cff2[OpenType CFF2 spec]
|
|
177
|
+
* https://learn.microsoft.com/en-us/typography/opentype/spec/cff2charstr[CFF2 CharString spec]
|
|
178
|
+
|
|
179
|
+
== See also
|
|
180
|
+
|
|
181
|
+
* link:UFO_COMPILATION.adoc[UFO Compilation Guide] — high-level
|
|
182
|
+
compiler overview
|
|
183
|
+
* link:VARIABLE_FONT_OPERATIONS.adoc[Variable Font Operations] —
|
|
184
|
+
reading and modifying existing variable fonts
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
= Stitcher Guide
|
|
2
|
+
|
|
3
|
+
== General
|
|
4
|
+
|
|
5
|
+
The Stitcher combines glyphs from multiple source fonts into one or
|
|
6
|
+
more new fonts. Sources can be UFO directories, loaded TTF/OTF files,
|
|
7
|
+
or any object responding to the `Ufo::Font` interface.
|
|
8
|
+
|
|
9
|
+
The design rule: **the user controls the collection structure
|
|
10
|
+
explicitly**. Every `include_*` method requires an `into:` keyword
|
|
11
|
+
naming the target subfont, and `write_to` requires a `subfont:` name.
|
|
12
|
+
There are no defaults and no after-the-fact splitting.
|
|
13
|
+
|
|
14
|
+
== API
|
|
15
|
+
|
|
16
|
+
[cols="2,3", options="header"]
|
|
17
|
+
|===
|
|
18
|
+
| Method | Purpose
|
|
19
|
+
|
|
20
|
+
| `Stitcher.new(deduplicate: true)` | Create a stitcher; dedup is on by default
|
|
21
|
+
|
|
22
|
+
| `add_source(label, font)` | Register a source font under a Symbol label
|
|
23
|
+
|
|
24
|
+
| `include_range(range, from:, into:)` | Add codepoint range from `from` into `into`
|
|
25
|
+
|
|
26
|
+
| `include_codepoints(cps, from:, into:)` | Add an explicit Array of codepoints
|
|
27
|
+
|
|
28
|
+
| `include_gid(donor_gid, from:, into:)` | Add a specific GID from the source
|
|
29
|
+
|
|
30
|
+
| `include_notdef(from:, into:)` | Convenience for `include_gid(0, ...)`
|
|
31
|
+
|
|
32
|
+
| `set_info(hash)` | Override font info (family name, etc.)
|
|
33
|
+
|
|
34
|
+
| `write_to(path, format:, subfont:)` | Compile one subfont to a single file
|
|
35
|
+
|
|
36
|
+
| `write_collection(path, format:)` | Compile all declared subfonts into a TTC/OTC
|
|
37
|
+
|===
|
|
38
|
+
|
|
39
|
+
`format:` is one of `:ttf`, `:otf`, `:otf2`. For collections, `:ttf`
|
|
40
|
+
produces a `.ttc` and `:otf`/`:otf2` produce an `.otc`.
|
|
41
|
+
|
|
42
|
+
== Single-font example
|
|
43
|
+
|
|
44
|
+
[source,ruby]
|
|
45
|
+
----
|
|
46
|
+
stitcher = Fontisan::Stitcher.new
|
|
47
|
+
stitcher.add_source(:latin, Fontisan::FontLoader.load("NotoSans.ttf"))
|
|
48
|
+
stitcher.add_source(:cjk, Fontisan::FontLoader.load("FSung-m.ttf"))
|
|
49
|
+
|
|
50
|
+
stitcher.include_notdef(from: :latin, into: :main)
|
|
51
|
+
stitcher.include_range(0x41..0x5A, from: :latin, into: :main) # A-Z
|
|
52
|
+
stitcher.include_range(0x4E00..0x9FFF, from: :cjk, into: :main) # CJK
|
|
53
|
+
|
|
54
|
+
stitcher.write_to("stitched.ttf", format: :ttf, subfont: :main)
|
|
55
|
+
----
|
|
56
|
+
|
|
57
|
+
== Collection example (TTC/OTC)
|
|
58
|
+
|
|
59
|
+
When the total glyph count would exceed 65,535 (the OpenType
|
|
60
|
+
`maxp.numGlyphs` cap), split the work across named subfonts and write
|
|
61
|
+
a collection:
|
|
62
|
+
|
|
63
|
+
[source,ruby]
|
|
64
|
+
----
|
|
65
|
+
stitcher = Fontisan::Stitcher.new
|
|
66
|
+
stitcher.add_source(:noto_sans, Fontisan::FontLoader.load("NotoSans.ttf"))
|
|
67
|
+
stitcher.add_source(:noto_cjk, Fontisan::FontLoader.load("NotoSansCJK.ttf"))
|
|
68
|
+
|
|
69
|
+
stitcher.include_range(0x41..0x5A, from: :noto_sans, into: :latin)
|
|
70
|
+
stitcher.include_range(0x4E00..0x9FFF, from: :noto_cjk, into: :cjk)
|
|
71
|
+
|
|
72
|
+
stitcher.write_collection("out.otc", format: :otf2)
|
|
73
|
+
----
|
|
74
|
+
|
|
75
|
+
Each subfont is compiled independently, then packed by
|
|
76
|
+
`Collection::Builder` with table deduplication enabled — shared tables
|
|
77
|
+
(head, name, OS/2, ...) are stored once and referenced from each
|
|
78
|
+
subfont's offset table.
|
|
79
|
+
|
|
80
|
+
== Glyph deduplication
|
|
81
|
+
|
|
82
|
+
By default the Stitcher deduplicates glyphs across all sources using
|
|
83
|
+
SHA-256 outline signatures (`Stitcher::GlyphSignature`). Two donor
|
|
84
|
+
glyphs with identical width + contours + components are merged into a
|
|
85
|
+
single target glyph; subsequent references are aliased by adding the
|
|
86
|
+
extra codepoint to the merged glyph's unicode set.
|
|
87
|
+
|
|
88
|
+
To disable deduplication (e.g., for forensic comparison):
|
|
89
|
+
|
|
90
|
+
[source,ruby]
|
|
91
|
+
----
|
|
92
|
+
stitcher = Fontisan::Stitcher.new(deduplicate: false)
|
|
93
|
+
----
|
|
94
|
+
|
|
95
|
+
== 65,535 glyph cap
|
|
96
|
+
|
|
97
|
+
OpenType `maxp.numGlyphs` is uint16 — no format (TTF, CFF1, or CFF2)
|
|
98
|
+
can hold more than 65,535 glyphs in a single font. The Stitcher
|
|
99
|
+
enforces this:
|
|
100
|
+
|
|
101
|
+
* `Stitcher::GlyphLimit.check!(count, format:)` raises
|
|
102
|
+
`GlyphLimitExceededError` when a single subfont would exceed the cap.
|
|
103
|
+
* The only path to >65,535 glyphs is a TTC/OTC collection — each
|
|
104
|
+
subfont is its own 65,535-space.
|
|
105
|
+
|
|
106
|
+
== CBDT/CBLC passthrough (color emoji)
|
|
107
|
+
|
|
108
|
+
Sources that contain CBDT/CBLC tables (e.g. NotoColorEmoji) can be
|
|
109
|
+
used as Stitcher sources. The bitmap data is propagated byte-for-byte
|
|
110
|
+
into the output:
|
|
111
|
+
|
|
112
|
+
[source,ruby]
|
|
113
|
+
----
|
|
114
|
+
stitcher = Fontisan::Stitcher.new
|
|
115
|
+
stitcher.add_source(:text, Fontisan::FontLoader.load("NotoSans.ttf"))
|
|
116
|
+
stitcher.add_source(:emoji, Fontisan::FontLoader.load("NotoColorEmoji.ttf"))
|
|
117
|
+
|
|
118
|
+
stitcher.include_notdef(from: :text, into: :main)
|
|
119
|
+
stitcher.include_range(0x41..0x5A, from: :text, into: :main)
|
|
120
|
+
stitcher.include_range(0x1F600..0x1F64F, from: :emoji, into: :main)
|
|
121
|
+
|
|
122
|
+
stitcher.write_to("out.ttf", format: :ttf, subfont: :main)
|
|
123
|
+
# → out.ttf has glyf + CBDT + CBLC, emoji renders correctly
|
|
124
|
+
----
|
|
125
|
+
|
|
126
|
+
Constraints:
|
|
127
|
+
|
|
128
|
+
* Only ONE CBDT source per Stitcher. Multiple CBDT sources raise
|
|
129
|
+
`Fontisan::MultipleCbdtSourcesError`.
|
|
130
|
+
* The CBDT source is processed first (GIDs 0..N-1) so the CBLC's GID
|
|
131
|
+
references remain valid in the output.
|
|
132
|
+
|
|
133
|
+
== CLI
|
|
134
|
+
|
|
135
|
+
[source,bash]
|
|
136
|
+
----
|
|
137
|
+
fontisan stitch \
|
|
138
|
+
--source latin=NotoSans.ttf \
|
|
139
|
+
--source cjk=FSung-m.ttf \
|
|
140
|
+
--include-range latin=0x41-0x5A \
|
|
141
|
+
--include-range cjk=0x4E00-0x9FFF \
|
|
142
|
+
--notdef-from latin \
|
|
143
|
+
--output stitched.ttf
|
|
144
|
+
----
|
|
145
|
+
|
|
146
|
+
== See also
|
|
147
|
+
|
|
148
|
+
* link:UFO_COMPILATION.adoc[UFO Compilation Guide] — TTF/OTF/CFF2 compilers
|
|
149
|
+
- link:COLLECTION_VALIDATION.adoc[Collection Validation Guide]
|
|
150
|
+
- link:SVG_TO_GLYF.adoc[SVG to Glyph Guide] — turn chart SVGs into
|
|
151
|
+
Stitcher sources
|