fontisan 0.4.22 → 0.4.24
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/.rubocop_todo.yml +240 -24
- data/Gemfile +6 -0
- data/Rakefile +9 -6
- data/benchmark/compile_benchmark.rb +13 -5
- data/docs/.vitepress/config.ts +1 -0
- data/docs/STITCHER_GUIDE.adoc +47 -3
- data/docs/api/layer.md +89 -0
- data/docs/cli/subset.md +25 -6
- data/docs/guide/cli/subset.md +31 -0
- data/docs/guide/color-fonts/bitmaps.md +17 -0
- data/lib/fontisan/cli.rb +2 -1
- data/lib/fontisan/collection/writer.rb +2 -1
- data/lib/fontisan/commands/validate_collection_command.rb +7 -1
- data/lib/fontisan/commands.rb +2 -1
- data/lib/fontisan/config/subset_profiles.yml +2 -0
- data/lib/fontisan/converters/collection_converter.rb +6 -1
- data/lib/fontisan/converters/svg_generator.rb +3 -1
- data/lib/fontisan/stitcher/cbdt_propagator.rb +44 -22
- data/lib/fontisan/stitcher/collection_result.rb +2 -1
- data/lib/fontisan/stitcher/glyph_cloner.rb +41 -0
- data/lib/fontisan/stitcher/glyph_copier.rb +5 -34
- data/lib/fontisan/stitcher/partition_strategy/by_block.rb +3 -1
- data/lib/fontisan/stitcher/partition_strategy/by_script.rb +6 -2
- data/lib/fontisan/stitcher/source.rb +2 -1
- data/lib/fontisan/stitcher/unique_glyph_name.rb +46 -0
- data/lib/fontisan/stitcher.rb +4 -1
- data/lib/fontisan/subset/shared_state.rb +42 -0
- data/lib/fontisan/subset/subset_context.rb +20 -0
- data/lib/fontisan/subset/table_strategy/cbdt.rb +30 -0
- data/lib/fontisan/subset/table_strategy/cblc.rb +27 -0
- data/lib/fontisan/subset/table_strategy/cmap.rb +146 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_placement.rb +20 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_strike_plan.rb +45 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_subset_plan.rb +62 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_subsetter.rb +182 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_subtable_plan.rb +59 -0
- data/lib/fontisan/subset/table_strategy/glyf.rb +29 -0
- data/lib/fontisan/subset/table_strategy/glyf_loca_builder.rb +146 -0
- data/lib/fontisan/subset/table_strategy/head.rb +42 -0
- data/lib/fontisan/subset/table_strategy/hhea.rb +67 -0
- data/lib/fontisan/subset/table_strategy/hmtx.rb +52 -0
- data/lib/fontisan/subset/table_strategy/loca.rb +41 -0
- data/lib/fontisan/subset/table_strategy/maxp.rb +23 -0
- data/lib/fontisan/subset/table_strategy/name.rb +19 -0
- data/lib/fontisan/subset/table_strategy/os2.rb +21 -0
- data/lib/fontisan/subset/table_strategy/pass_through.rb +20 -0
- data/lib/fontisan/subset/table_strategy/post.rb +37 -0
- data/lib/fontisan/subset/table_strategy.rb +75 -0
- data/lib/fontisan/subset/table_subsetter.rb +49 -616
- data/lib/fontisan/subset.rb +3 -0
- data/lib/fontisan/svg/standalone_glyph.rb +4 -1
- data/lib/fontisan/svg_to_glyf/assembler.rb +6 -3
- data/lib/fontisan/svg_to_glyf/geometry/transform_parser.rb +11 -4
- data/lib/fontisan/svg_to_glyf/geometry.rb +4 -2
- data/lib/fontisan/tables/cbdt.rb +66 -121
- data/lib/fontisan/tables/cblc.rb +110 -231
- data/lib/fontisan/tables/cblc_big_glyph_metrics.rb +28 -0
- data/lib/fontisan/tables/cblc_bitmap_size.rb +67 -0
- data/lib/fontisan/tables/cblc_glyph_bitmap_location.rb +28 -0
- data/lib/fontisan/tables/cblc_index_subtable.rb +104 -0
- data/lib/fontisan/tables/cblc_index_subtable_array_entry.rb +20 -0
- data/lib/fontisan/tables/cblc_index_subtable_format_parser.rb +150 -0
- data/lib/fontisan/tables/cblc_index_subtable_header.rb +18 -0
- data/lib/fontisan/tables/cblc_sbit_line_metrics.rb +28 -0
- data/lib/fontisan/tables/cff/cff2_charstring_builder.rb +11 -5
- data/lib/fontisan/tables/cff.rb +2 -1
- data/lib/fontisan/tables/cff2/index_builder.rb +2 -1
- data/lib/fontisan/tables.rb +12 -0
- data/lib/fontisan/ufo/cli.rb +4 -1
- data/lib/fontisan/ufo/compile/cbdt_cblc.rb +2 -1
- data/lib/fontisan/ufo/compile/cff.rb +6 -2
- data/lib/fontisan/ufo/compile/cmap.rb +11 -5
- data/lib/fontisan/ufo/compile/cpal.rb +2 -1
- data/lib/fontisan/ufo/compile/feature_writers/gdef.rb +3 -1
- data/lib/fontisan/ufo/compile/feature_writers/mark_family_base.rb +3 -1
- data/lib/fontisan/ufo/compile/feature_writers.rb +2 -1
- data/lib/fontisan/ufo/compile/filters/cubic_to_quadratic.rb +4 -2
- data/lib/fontisan/ufo/compile/filters/remove_overlaps.rb +2 -1
- data/lib/fontisan/ufo/compile/fvar.rb +7 -3
- data/lib/fontisan/ufo/compile/glyf_loca.rb +4 -2
- data/lib/fontisan/ufo/compile/gvar.rb +5 -2
- data/lib/fontisan/ufo/compile/head.rb +2 -1
- data/lib/fontisan/ufo/compile/item_variation_store.rb +2 -1
- data/lib/fontisan/ufo/compile/name.rb +3 -1
- data/lib/fontisan/ufo/compile/sbix.rb +2 -1
- data/lib/fontisan/ufo/compile/stat.rb +6 -3
- data/lib/fontisan/ufo/convert/from_bin_data.rb +6 -2
- data/lib/fontisan/ufo/convert/to_dfont.rb +6 -2
- data/lib/fontisan/ufo/convert/to_otc.rb +4 -1
- data/lib/fontisan/ufo/convert/to_postscript.rb +8 -3
- data/lib/fontisan/ufo/convert/to_woff.rb +8 -3
- data/lib/fontisan/ufo/convert/to_woff2.rb +8 -3
- data/lib/fontisan/ufo/convert.rb +4 -1
- data/lib/fontisan/ufo/glyph_exists_error.rb +23 -0
- data/lib/fontisan/ufo/info.rb +4 -2
- data/lib/fontisan/ufo/layer.rb +42 -2
- data/lib/fontisan/ufo/plist.rb +7 -2
- data/lib/fontisan/ufo/reader.rb +3 -1
- data/lib/fontisan/ufo/writer.rb +13 -5
- data/lib/fontisan/ufo.rb +2 -1
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/collection_decoder.rb +4 -1
- data/lib/fontisan/woff2/collection_encoder.rb +4 -1
- data/lib/fontisan/woff2/glyf_canonicalizer.rb +2 -1
- data/lib/fontisan/woff2/glyf_loca_reconstruct.rb +5 -2
- data/lib/fontisan/woff2/sfnt_checksum.rb +2 -1
- data/lib/fontisan.rb +0 -1
- metadata +36 -4
- data/lib/fontisan/tasks/fixture_downloader.rb +0 -162
- data/lib/fontisan/tasks.rb +0 -11
data/docs/cli/subset.md
CHANGED
|
@@ -18,8 +18,23 @@ fontisan subset <font> [options]
|
|
|
18
18
|
|--------|-------------|
|
|
19
19
|
| `--chars TEXT` | Characters to include |
|
|
20
20
|
| `--file FILE` | File containing characters |
|
|
21
|
+
| `--unicodes RANGE` | Unicode ranges (`"U+0000-007F"`) |
|
|
22
|
+
| `--glyphs LIST` | Glyph names/IDs |
|
|
21
23
|
| `--output FILE` | Output file path |
|
|
22
|
-
| `--format FORMAT` | Output format |
|
|
24
|
+
| `--format FORMAT` | Output format (`ttf`, `woff`, `woff2`, …) |
|
|
25
|
+
| `--profile PROFILE` | Subsetting profile (default: `pdf`) |
|
|
26
|
+
| `--retain-gids` | Preserve original glyph IDs |
|
|
27
|
+
|
|
28
|
+
## Profiles
|
|
29
|
+
|
|
30
|
+
| Profile | Tables included | Use case |
|
|
31
|
+
|---------|-----------------|----------|
|
|
32
|
+
| `pdf` | core + glyf/loca | PDF embedding (smallest) |
|
|
33
|
+
| `web` | core + glyf/loca + GSUB/GPOS + **CBDT/CBLC** | Web fonts that retain color-emoji bitmaps |
|
|
34
|
+
| `minimal` | core only | Smallest valid font |
|
|
35
|
+
| `full` | every standard table | Lossless re-export |
|
|
36
|
+
|
|
37
|
+
The `web` profile is the one to reach for when the subset must still render color emoji in a browser: it preserves the CBDT (bitmap data) and CBLC (bitmap location) tables, and the paired subsetter rewrites them to keep only the bitmaps for the retained glyphs.
|
|
23
38
|
|
|
24
39
|
## Examples
|
|
25
40
|
|
|
@@ -27,13 +42,17 @@ fontisan subset <font> [options]
|
|
|
27
42
|
# Subset to specific characters
|
|
28
43
|
fontisan subset font.ttf --chars "ABCDEF" --output subset.ttf
|
|
29
44
|
|
|
30
|
-
#
|
|
31
|
-
fontisan subset
|
|
45
|
+
# Web subset that retains color-emoji bitmaps
|
|
46
|
+
fontisan subset NotoColorEmoji.ttf \
|
|
47
|
+
--chars "😀😁😂🤣😊" \
|
|
48
|
+
--profile web \
|
|
49
|
+
--format woff2 \
|
|
50
|
+
--output emoji.woff2
|
|
32
51
|
|
|
33
|
-
#
|
|
34
|
-
fontisan subset font.ttf --chars "
|
|
52
|
+
# PDF embedding profile (drops CBDT/CBLC, smaller output)
|
|
53
|
+
fontisan subset font.ttf --chars "ABC" --profile pdf --output pdf-subset.ttf
|
|
35
54
|
```
|
|
36
55
|
|
|
37
56
|
## Detailed Documentation
|
|
38
57
|
|
|
39
|
-
For comprehensive documentation including unicode ranges and
|
|
58
|
+
For comprehensive documentation including unicode ranges, profile selection, and CBDT/CBLC behavior, see the [subset command guide](/guide/cli/subset).
|
data/docs/guide/cli/subset.md
CHANGED
|
@@ -28,6 +28,7 @@ fontisan subset FONT [options]
|
|
|
28
28
|
| `--glyphs LIST` | Glyph names/IDs |
|
|
29
29
|
| `--output PATH` | Output file |
|
|
30
30
|
| `--output-format FORMAT` | Output format |
|
|
31
|
+
| `--profile PROFILE` | Subsetting profile (`pdf`, `web`, `minimal`, `full`; default `pdf`) |
|
|
31
32
|
| `--retain-gids` | Retain glyph IDs |
|
|
32
33
|
|
|
33
34
|
## Examples
|
|
@@ -86,6 +87,36 @@ fontisan subset font.ttf --chars "ABC" --output-format woff2 --output subset.wof
|
|
|
86
87
|
|
|
87
88
|
## Subsetting Strategies
|
|
88
89
|
|
|
90
|
+
### Profile selection
|
|
91
|
+
|
|
92
|
+
The `--profile` option controls which font tables the subset retains. The right choice depends on what the subset is for:
|
|
93
|
+
|
|
94
|
+
| Profile | When to use | Notable tables dropped |
|
|
95
|
+
|---------|-------------|------------------------|
|
|
96
|
+
| `pdf` (default) | PDF embedding, smallest valid output | GSUB, GPOS, CBDT, CBLC |
|
|
97
|
+
| `web` | Browser/web font, **retains color-emoji bitmaps** | nothing that browsers need |
|
|
98
|
+
| `minimal` | Absolute smallest core that still renders | glyf/loca, CBDT/CBLC, GSUB/GPOS |
|
|
99
|
+
| `full` | Lossless re-export | (none) |
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Web subset that keeps color-emoji bitmaps so 😀 renders in color
|
|
103
|
+
fontisan subset NotoColorEmoji.ttf \
|
|
104
|
+
--chars "😀😁😂🤣😊" \
|
|
105
|
+
--profile web \
|
|
106
|
+
--output-format woff2 \
|
|
107
|
+
--output emoji.woff2
|
|
108
|
+
|
|
109
|
+
# PDF embedding — drops color bitmaps for size
|
|
110
|
+
fontisan subset NotoColorEmoji.ttf \
|
|
111
|
+
--chars "😀" \
|
|
112
|
+
--profile pdf \
|
|
113
|
+
--output emoji-pdf.ttf
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### Web profile and CBDT/CBLC
|
|
117
|
+
|
|
118
|
+
The `web` profile is the only built-in profile that retains CBDT (Color Bitmap Data) and CBLC (Color Bitmap Location) tables. The subsetter walks CBLC to find each retained glyph's bitmap block, rewrites CBDT to keep only those blocks, and rebuilds CBLC's IndexSubTableArray to point at the new offsets. Glyph IDs are remapped through the subset's `GlyphMapping`, so the output CBLC references subset GIDs, not source GIDs.
|
|
119
|
+
|
|
89
120
|
### Web Font Optimization
|
|
90
121
|
|
|
91
122
|
```bash
|
|
@@ -144,6 +144,23 @@ fontisan convert bitmap-font.ttf --to ttf --output bitmap-font.ttf
|
|
|
144
144
|
fontisan convert bitmap-font.ttf --to ttf --no-bitmaps --output outline-only.ttf
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
### Subsetting color-emoji fonts
|
|
148
|
+
|
|
149
|
+
The `web` subsetting profile retains CBDT and CBLC tables and rewrites them to keep only the bitmaps for retained glyphs. Use it when subsetting a color-emoji source for browser delivery:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# Keep only 😀 + 😁 bitmaps; output WOFF2 for web
|
|
153
|
+
fontisan subset NotoColorEmoji.ttf \
|
|
154
|
+
--chars "😀😁" \
|
|
155
|
+
--profile web \
|
|
156
|
+
--output-format woff2 \
|
|
157
|
+
--output emoji-subset.woff2
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Other profiles (`pdf`, `minimal`, `full`) drop CBDT/CBLC — pick them when the subset doesn't need color-emoji rendering (e.g. PDF embedding where the subsetter is just selecting glyphs for outlines).
|
|
161
|
+
|
|
162
|
+
See the [subset CLI docs](/cli/subset) and `docs/STITCHER_GUIDE.adoc` (CBDT/CBLC passthrough section) for the full story, including the GID-stability caveat when CBDT and outline donors cover overlapping codepoint ranges in stitch mode.
|
|
163
|
+
|
|
147
164
|
## Comparison
|
|
148
165
|
|
|
149
166
|
| Feature | sbix | CBDT/CBLC |
|
data/lib/fontisan/cli.rb
CHANGED
|
@@ -360,7 +360,8 @@ module Fontisan
|
|
|
360
360
|
|
|
361
361
|
# Handle --show-options
|
|
362
362
|
if options[:show_options]
|
|
363
|
-
show_recommended_options(source_format,
|
|
363
|
+
show_recommended_options(source_format,
|
|
364
|
+
options[:to].is_a?(Array) ? options[:to].first : options[:to])
|
|
364
365
|
return
|
|
365
366
|
end
|
|
366
367
|
|
|
@@ -95,7 +95,8 @@ module Fontisan
|
|
|
95
95
|
#
|
|
96
96
|
# @return [String] TTC header binary
|
|
97
97
|
def write_ttc_header
|
|
98
|
-
[TTC_TAG, VERSION_1_0_MAJOR, VERSION_1_0_MINOR,
|
|
98
|
+
[TTC_TAG, VERSION_1_0_MAJOR, VERSION_1_0_MINOR,
|
|
99
|
+
@fonts.size].pack("a4nnN")
|
|
99
100
|
end
|
|
100
101
|
|
|
101
102
|
# Write offset table
|
|
@@ -81,7 +81,13 @@ module Fontisan
|
|
|
81
81
|
Check.new(
|
|
82
82
|
name: :glyph_cap,
|
|
83
83
|
passed: over.empty?,
|
|
84
|
-
message: over.empty?
|
|
84
|
+
message: if over.empty?
|
|
85
|
+
nil
|
|
86
|
+
else
|
|
87
|
+
"faces over cap: #{over.map do |s|
|
|
88
|
+
"##{s.index}=#{s.glyph_count}"
|
|
89
|
+
end.join(', ')}"
|
|
90
|
+
end,
|
|
85
91
|
)
|
|
86
92
|
end
|
|
87
93
|
|
data/lib/fontisan/commands.rb
CHANGED
|
@@ -21,7 +21,8 @@ module Fontisan
|
|
|
21
21
|
autoload :TablesCommand, "fontisan/commands/tables_command"
|
|
22
22
|
autoload :UnicodeCommand, "fontisan/commands/unicode_command"
|
|
23
23
|
autoload :UnpackCommand, "fontisan/commands/unpack_command"
|
|
24
|
-
autoload :ValidateCollectionCommand,
|
|
24
|
+
autoload :ValidateCollectionCommand,
|
|
25
|
+
"fontisan/commands/validate_collection_command"
|
|
25
26
|
autoload :ValidateCommand, "fontisan/commands/validate_command"
|
|
26
27
|
autoload :VariableCommand, "fontisan/commands/variable_command"
|
|
27
28
|
end
|
|
@@ -173,7 +173,12 @@ module Fontisan
|
|
|
173
173
|
io.read(8) # signature + flavor
|
|
174
174
|
io.rewind
|
|
175
175
|
flavor = File.binread(path, 4, 4)&.unpack1("N")
|
|
176
|
-
flavor == Woff2::CollectionDecoder::TTC_FLAVOR
|
|
176
|
+
if flavor == Woff2::CollectionDecoder::TTC_FLAVOR
|
|
177
|
+
:woff2_collection
|
|
178
|
+
else
|
|
179
|
+
(raise Error,
|
|
180
|
+
"Not a WOFF2 collection: #{path}")
|
|
181
|
+
end
|
|
177
182
|
elsif Parsers::DfontParser.dfont?(io)
|
|
178
183
|
:dfont
|
|
179
184
|
else
|
|
@@ -192,7 +192,9 @@ palette_index: 0)
|
|
|
192
192
|
def build_glyph_to_codepoints(cmap)
|
|
193
193
|
return {} unless cmap
|
|
194
194
|
|
|
195
|
-
cmap.unicode_mappings.each_with_object(Hash.new
|
|
195
|
+
cmap.unicode_mappings.each_with_object(Hash.new do |h, k|
|
|
196
|
+
h[k] = []
|
|
197
|
+
end) do |(cp, gid), h|
|
|
196
198
|
h[gid] << cp
|
|
197
199
|
end
|
|
198
200
|
rescue StandardError => e
|
|
@@ -60,6 +60,19 @@ module Fontisan
|
|
|
60
60
|
# Outline data is intentionally absent — the bitmap lives in
|
|
61
61
|
# the CBDT table that #propagate_tables_into will copy later.
|
|
62
62
|
#
|
|
63
|
+
# == Why placeholders are name-deconflicted
|
|
64
|
+
#
|
|
65
|
+
# Placeholders are named after the CBDT source's gid ("gid{N}").
|
|
66
|
+
# Outline donors compiled earlier into the same target use the
|
|
67
|
+
# SAME naming scheme (see Source#extract_truetype_glyph), so a
|
|
68
|
+
# CBDT placeholder at "gid110" would collide with an outline
|
|
69
|
+
# glyph at "gid110" from a different donor. Layer#add raises on
|
|
70
|
+
# conflict (it never silently overwrites — that path previously
|
|
71
|
+
# erased real outline glyphs and dropped their cmap entries,
|
|
72
|
+
# causing the CJK Ext G loss in Essenfont-Regular.ttc). We
|
|
73
|
+
# allocate a unique name via UniqueGlyphName so each placeholder
|
|
74
|
+
# lands at its own GID slot without touching the outlines.
|
|
75
|
+
#
|
|
63
76
|
# Glyphs are NOT registered with the deduplicator: each CBDT
|
|
64
77
|
# glyph is unique to its source, and deduplication would
|
|
65
78
|
# incorrectly collapse distinct bitmaps.
|
|
@@ -69,7 +82,10 @@ module Fontisan
|
|
|
69
82
|
def add_placeholder_glyphs(source, target)
|
|
70
83
|
ufo = source.font.is_a?(Ufo::Font) ? source.font : nil
|
|
71
84
|
if ufo
|
|
72
|
-
ufo.glyphs.each_value
|
|
85
|
+
ufo.glyphs.each_value do |g|
|
|
86
|
+
name = UniqueGlyphName.in(target, g.name)
|
|
87
|
+
target.layers.default_layer.add(GlyphCloner.clone(g, name: name))
|
|
88
|
+
end
|
|
73
89
|
return
|
|
74
90
|
end
|
|
75
91
|
|
|
@@ -82,7 +98,8 @@ module Fontisan
|
|
|
82
98
|
mappings.each { |cp, gid| gid_cps[gid] << cp }
|
|
83
99
|
|
|
84
100
|
num_glyphs.times do |gid|
|
|
85
|
-
|
|
101
|
+
base_name = gid.zero? ? ".notdef" : "gid#{gid}"
|
|
102
|
+
name = UniqueGlyphName.in(target, base_name)
|
|
86
103
|
glyph = Ufo::Glyph.new(name: name)
|
|
87
104
|
glyph.width = 0
|
|
88
105
|
gid_cps[gid].each { |cp| glyph.add_unicode(cp) }
|
|
@@ -100,6 +117,31 @@ module Fontisan
|
|
|
100
117
|
#
|
|
101
118
|
# No-op when called with a non-CBDT source or nil.
|
|
102
119
|
#
|
|
120
|
+
# == Limitation: GID stability
|
|
121
|
+
#
|
|
122
|
+
# The propagation copies CBDT/CBLC bytes verbatim. CBLC indexes
|
|
123
|
+
# glyphs by SOURCE GID — the GIDs of the CBDT donor. The compiled
|
|
124
|
+
# font's GIDs may differ because:
|
|
125
|
+
#
|
|
126
|
+
# 1. +add_placeholder_glyphs+ renames CBDT placeholders via
|
|
127
|
+
# UniqueGlyphName when their default "gid{N}" name collides
|
|
128
|
+
# with outline glyphs sharing the same donor-gid scheme
|
|
129
|
+
# (see Layer's naming contract).
|
|
130
|
+
# 2. The compiler assigns GIDs in target-namespace order, which
|
|
131
|
+
# is not the same as source-GID order once renaming happens.
|
|
132
|
+
#
|
|
133
|
+
# The bitmaps line up correctly only when the CBDT source and
|
|
134
|
+
# every outline source cover disjoint codepoint ranges (the
|
|
135
|
+
# Essenfont TTC case: emoji vs CJK Ext G). When ranges overlap,
|
|
136
|
+
# CBLC's source-GID-indexed bitmaps may point at the wrong
|
|
137
|
+
# compiled glyphs and the colour rendering for affected
|
|
138
|
+
# codepoints will fall back to outlines.
|
|
139
|
+
#
|
|
140
|
+
# A proper fix requires a CBLC rebuild pass: walk the compiled
|
|
141
|
+
# font's cmap to find the new GID for every CBDT-covered source
|
|
142
|
+
# glyph, then rewrite CBLC's IndexSubTableArray + IndexSubTable
|
|
143
|
+
# offsets to match. Tracked as a follow-up.
|
|
144
|
+
#
|
|
103
145
|
# @param source [Stitcher::Source, nil] the CBDT source
|
|
104
146
|
# @param path [String] compiled font file to rewrite
|
|
105
147
|
def propagate_tables_into(source, path)
|
|
@@ -126,26 +168,6 @@ module Fontisan
|
|
|
126
168
|
sfnt = tables.key?("CFF ") || tables.key?("CFF2") ? 0x4F54544F : 0x00010000
|
|
127
169
|
FontWriter.write_to_file(tables, path, sfnt_version: sfnt)
|
|
128
170
|
end
|
|
129
|
-
|
|
130
|
-
private
|
|
131
|
-
|
|
132
|
-
def clone_glyph(original, name:)
|
|
133
|
-
copy = Ufo::Glyph.new(name: name)
|
|
134
|
-
copy.width = original.width
|
|
135
|
-
copy.height = original.height
|
|
136
|
-
original.contours.each { |c| copy.add_contour(clone_contour(c)) }
|
|
137
|
-
original.components.each { |c| copy.add_component(c) }
|
|
138
|
-
original.anchors.each { |a| copy.add_anchor(a) }
|
|
139
|
-
original.guidelines.each { |g| copy.add_guideline(g) }
|
|
140
|
-
copy
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def clone_contour(original)
|
|
144
|
-
points = original.points.map do |p|
|
|
145
|
-
Ufo::Point.new(x: p.x, y: p.y, type: p.type, smooth: p.smooth)
|
|
146
|
-
end
|
|
147
|
-
Ufo::Contour.new(points)
|
|
148
|
-
end
|
|
149
171
|
end
|
|
150
172
|
end
|
|
151
173
|
end
|
|
@@ -9,7 +9,8 @@ module Fontisan
|
|
|
9
9
|
|
|
10
10
|
# Return value of {Stitcher#write_collection}. Carries the output path,
|
|
11
11
|
# total bytes, and one {SubfontStats} per declared subfont.
|
|
12
|
-
CollectionResult = Struct.new(:path, :bytes, :subfonts,
|
|
12
|
+
CollectionResult = Struct.new(:path, :bytes, :subfonts,
|
|
13
|
+
keyword_init: true) do
|
|
13
14
|
def face_count
|
|
14
15
|
subfonts.size
|
|
15
16
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
class Stitcher
|
|
5
|
+
# Deep-clone helper for UFO glyphs. Stateless — every call returns
|
|
6
|
+
# a fresh +Ufo::Glyph+ with copied contours, components, anchors,
|
|
7
|
+
# and guidelines plus the original's width/height.
|
|
8
|
+
#
|
|
9
|
+
# Extracted as a collaborator so +GlyphCopier+ and +CbdtPropagator+
|
|
10
|
+
# share one implementation of the clone algorithm. Both previously
|
|
11
|
+
# defined identical +clone_glyph+ / +clone_contour+ private methods
|
|
12
|
+
# — a DRY violation that risked drifting apart as the glyph model
|
|
13
|
+
# gained fields.
|
|
14
|
+
module GlyphCloner
|
|
15
|
+
# @param original [Ufo::Glyph] glyph to copy
|
|
16
|
+
# @param name [String, nil] name for the copy; defaults to the
|
|
17
|
+
# original's name (used by callers that have already allocated
|
|
18
|
+
# a unique name via +UniqueGlyphName+).
|
|
19
|
+
# @return [Ufo::Glyph] a fresh glyph with the original's geometry
|
|
20
|
+
# and metrics, sharing no mutable state with it.
|
|
21
|
+
def self.clone(original, name: nil)
|
|
22
|
+
copy = Ufo::Glyph.new(name: name || original.name)
|
|
23
|
+
copy.width = original.width
|
|
24
|
+
copy.height = original.height
|
|
25
|
+
original.contours.each { |c| copy.add_contour(clone_contour(c)) }
|
|
26
|
+
original.components.each { |c| copy.add_component(c) }
|
|
27
|
+
original.anchors.each { |a| copy.add_anchor(a) }
|
|
28
|
+
original.guidelines.each { |g| copy.add_guideline(g) }
|
|
29
|
+
copy
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.clone_contour(original)
|
|
33
|
+
points = original.points.map do |p|
|
|
34
|
+
Ufo::Point.new(x: p.x, y: p.y, type: p.type, smooth: p.smooth)
|
|
35
|
+
end
|
|
36
|
+
Ufo::Contour.new(points)
|
|
37
|
+
end
|
|
38
|
+
private_class_method :clone_contour
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Fontisan
|
|
4
4
|
class Stitcher
|
|
@@ -74,7 +74,7 @@ module Fontisan
|
|
|
74
74
|
if canonical && target.glyphs.key?(canonical)
|
|
75
75
|
add_extra_unicode(target, canonical, binding[:codepoint])
|
|
76
76
|
else
|
|
77
|
-
name =
|
|
77
|
+
name = UniqueGlyphName.in(target, glyph.name)
|
|
78
78
|
copy_glyph_into(target, name: name, source: binding[:source],
|
|
79
79
|
donor_gid: binding[:donor_gid],
|
|
80
80
|
codepoint: binding[:codepoint])
|
|
@@ -89,11 +89,12 @@ module Fontisan
|
|
|
89
89
|
bindings.sort_by { |b| [b[:codepoint] || Float::INFINITY, b[:donor_gid]] }
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
-
def copy_glyph_into(target_font, name:, source:, donor_gid:,
|
|
92
|
+
def copy_glyph_into(target_font, name:, source:, donor_gid:,
|
|
93
|
+
codepoint: nil)
|
|
93
94
|
original = source.glyph_for_gid(donor_gid)
|
|
94
95
|
return unless original
|
|
95
96
|
|
|
96
|
-
copy =
|
|
97
|
+
copy = GlyphCloner.clone(original, name: name)
|
|
97
98
|
copy.add_unicode(codepoint) if codepoint
|
|
98
99
|
target_font.layers.default_layer.add(copy)
|
|
99
100
|
end
|
|
@@ -104,36 +105,6 @@ module Fontisan
|
|
|
104
105
|
glyph = target_font.glyph(glyph_name)
|
|
105
106
|
glyph.add_unicode(codepoint) unless glyph.unicodes.include?(codepoint)
|
|
106
107
|
end
|
|
107
|
-
|
|
108
|
-
def unique_target_name(target_font, base_name)
|
|
109
|
-
return base_name unless target_font.glyphs.key?(base_name)
|
|
110
|
-
|
|
111
|
-
suffix = 1
|
|
112
|
-
loop do
|
|
113
|
-
candidate = "#{base_name}.#{suffix}"
|
|
114
|
-
return candidate unless target_font.glyphs.key?(candidate)
|
|
115
|
-
|
|
116
|
-
suffix += 1
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def clone_glyph(original, name:)
|
|
121
|
-
copy = Ufo::Glyph.new(name: name)
|
|
122
|
-
copy.width = original.width
|
|
123
|
-
copy.height = original.height
|
|
124
|
-
original.contours.each { |c| copy.add_contour(clone_contour(c)) }
|
|
125
|
-
original.components.each { |c| copy.add_component(c) }
|
|
126
|
-
original.anchors.each { |a| copy.add_anchor(a) }
|
|
127
|
-
original.guidelines.each { |g| copy.add_guideline(g) }
|
|
128
|
-
copy
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def clone_contour(original)
|
|
132
|
-
points = original.points.map do |p|
|
|
133
|
-
Ufo::Point.new(x: p.x, y: p.y, type: p.type, smooth: p.smooth)
|
|
134
|
-
end
|
|
135
|
-
Ufo::Contour.new(points)
|
|
136
|
-
end
|
|
137
108
|
end
|
|
138
109
|
end
|
|
139
110
|
end
|
|
@@ -403,7 +403,9 @@ module Fontisan
|
|
|
403
403
|
private
|
|
404
404
|
|
|
405
405
|
def group_by_block(cp_map)
|
|
406
|
-
cp_map.each_with_object(Hash.new
|
|
406
|
+
cp_map.each_with_object(Hash.new do |h, k|
|
|
407
|
+
h[k] = []
|
|
408
|
+
end) do |(cp, label), h|
|
|
407
409
|
block_label = find_block_label(cp) || :other
|
|
408
410
|
h[block_label] << [cp, label]
|
|
409
411
|
end
|
|
@@ -274,14 +274,18 @@ module Fontisan
|
|
|
274
274
|
private
|
|
275
275
|
|
|
276
276
|
def group_by_script(cp_map)
|
|
277
|
-
cp_map.each_with_object(Hash.new
|
|
277
|
+
cp_map.each_with_object(Hash.new do |h, k|
|
|
278
|
+
h[k] = []
|
|
279
|
+
end) do |(cp, label), h|
|
|
278
280
|
script = script_of_codepoint(cp)
|
|
279
281
|
h[script] << [cp, label]
|
|
280
282
|
end
|
|
281
283
|
end
|
|
282
284
|
|
|
283
285
|
def script_of_codepoint(cp)
|
|
284
|
-
block_label = ByBlock::BLOCKS.find
|
|
286
|
+
block_label = ByBlock::BLOCKS.find do |_label, range|
|
|
287
|
+
range.cover?(cp)
|
|
288
|
+
end&.first
|
|
285
289
|
return :other unless block_label
|
|
286
290
|
|
|
287
291
|
SCRIPT_OF_BLOCK[block_label] || :other
|
|
@@ -321,7 +321,8 @@ module Fontisan
|
|
|
321
321
|
compound.components.each do |component|
|
|
322
322
|
next unless component.args_are_xy?
|
|
323
323
|
|
|
324
|
-
raw = cache[:glyf].glyph_for(component.glyph_index, cache[:loca],
|
|
324
|
+
raw = cache[:glyf].glyph_for(component.glyph_index, cache[:loca],
|
|
325
|
+
cache[:head])
|
|
325
326
|
next unless raw
|
|
326
327
|
|
|
327
328
|
matrix = component.transformation_matrix
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
class Stitcher
|
|
5
|
+
# Stateless glyph-name deconfliction for the Stitcher's target UFO.
|
|
6
|
+
#
|
|
7
|
+
# The Layer's contract (see +Ufo::Layer+) is that two glyphs cannot
|
|
8
|
+
# share a name — +#add+ raises on conflict. Most Stitcher call sites
|
|
9
|
+
# already know the name they want is free (e.g. they just synthesised
|
|
10
|
+
# it). The two that don't — GlyphCopier (outline donors may share
|
|
11
|
+
# post-table names) and CbdtPropagator (placeholders named "gid{N}"
|
|
12
|
+
# collide with outline glyphs sharing the same donor-gid scheme) —
|
|
13
|
+
# ask this helper to allocate a non-colliding name first.
|
|
14
|
+
#
|
|
15
|
+
# Centralising the rule here keeps the deconfliction algorithm in
|
|
16
|
+
# one place: the suffix-search loop, the separator, the order in
|
|
17
|
+
# which candidates are tried. Changing the rule (e.g. to a UUID
|
|
18
|
+
# scheme) is a one-file edit.
|
|
19
|
+
module UniqueGlyphName
|
|
20
|
+
# Separator between the base name and the disambiguating suffix.
|
|
21
|
+
# A literal period so the generated name ("gid110.1") still
|
|
22
|
+
# reads as a variant of the original.
|
|
23
|
+
SUFFIX_SEPARATOR = "."
|
|
24
|
+
|
|
25
|
+
# @param target [Ufo::Font, #glyphs] the target whose +#glyphs+
|
|
26
|
+
# hash is the namespace being deconflicted against. Accepts the
|
|
27
|
+
# UFO font directly so callers don't have to dig out the layer.
|
|
28
|
+
# @param base_name [String, #to_s] the requested name.
|
|
29
|
+
# @return [String] +base_name+ unchanged if no glyph with that
|
|
30
|
+
# name exists in +target.glyphs+; otherwise the first
|
|
31
|
+
# +"base.1"+, +"base.2"+, ... that does not exist.
|
|
32
|
+
def self.in(target, base_name)
|
|
33
|
+
base = base_name.to_s
|
|
34
|
+
return base unless target.glyphs.key?(base)
|
|
35
|
+
|
|
36
|
+
suffix = 1
|
|
37
|
+
loop do
|
|
38
|
+
candidate = "#{base}#{SUFFIX_SEPARATOR}#{suffix}"
|
|
39
|
+
return candidate unless target.glyphs.key?(candidate)
|
|
40
|
+
|
|
41
|
+
suffix += 1
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/fontisan/stitcher.rb
CHANGED
|
@@ -35,7 +35,9 @@ module Fontisan
|
|
|
35
35
|
autoload :FormatMetadata, "fontisan/stitcher/format_metadata"
|
|
36
36
|
autoload :PartitionStrategy, "fontisan/stitcher/partition_strategy"
|
|
37
37
|
autoload :CbdtPropagator, "fontisan/stitcher/cbdt_propagator"
|
|
38
|
+
autoload :GlyphCloner, "fontisan/stitcher/glyph_cloner"
|
|
38
39
|
autoload :GlyphCopier, "fontisan/stitcher/glyph_copier"
|
|
40
|
+
autoload :UniqueGlyphName, "fontisan/stitcher/unique_glyph_name"
|
|
39
41
|
|
|
40
42
|
# Internal: pairs a compiled loaded font with its stats so
|
|
41
43
|
# +write_collection+ can build the collection and the result from a
|
|
@@ -148,7 +150,8 @@ module Fontisan
|
|
|
148
150
|
Dir.mktmpdir do |dir|
|
|
149
151
|
sub_path = File.join(dir, "sub#{subfont_name}#{metadata.extension}")
|
|
150
152
|
metadata.compiler_class.new(target).compile(output_path: sub_path)
|
|
151
|
-
cbdt_propagator.propagate_tables_into(cbdt_propagator.cbdt_source,
|
|
153
|
+
cbdt_propagator.propagate_tables_into(cbdt_propagator.cbdt_source,
|
|
154
|
+
sub_path)
|
|
152
155
|
|
|
153
156
|
loaded = Fontisan::FontLoader.load(sub_path)
|
|
154
157
|
stats = SubfontStats.new(
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
# Per-subsetting shared state.
|
|
6
|
+
#
|
|
7
|
+
# Several subset strategies depend on values computed by earlier
|
|
8
|
+
# strategies during the same subsetting run:
|
|
9
|
+
#
|
|
10
|
+
# - `subset_max_advance` is computed by the Hmtx strategy and read
|
|
11
|
+
# by Hhea (because hhea is processed alphabetically before hmtx).
|
|
12
|
+
# - `glyf_data`, `loca_offsets`, and `subset_bbox` are computed by
|
|
13
|
+
# the Glyf strategy and consumed by Loca and Head.
|
|
14
|
+
#
|
|
15
|
+
# Rather than giving each TableStrategy access to the entire
|
|
16
|
+
# TableSubsetter (which would couple them to internals), the
|
|
17
|
+
# cross-strategy state lives here. Each strategy receives a Context
|
|
18
|
+
# exposing [font], [mapping], [options], and this [SharedState].
|
|
19
|
+
class SharedState
|
|
20
|
+
# @return [String, nil] binary glyf data built by the Glyf strategy
|
|
21
|
+
attr_accessor :glyf_data
|
|
22
|
+
|
|
23
|
+
# @return [Array<Integer>, nil] loca offsets produced by the Glyf strategy
|
|
24
|
+
attr_accessor :loca_offsets
|
|
25
|
+
|
|
26
|
+
# @return [Array(Integer, Integer, Integer, Integer), nil] union
|
|
27
|
+
# xMin/yMin/xMax/yMax over the subset's glyphs
|
|
28
|
+
attr_accessor :subset_bbox
|
|
29
|
+
|
|
30
|
+
# @return [Integer] largest advanceWidth seen while subsetting hmtx
|
|
31
|
+
attr_accessor :subset_max_advance
|
|
32
|
+
|
|
33
|
+
# @return [TableStrategy::ColorBitmapSubsetter, nil] cached paired
|
|
34
|
+
# CBDT+CBLC subsetter so both strategies reuse the same pass
|
|
35
|
+
attr_accessor :color_bitmap_subsetter
|
|
36
|
+
|
|
37
|
+
def initialize
|
|
38
|
+
@subset_max_advance = 0
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
# Value object handed to every TableStrategy. Encapsulates the inputs
|
|
6
|
+
# (font, mapping, options) plus the cross-strategy [SharedState] so
|
|
7
|
+
# strategies don't need a back-reference to the TableSubsetter.
|
|
8
|
+
#
|
|
9
|
+
# @!attribute font
|
|
10
|
+
# @return [SfntFont]
|
|
11
|
+
# @!attribute mapping
|
|
12
|
+
# @return [GlyphMapping]
|
|
13
|
+
# @!attribute options
|
|
14
|
+
# @return [Options]
|
|
15
|
+
# @!attribute state
|
|
16
|
+
# @return [SharedState]
|
|
17
|
+
SubsetContext = Struct.new(:font, :mapping, :options, :state,
|
|
18
|
+
keyword_init: true)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# CBDT strategy: delegate to [ColorBitmapSubsetter], which produces
|
|
7
|
+
# both CBDT and CBLC bytes in a single pass. The Cblc strategy
|
|
8
|
+
# reads from the same collaborator so the two tables stay
|
|
9
|
+
# consistent.
|
|
10
|
+
class Cbdt
|
|
11
|
+
# @param context [SubsetContext]
|
|
12
|
+
# @param tag [String] "CBDT"
|
|
13
|
+
# @param table [Cbdt] parsed CBDT table (unused)
|
|
14
|
+
# @return [String] subset CBDT bytes
|
|
15
|
+
def self.call(context:, tag:, table:)
|
|
16
|
+
color_bitmap_subsetter(context).cbdt_bytes
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param context [SubsetContext]
|
|
20
|
+
# @return [ColorBitmapSubsetter] cached on the shared state so the
|
|
21
|
+
# Cblc strategy reuses the same pass
|
|
22
|
+
def self.color_bitmap_subsetter(context)
|
|
23
|
+
context.state.color_bitmap_subsetter ||= ColorBitmapSubsetter.new(font: context.font,
|
|
24
|
+
mapping: context.mapping).build
|
|
25
|
+
end
|
|
26
|
+
private_class_method :color_bitmap_subsetter
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# CBLC strategy: delegate to [ColorBitmapSubsetter] (the same
|
|
7
|
+
# collaborator used by the Cbdt strategy) so both tables stay
|
|
8
|
+
# consistent. The subsetter is built lazily on first access and
|
|
9
|
+
# cached on [SharedState].
|
|
10
|
+
class Cblc
|
|
11
|
+
# @param context [SubsetContext]
|
|
12
|
+
# @param tag [String] "CBLC"
|
|
13
|
+
# @param table [Cblc] parsed CBLC table (unused)
|
|
14
|
+
# @return [String] subset CBLC bytes
|
|
15
|
+
def self.call(context:, tag:, table:)
|
|
16
|
+
color_bitmap_subsetter(context).cblc_bytes
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.color_bitmap_subsetter(context)
|
|
20
|
+
context.state.color_bitmap_subsetter ||= ColorBitmapSubsetter.new(font: context.font,
|
|
21
|
+
mapping: context.mapping).build
|
|
22
|
+
end
|
|
23
|
+
private_class_method :color_bitmap_subsetter
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|