fontisan 0.4.23 → 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 -29
- 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/converters/collection_converter.rb +6 -1
- data/lib/fontisan/converters/svg_generator.rb +3 -1
- data/lib/fontisan/stitcher/collection_result.rb +2 -1
- data/lib/fontisan/stitcher/glyph_copier.rb +2 -1
- 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.rb +2 -1
- data/lib/fontisan/subset/table_strategy/glyf_loca_builder.rb +2 -1
- 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/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 +2 -1
- 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/info.rb +4 -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/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 +2 -3
- data/lib/fontisan/tasks/fixture_downloader.rb +0 -162
- data/lib/fontisan/tasks.rb +0 -11
data/docs/api/layer.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Ufo::Layer
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# `Ufo::Layer`
|
|
6
|
+
|
|
7
|
+
A single layer in a UFO source. A `Layer` holds a set of glyphs keyed by name. The default layer is `public.default` per UFO 3.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
layer = Fontisan::Ufo::Layer.new # => "public.default"
|
|
11
|
+
layer.add(Fontisan::Ufo::Glyph.new(name: "A"))
|
|
12
|
+
layer["A"] # => the glyph
|
|
13
|
+
layer.size # => 1
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Naming contract
|
|
17
|
+
|
|
18
|
+
Glyph names are the layer's primary key. Two distinct glyphs with the same name cannot coexist — adding the second would silently destroy the first, a class of bug that has historically cost real data (e.g. the CJK Ext G cmap loss when CBDT placeholders collided with outline glyphs sharing `"gid{N}"` names).
|
|
19
|
+
|
|
20
|
+
To make that contract unbreakable, the API exposes two methods with named semantics:
|
|
21
|
+
|
|
22
|
+
| Method | Behavior on conflict |
|
|
23
|
+
|--------|----------------------|
|
|
24
|
+
| `Layer#add(glyph)` | **Raises** `Ufo::GlyphExistsError` (the safe default) |
|
|
25
|
+
| `Layer#put(glyph)` | Overwrites the existing glyph (explicit replace) |
|
|
26
|
+
|
|
27
|
+
Callers that need auto-renaming — insert without giving up on a collision — call `Stitcher::UniqueGlyphName.in(target, base)` first to allocate a free name, then `Layer#add` the glyph under that name.
|
|
28
|
+
|
|
29
|
+
## API
|
|
30
|
+
|
|
31
|
+
### `Layer.new(name = "public.default")`
|
|
32
|
+
|
|
33
|
+
Initialize an empty layer.
|
|
34
|
+
|
|
35
|
+
### `#add(glyph) -> Glyph`
|
|
36
|
+
|
|
37
|
+
Insert `glyph`. Raises `Ufo::GlyphExistsError` if a glyph with the same name is already present.
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
layer = Fontisan::Ufo::Layer.new
|
|
41
|
+
layer.add(Fontisan::Ufo::Glyph.new(name: "A"))
|
|
42
|
+
|
|
43
|
+
layer.add(Fontisan::Ufo::Glyph.new(name: "A"))
|
|
44
|
+
# => raises Fontisan::Ufo::GlyphExistsError: a glyph named "A" already exists
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The original glyph is preserved when the second `add` raises — the layer is never partially mutated.
|
|
48
|
+
|
|
49
|
+
### `#put(glyph) -> Glyph`
|
|
50
|
+
|
|
51
|
+
Insert `glyph`, replacing any existing glyph with the same name. Use this when the caller has positively decided the previous glyph (if any) should be discarded.
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
layer.put(Fontisan::Ufo::Glyph.new(name: "A")) # overwrites the prior "A"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### `#[](glyph_name) -> Glyph, nil`
|
|
58
|
+
|
|
59
|
+
Look up a glyph by name.
|
|
60
|
+
|
|
61
|
+
### `#each { |glyph| ... }`
|
|
62
|
+
|
|
63
|
+
Iterate over glyphs (no ordering guarantee).
|
|
64
|
+
|
|
65
|
+
### `#size -> Integer`
|
|
66
|
+
|
|
67
|
+
Number of glyphs in the layer.
|
|
68
|
+
|
|
69
|
+
## `Ufo::GlyphExistsError`
|
|
70
|
+
|
|
71
|
+
Raised by `Layer#add` on a name conflict. Carries the colliding name so callers can branch programmatically:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
begin
|
|
75
|
+
layer.add(glyph)
|
|
76
|
+
rescue Fontisan::Ufo::GlyphExistsError => e
|
|
77
|
+
puts "name #{e.name.inspect} already taken"
|
|
78
|
+
# Decide: layer.put(glyph) to overwrite, or
|
|
79
|
+
# unique = Fontisan::Stitcher::UniqueGlyphName.in(target, e.name)
|
|
80
|
+
# layer.add(glyph.dup_as(name: unique))
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The error class is a sibling of `Layer` (not nested inside it) so the namespace stays flat — both `Ufo::Layer` and `Ufo::GlyphExistsError` live directly under `Fontisan::Ufo`.
|
|
85
|
+
|
|
86
|
+
## See also
|
|
87
|
+
|
|
88
|
+
- `docs/STITCHER_GUIDE.adoc` — `UniqueGlyphName`, `GlyphCopier`, and `CbdtPropagator` all build on the Layer naming contract.
|
|
89
|
+
- `docs/UFO_COMPILATION.adoc` — how layers feed the TTF/OTF/CFF2 compilers.
|
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
|
|
@@ -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
|
|
@@ -89,7 +89,8 @@ 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
|
|
|
@@ -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
|
data/lib/fontisan/stitcher.rb
CHANGED
|
@@ -150,7 +150,8 @@ module Fontisan
|
|
|
150
150
|
Dir.mktmpdir do |dir|
|
|
151
151
|
sub_path = File.join(dir, "sub#{subfont_name}#{metadata.extension}")
|
|
152
152
|
metadata.compiler_class.new(target).compile(output_path: sub_path)
|
|
153
|
-
cbdt_propagator.propagate_tables_into(cbdt_propagator.cbdt_source,
|
|
153
|
+
cbdt_propagator.propagate_tables_into(cbdt_propagator.cbdt_source,
|
|
154
|
+
sub_path)
|
|
154
155
|
|
|
155
156
|
loaded = Fontisan::FontLoader.load(sub_path)
|
|
156
157
|
stats = SubfontStats.new(
|
|
@@ -61,7 +61,8 @@ module Fontisan
|
|
|
61
61
|
glyph_data = glyf.raw_data[offset, size]
|
|
62
62
|
|
|
63
63
|
if glyph_data.bytesize >= 10
|
|
64
|
-
_n, gx_min, gy_min, gx_max, gy_max = glyph_data[0,
|
|
64
|
+
_n, gx_min, gy_min, gx_max, gy_max = glyph_data[0,
|
|
65
|
+
10].unpack("n5")
|
|
65
66
|
gx_min = to_signed_16(gx_min)
|
|
66
67
|
gy_min = to_signed_16(gy_min)
|
|
67
68
|
gx_max = to_signed_16(gx_max)
|
|
@@ -118,7 +118,10 @@ module Fontisan
|
|
|
118
118
|
|
|
119
119
|
def bounding_box(glyph)
|
|
120
120
|
points = glyph.contours.flat_map(&:points)
|
|
121
|
-
|
|
121
|
+
if points.empty?
|
|
122
|
+
return { x_min: 0, y_min: 0, x_max: @units_per_em,
|
|
123
|
+
y_max: @units_per_em }
|
|
124
|
+
end
|
|
122
125
|
|
|
123
126
|
xs = points.map(&:x)
|
|
124
127
|
ys = points.map(&:y)
|
|
@@ -74,7 +74,8 @@ module Fontisan
|
|
|
74
74
|
private
|
|
75
75
|
|
|
76
76
|
def build_from_doc_path(data, group_transform, doc, codepoint)
|
|
77
|
-
final = normalizer_for(width: doc.viewbox_width,
|
|
77
|
+
final = normalizer_for(width: doc.viewbox_width,
|
|
78
|
+
height: doc.viewbox_height)
|
|
78
79
|
.final_transform(group_transform)
|
|
79
80
|
contours = build_contours(data, final)
|
|
80
81
|
assemble_glyph(glyph_name_for(codepoint), contours, codepoint)
|
|
@@ -89,13 +90,15 @@ module Fontisan
|
|
|
89
90
|
def transform_contour(contour, transform)
|
|
90
91
|
points = contour.points.map do |pt|
|
|
91
92
|
x, y = transform.apply(pt.x, pt.y)
|
|
92
|
-
Fontisan::Ufo::Point.new(x: x.round, y: y.round, type: pt.type,
|
|
93
|
+
Fontisan::Ufo::Point.new(x: x.round, y: y.round, type: pt.type,
|
|
94
|
+
smooth: pt.smooth)
|
|
93
95
|
end
|
|
94
96
|
Fontisan::Ufo::Contour.new(points)
|
|
95
97
|
end
|
|
96
98
|
|
|
97
99
|
def normalizer_for(width:, height:)
|
|
98
|
-
Geometry::Normalizer.new(viewbox_width: width, viewbox_height: height,
|
|
100
|
+
Geometry::Normalizer.new(viewbox_width: width, viewbox_height: height,
|
|
101
|
+
upm: @upm)
|
|
99
102
|
end
|
|
100
103
|
|
|
101
104
|
def assemble_glyph(name, contours, codepoint)
|
|
@@ -22,7 +22,9 @@ module Fontisan
|
|
|
22
22
|
transforms = transform_string.scan(FUNCTION_RE).map do |name, args|
|
|
23
23
|
build_transform(name, args)
|
|
24
24
|
end
|
|
25
|
-
transforms.reduce(AffineTransform.identity)
|
|
25
|
+
transforms.reduce(AffineTransform.identity) do |acc, t|
|
|
26
|
+
acc.compose(t)
|
|
27
|
+
end
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
def self.build_transform(name, args_string)
|
|
@@ -35,7 +37,8 @@ module Fontisan
|
|
|
35
37
|
when "skewX" then AffineTransform.skew_x_radians(degrees_to_radians(args.fetch(0)))
|
|
36
38
|
when "skewY" then AffineTransform.skew_y_radians(degrees_to_radians(args.fetch(0)))
|
|
37
39
|
else
|
|
38
|
-
raise ArgumentError,
|
|
40
|
+
raise ArgumentError,
|
|
41
|
+
"unknown SVG transform function: #{name.inspect}"
|
|
39
42
|
end
|
|
40
43
|
end
|
|
41
44
|
|
|
@@ -59,9 +62,13 @@ module Fontisan
|
|
|
59
62
|
end
|
|
60
63
|
|
|
61
64
|
def self.build_matrix(args)
|
|
62
|
-
|
|
65
|
+
if args.size != 6
|
|
66
|
+
raise ArgumentError,
|
|
67
|
+
"matrix() requires 6 arguments, got #{args.size}"
|
|
68
|
+
end
|
|
63
69
|
|
|
64
|
-
AffineTransform.new(args[0], args[1], args[2], args[3], args[4],
|
|
70
|
+
AffineTransform.new(args[0], args[1], args[2], args[3], args[4],
|
|
71
|
+
args[5])
|
|
65
72
|
end
|
|
66
73
|
|
|
67
74
|
# Rotate around a specific point: translate to origin, rotate,
|
|
@@ -5,8 +5,10 @@ module Fontisan
|
|
|
5
5
|
# Geometry primitives for SvgToGlyf: affine transforms, SVG
|
|
6
6
|
# transform attribute parsing, and coordinate normalization.
|
|
7
7
|
module Geometry
|
|
8
|
-
autoload :AffineTransform,
|
|
9
|
-
|
|
8
|
+
autoload :AffineTransform,
|
|
9
|
+
"fontisan/svg_to_glyf/geometry/affine_transform"
|
|
10
|
+
autoload :TransformParser,
|
|
11
|
+
"fontisan/svg_to_glyf/geometry/transform_parser"
|
|
10
12
|
autoload :Normalizer, "fontisan/svg_to_glyf/geometry/normalizer"
|
|
11
13
|
end
|
|
12
14
|
end
|
|
@@ -40,8 +40,10 @@ module Fontisan
|
|
|
40
40
|
# @param num_regions [Integer] number of variation regions
|
|
41
41
|
# @param width [Integer, nil] advance width
|
|
42
42
|
# @return [String] CFF2 charstring bytes with blend sequences
|
|
43
|
-
def self.build_variable(outline, master_outlines:, num_regions:,
|
|
44
|
-
|
|
43
|
+
def self.build_variable(outline, master_outlines:, num_regions:,
|
|
44
|
+
width: nil)
|
|
45
|
+
new.build_variable_outline(outline, master_outlines, num_regions,
|
|
46
|
+
width)
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
# ---------- variable-font encoding ----------
|
|
@@ -65,7 +67,9 @@ module Fontisan
|
|
|
65
67
|
|
|
66
68
|
# Process an outline with blend operators for each varying coordinate.
|
|
67
69
|
def encode_variable_outline(outline, master_outlines, num_regions)
|
|
68
|
-
@master_states = master_outlines.map
|
|
70
|
+
@master_states = master_outlines.map do
|
|
71
|
+
MasterState.new(current_x: 0, current_y: 0)
|
|
72
|
+
end
|
|
69
73
|
@num_regions = num_regions
|
|
70
74
|
@has_blend = false
|
|
71
75
|
|
|
@@ -90,7 +94,8 @@ module Fontisan
|
|
|
90
94
|
# ---------- variable moveto ----------
|
|
91
95
|
|
|
92
96
|
def encode_variable_moveto(cmd, master_cmds)
|
|
93
|
-
dx, dy, dx_deltas, dy_deltas = compute_variable_deltas(cmd,
|
|
97
|
+
dx, dy, dx_deltas, dy_deltas = compute_variable_deltas(cmd,
|
|
98
|
+
master_cmds)
|
|
94
99
|
|
|
95
100
|
write_variable_number(dx, dx_deltas)
|
|
96
101
|
write_variable_number(dy, dy_deltas)
|
|
@@ -102,7 +107,8 @@ module Fontisan
|
|
|
102
107
|
# ---------- variable lineto ----------
|
|
103
108
|
|
|
104
109
|
def encode_variable_lineto(cmd, master_cmds)
|
|
105
|
-
dx, dy, dx_deltas, dy_deltas = compute_variable_deltas(cmd,
|
|
110
|
+
dx, dy, dx_deltas, dy_deltas = compute_variable_deltas(cmd,
|
|
111
|
+
master_cmds)
|
|
106
112
|
|
|
107
113
|
write_variable_number(dx, dx_deltas)
|
|
108
114
|
write_variable_number(dy, dy_deltas)
|
data/lib/fontisan/tables/cff.rb
CHANGED
|
@@ -47,7 +47,8 @@ module Fontisan
|
|
|
47
47
|
autoload :Charset, "fontisan/tables/cff/charset"
|
|
48
48
|
autoload :CharString, "fontisan/tables/cff/charstring"
|
|
49
49
|
autoload :CharStringBuilder, "fontisan/tables/cff/charstring_builder"
|
|
50
|
-
autoload :Cff2CharStringBuilder,
|
|
50
|
+
autoload :Cff2CharStringBuilder,
|
|
51
|
+
"fontisan/tables/cff/cff2_charstring_builder"
|
|
51
52
|
autoload :CharStringParser, "fontisan/tables/cff/charstring_parser"
|
|
52
53
|
autoload :CharStringRebuilder, "fontisan/tables/cff/charstring_rebuilder"
|
|
53
54
|
autoload :CharstringsIndex, "fontisan/tables/cff/charstrings_index"
|
|
@@ -66,7 +66,8 @@ module Fontisan
|
|
|
66
66
|
case off_size
|
|
67
67
|
when 1 then [value].pack("C")
|
|
68
68
|
when 2 then [value].pack("n")
|
|
69
|
-
when 3 then [(value >> 16) & 0xFF, (value >> 8) & 0xFF,
|
|
69
|
+
when 3 then [(value >> 16) & 0xFF, (value >> 8) & 0xFF,
|
|
70
|
+
value & 0xFF].pack("CCC")
|
|
70
71
|
when 4 then [value].pack("N")
|
|
71
72
|
else raise ArgumentError, "invalid off_size: #{off_size}"
|
|
72
73
|
end
|
data/lib/fontisan/tables.rb
CHANGED
|
@@ -6,7 +6,8 @@ module Fontisan
|
|
|
6
6
|
module Tables
|
|
7
7
|
autoload :CblcBigGlyphMetrics, "fontisan/tables/cblc_big_glyph_metrics"
|
|
8
8
|
autoload :CblcBitmapSize, "fontisan/tables/cblc_bitmap_size"
|
|
9
|
-
autoload :CblcGlyphBitmapLocation,
|
|
9
|
+
autoload :CblcGlyphBitmapLocation,
|
|
10
|
+
"fontisan/tables/cblc_glyph_bitmap_location"
|
|
10
11
|
autoload :CblcIndexSubTable, "fontisan/tables/cblc_index_subtable"
|
|
11
12
|
autoload :CblcIndexSubTableArrayEntry,
|
|
12
13
|
"fontisan/tables/cblc_index_subtable_array_entry"
|
data/lib/fontisan/ufo/cli.rb
CHANGED
|
@@ -74,7 +74,10 @@ module Fontisan
|
|
|
74
74
|
def extract(ufo, glyph_name, output)
|
|
75
75
|
font = Font.open(ufo)
|
|
76
76
|
glyph = font.glyph(glyph_name)
|
|
77
|
-
|
|
77
|
+
unless glyph
|
|
78
|
+
raise ArgumentError,
|
|
79
|
+
"glyph not found: #{glyph_name.inspect}"
|
|
80
|
+
end
|
|
78
81
|
|
|
79
82
|
head = font.info
|
|
80
83
|
renderer = Fontisan::Svg::StandaloneGlyph.new(
|
|
@@ -65,7 +65,8 @@ module Fontisan
|
|
|
65
65
|
# SbitLineMetrics (12 bytes): height, width, hori/vert bearings
|
|
66
66
|
def self.build_sbit_line_metrics(ppem, num_glyphs)
|
|
67
67
|
io = +""
|
|
68
|
-
io << [ppem & 0xFF, 0, ppem & 0xFF, 0, 0, 0, ppem & 0xFF, 0, 0, 0,
|
|
68
|
+
io << [ppem & 0xFF, 0, ppem & 0xFF, 0, 0, 0, ppem & 0xFF, 0, 0, 0,
|
|
69
|
+
0].pack("C11")
|
|
69
70
|
io << [12 + num_glyphs * 8].pack("n")
|
|
70
71
|
io
|
|
71
72
|
end
|
|
@@ -155,7 +155,10 @@ module Fontisan
|
|
|
155
155
|
return +"" if glyphs.size <= 1
|
|
156
156
|
|
|
157
157
|
bytes = +"\x00"
|
|
158
|
-
|
|
158
|
+
# SID 0 placeholder
|
|
159
|
+
(1...glyphs.size).each do |_|
|
|
160
|
+
bytes << [0].pack("n")
|
|
161
|
+
end
|
|
159
162
|
bytes
|
|
160
163
|
end
|
|
161
164
|
|
|
@@ -193,7 +196,8 @@ module Fontisan
|
|
|
193
196
|
when 1 then [value].pack("C")
|
|
194
197
|
when 2 then [value].pack("n")
|
|
195
198
|
when 3
|
|
196
|
-
[(value >> 16) & 0xFF, (value >> 8) & 0xFF,
|
|
199
|
+
[(value >> 16) & 0xFF, (value >> 8) & 0xFF,
|
|
200
|
+
value & 0xFF].pack("CCC")
|
|
197
201
|
when 4 then [value].pack("N")
|
|
198
202
|
end
|
|
199
203
|
end
|
|
@@ -36,7 +36,9 @@ module Fontisan
|
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
subtable_bmp = format4_subtable(mappings.reject
|
|
39
|
+
subtable_bmp = format4_subtable(mappings.reject do |cp, _|
|
|
40
|
+
cp > 0xFFFF
|
|
41
|
+
end)
|
|
40
42
|
subtable_full = format12_subtable(mappings)
|
|
41
43
|
|
|
42
44
|
header_size = 4 + (2 * 8) # version + numTables + 2 records
|
|
@@ -44,8 +46,10 @@ module Fontisan
|
|
|
44
46
|
offset_full = header_size + subtable_bmp.bytesize
|
|
45
47
|
|
|
46
48
|
header = [0, 2].pack("nn")
|
|
47
|
-
header << subtable_record(PLATFORM_WINDOWS, ENCODING_WINDOWS_BMP,
|
|
48
|
-
|
|
49
|
+
header << subtable_record(PLATFORM_WINDOWS, ENCODING_WINDOWS_BMP,
|
|
50
|
+
offset_bmp)
|
|
51
|
+
header << subtable_record(PLATFORM_WINDOWS, ENCODING_WINDOWS_FULL,
|
|
52
|
+
offset_full)
|
|
49
53
|
header + subtable_bmp + subtable_full
|
|
50
54
|
end
|
|
51
55
|
|
|
@@ -82,7 +86,8 @@ module Fontisan
|
|
|
82
86
|
segments = build_segments(mappings) + [0xFFFF..0xFFFF] # sentinel
|
|
83
87
|
seg_count = segments.size
|
|
84
88
|
search_range = largest_pow2_le(seg_count) * 2
|
|
85
|
-
entry_selector = (Math.log([1,
|
|
89
|
+
entry_selector = (Math.log([1,
|
|
90
|
+
search_range / 2].max) / Math.log(2)).to_i
|
|
86
91
|
range_shift = seg_count * 2 - search_range
|
|
87
92
|
|
|
88
93
|
end_codes = segments.map(&:end)
|
|
@@ -98,7 +103,8 @@ module Fontisan
|
|
|
98
103
|
end
|
|
99
104
|
|
|
100
105
|
body = +""
|
|
101
|
-
body << [seg_count * 2, search_range, entry_selector,
|
|
106
|
+
body << [seg_count * 2, search_range, entry_selector,
|
|
107
|
+
range_shift].pack("nnnn")
|
|
102
108
|
body << end_codes.pack("n*")
|
|
103
109
|
body << [0].pack("n") # reservedPad
|
|
104
110
|
body << start_codes.pack("n*")
|
|
@@ -38,7 +38,8 @@ module Fontisan
|
|
|
38
38
|
offset = HEADER_SIZE + (num_palettes * 2) # header + indices
|
|
39
39
|
|
|
40
40
|
io = +""
|
|
41
|
-
io << [VERSION, num_entries, num_palettes, num_records,
|
|
41
|
+
io << [VERSION, num_entries, num_palettes, num_records,
|
|
42
|
+
offset].pack("nnnnN")
|
|
42
43
|
indices.each { |idx| io << [idx].pack("n") }
|
|
43
44
|
records.each { |c| io << color_bytes(c) }
|
|
44
45
|
io
|