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
|
@@ -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
|
|
@@ -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/error.rb
CHANGED
|
@@ -206,6 +206,37 @@ module Fontisan
|
|
|
206
206
|
# CBDT/CBLC across multiple sources requires a dedicated rebuild.
|
|
207
207
|
class MultipleCbdtSourcesError < Error; end
|
|
208
208
|
|
|
209
|
+
# Stitcher produced more glyphs than the output format supports.
|
|
210
|
+
#
|
|
211
|
+
# Raised by Stitcher::GlyphLimit.check! before writing, so the user
|
|
212
|
+
# gets an actionable message instead of a silently truncated font.
|
|
213
|
+
class GlyphLimitExceededError < Error
|
|
214
|
+
attr_reader :actual, :limit, :format
|
|
215
|
+
|
|
216
|
+
def initialize(actual:, limit:, format:)
|
|
217
|
+
@actual = actual
|
|
218
|
+
@limit = limit
|
|
219
|
+
@format = format
|
|
220
|
+
super(build_message)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
private
|
|
224
|
+
|
|
225
|
+
def build_message
|
|
226
|
+
"Stitcher produced #{actual} unique glyphs, exceeding the " \
|
|
227
|
+
"#{format.to_s.upcase} limit of #{format_limit}. Both TTF and OTF " \
|
|
228
|
+
"(CFF1) cap at 65,535 glyphs — the maxp.num_glyphs field is uint16 " \
|
|
229
|
+
"and the CFF CharStrings INDEX count is card16. Options: " \
|
|
230
|
+
"(1) split the output into a TTC (TrueType Collection) by Unicode plane, " \
|
|
231
|
+
"(2) reduce the number of donors, " \
|
|
232
|
+
"(3) wait for CFF2 support in fontisan (card24 INDEX counts, no glyph cap)."
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def format_limit
|
|
236
|
+
limit == Float::INFINITY ? "infinity" : limit.to_s
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
209
240
|
# Variation data corrupted (for use in data_extractor)
|
|
210
241
|
#
|
|
211
242
|
# Raised when extracted variation data appears corrupted.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
class Stitcher
|
|
5
|
+
# Registry mapping glyph signatures to canonical glyph names in
|
|
6
|
+
# the target font. Enables signature-based deduplication: when
|
|
7
|
+
# two bindings produce glyphs with identical outlines, they share
|
|
8
|
+
# one gid and the duplicate's codepoint is redirected to the
|
|
9
|
+
# canonical glyph.
|
|
10
|
+
#
|
|
11
|
+
# Replaces the Stitcher's previous name-based dedup with
|
|
12
|
+
# outline-based dedup. This merges visually identical glyphs
|
|
13
|
+
# from different donors even when their names differ, reducing
|
|
14
|
+
# the glyph count below the TrueType 65,535 cap.
|
|
15
|
+
class Deduplicator
|
|
16
|
+
attr_reader :signatures
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
@signatures = {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Record that `glyph` maps to `canonical_name` in the target.
|
|
23
|
+
# @param glyph [Fontisan::Ufo::Glyph]
|
|
24
|
+
# @param canonical_name [String] the name under which the glyph
|
|
25
|
+
# was added to the target font
|
|
26
|
+
def register(glyph, canonical_name)
|
|
27
|
+
@signatures[GlyphSignature.for(glyph)] = canonical_name
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param glyph [Fontisan::Ufo::Glyph]
|
|
31
|
+
# @return [String, nil] the canonical name if an identical
|
|
32
|
+
# glyph was already registered, nil otherwise
|
|
33
|
+
def find(glyph)
|
|
34
|
+
@signatures[GlyphSignature.for(glyph)]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Integer] number of unique signatures registered
|
|
38
|
+
def size
|
|
39
|
+
@signatures.size
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def empty?
|
|
43
|
+
@signatures.empty?
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
class Stitcher
|
|
5
|
+
# Format-specific glyph-count caps.
|
|
6
|
+
#
|
|
7
|
+
# Both TTF and OTF (CFF1) cap at 65,535 because:
|
|
8
|
+
# - TTF: maxp.num_glyphs is uint16
|
|
9
|
+
# - OTF (CFF1): maxp.num_glyphs is uint16 AND the CFF CharStrings
|
|
10
|
+
# INDEX count is card16
|
|
11
|
+
#
|
|
12
|
+
# Exceeding the cap produces a silently truncated font (the BinData
|
|
13
|
+
# uint16 writer truncates without raising). The Stitcher checks
|
|
14
|
+
# the cap BEFORE writing so the user gets a clear error.
|
|
15
|
+
#
|
|
16
|
+
# To exceed 65,535 glyphs, the font must be split into a TTC
|
|
17
|
+
# (TrueType Collection) or fontisan must implement CFF2 (card24
|
|
18
|
+
# INDEX counts). Both are future work.
|
|
19
|
+
module GlyphLimit
|
|
20
|
+
TTF_GLYPH_CAP = 65_535
|
|
21
|
+
OTF_GLYPH_CAP = 65_535
|
|
22
|
+
|
|
23
|
+
# @param format [Symbol] :ttf or :otf
|
|
24
|
+
# @return [Integer, Float::INFINITY] the max glyph count
|
|
25
|
+
def self.for_format(format)
|
|
26
|
+
case format.to_sym
|
|
27
|
+
when :ttf then TTF_GLYPH_CAP
|
|
28
|
+
when :otf then OTF_GLYPH_CAP
|
|
29
|
+
when :otf2 then OTF_GLYPH_CAP # CFF2 CharStrings count must match maxp.numGlyphs
|
|
30
|
+
else
|
|
31
|
+
raise ArgumentError, "unknown format: #{format.inspect}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Raise GlyphLimitExceededError if `glyph_count` exceeds the cap
|
|
36
|
+
# for the given format.
|
|
37
|
+
#
|
|
38
|
+
# @param glyph_count [Integer]
|
|
39
|
+
# @param format [Symbol]
|
|
40
|
+
# @raise [GlyphLimitExceededError]
|
|
41
|
+
def self.check!(glyph_count, format:)
|
|
42
|
+
limit = for_format(format)
|
|
43
|
+
return if glyph_count <= limit
|
|
44
|
+
|
|
45
|
+
raise GlyphLimitExceededError.new(
|
|
46
|
+
actual: glyph_count,
|
|
47
|
+
limit: limit,
|
|
48
|
+
format: format,
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
class Stitcher
|
|
7
|
+
# Stateless signature computation for a Ufo::Glyph's visual identity.
|
|
8
|
+
#
|
|
9
|
+
# Two glyphs with the same signature are visually interchangeable
|
|
10
|
+
# and can share a gid in the output font. The signature captures
|
|
11
|
+
# advance width, every contour's points (x, y, type), and every
|
|
12
|
+
# component reference (base glyph + transform presence).
|
|
13
|
+
#
|
|
14
|
+
# Used by Deduplicator to merge identical outlines from different
|
|
15
|
+
# donors, reducing the glyph count below the TrueType 65,535 cap.
|
|
16
|
+
module GlyphSignature
|
|
17
|
+
# @param glyph [Fontisan::Ufo::Glyph]
|
|
18
|
+
# @return [String] SHA-256 hex digest of the glyph's outline identity
|
|
19
|
+
def self.for(glyph)
|
|
20
|
+
Digest::SHA256.hexdigest(canonical_representation(glyph))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Build a deterministic string capturing the glyph's visual identity.
|
|
24
|
+
# The representation is ordered and normalized so that semantically
|
|
25
|
+
# identical glyphs produce byte-identical strings.
|
|
26
|
+
#
|
|
27
|
+
# @param glyph [Fontisan::Ufo::Glyph]
|
|
28
|
+
# @return [String]
|
|
29
|
+
def self.canonical_representation(glyph)
|
|
30
|
+
io = +""
|
|
31
|
+
io << "w:#{glyph.width.to_i};"
|
|
32
|
+
|
|
33
|
+
glyph.contours.each_with_index do |contour, ci|
|
|
34
|
+
io << "c#{ci}:"
|
|
35
|
+
contour.points.each do |pt|
|
|
36
|
+
io << "#{pt.x.to_i},#{pt.y.to_i},#{pt.type};"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
glyph.components.each_with_index do |comp, i|
|
|
41
|
+
io << "comp#{i}:#{comp.base_glyph}"
|
|
42
|
+
io << ":t" if comp.transformation
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
io
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private_class_method :canonical_representation
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|