fontisan 0.4.6 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/BUG-stitcher-drops-isolated-cps.md +58 -0
- data/BUG-stitcher-drops-plane1-codepoints.md +310 -0
- data/BUG-stitcher-gid-cap-65535.md +110 -0
- data/CHANGELOG.md +119 -0
- data/README.adoc +121 -68
- data/benchmark/compile_benchmark.rb +70 -0
- data/docs/.vitepress/config.ts +0 -7
- data/docs/CFF2_SUPPORT.adoc +184 -0
- data/docs/STITCHER_GUIDE.adoc +151 -0
- data/docs/SVG_TO_GLYF.adoc +118 -0
- data/docs/UFO_COMPILATION.adoc +119 -0
- data/docs/cli/index.md +5 -28
- data/docs/index.md +0 -2
- data/lib/fontisan/cli.rb +29 -8
- data/lib/fontisan/collection/reader/stats.rb +23 -0
- data/lib/fontisan/collection/reader.rb +90 -0
- data/lib/fontisan/collection/writer.rb +5 -6
- data/lib/fontisan/collection.rb +1 -0
- data/lib/fontisan/commands/convert_command.rb +96 -18
- data/lib/fontisan/commands/multi_format_output.rb +59 -0
- data/lib/fontisan/commands/validate_collection_command.rb +121 -0
- data/lib/fontisan/commands.rb +2 -0
- data/lib/fontisan/error.rb +56 -0
- data/lib/fontisan/models.rb +0 -1
- data/lib/fontisan/stitcher/collection_result.rb +18 -0
- data/lib/fontisan/stitcher/deduplicator.rb +47 -0
- data/lib/fontisan/stitcher/glyph_limit.rb +53 -0
- data/lib/fontisan/stitcher/glyph_signature.rb +51 -0
- data/lib/fontisan/stitcher/partition_strategy/base.rb +23 -0
- data/lib/fontisan/stitcher/partition_strategy/blueprint.rb +24 -0
- data/lib/fontisan/stitcher/partition_strategy/by_plane.rb +131 -0
- data/lib/fontisan/stitcher/partition_strategy/partition.rb +24 -0
- data/lib/fontisan/stitcher/partition_strategy.rb +22 -0
- data/lib/fontisan/stitcher.rb +223 -168
- data/lib/fontisan/svg_to_glyf/assembler.rb +132 -0
- data/lib/fontisan/svg_to_glyf/document.rb +83 -0
- data/lib/fontisan/svg_to_glyf/geometry/affine_transform.rb +112 -0
- data/lib/fontisan/svg_to_glyf/geometry/normalizer.rb +45 -0
- data/lib/fontisan/svg_to_glyf/geometry/transform_parser.rb +91 -0
- data/lib/fontisan/svg_to_glyf/geometry.rb +13 -0
- data/lib/fontisan/svg_to_glyf/path/command.rb +18 -0
- data/lib/fontisan/svg_to_glyf/path/contour_builder.rb +140 -0
- data/lib/fontisan/svg_to_glyf/path/parser.rb +98 -0
- data/lib/fontisan/svg_to_glyf/path/state.rb +79 -0
- data/lib/fontisan/svg_to_glyf/path.rb +14 -0
- data/lib/fontisan/svg_to_glyf.rb +62 -0
- data/lib/fontisan/tables/cff/cff2_charstring_builder.rb +216 -0
- data/lib/fontisan/tables/cff.rb +1 -0
- data/lib/fontisan/tables/cff2/dict_encoder.rb +94 -0
- data/lib/fontisan/tables/cff2/fd_select.rb +69 -0
- data/lib/fontisan/tables/cff2/header.rb +34 -0
- data/lib/fontisan/tables/cff2/index_builder.rb +79 -0
- data/lib/fontisan/tables/cff2.rb +4 -0
- data/lib/fontisan/ufo/compile/cbdt_cblc.rb +103 -0
- data/lib/fontisan/ufo/compile/cff2.rb +181 -0
- data/lib/fontisan/ufo/compile/cff2_subroutines.rb +39 -0
- data/lib/fontisan/ufo/compile/colr.rb +80 -0
- data/lib/fontisan/ufo/compile/cpal.rb +61 -0
- data/lib/fontisan/ufo/compile/math.rb +143 -0
- data/lib/fontisan/ufo/compile/meta.rb +51 -0
- data/lib/fontisan/ufo/compile/name.rb +2 -2
- data/lib/fontisan/ufo/compile/otf2_compiler.rb +46 -0
- data/lib/fontisan/ufo/compile/sbix.rb +99 -0
- data/lib/fontisan/ufo/compile/svg_table.rb +60 -0
- data/lib/fontisan/ufo/compile/variable_otf.rb +75 -0
- data/lib/fontisan/ufo/compile.rb +11 -0
- data/lib/fontisan/ufo/info.rb +48 -0
- data/lib/fontisan/unicode/plane.rb +56 -0
- data/lib/fontisan/unicode.rb +17 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +5 -2
- metadata +53 -19
- data/docs/cli/audit.md +0 -337
- data/lib/fontisan/cldr/aggregator.rb +0 -33
- data/lib/fontisan/cldr/cache_manager.rb +0 -110
- data/lib/fontisan/cldr/config.rb +0 -59
- data/lib/fontisan/cldr/download_error.rb +0 -9
- data/lib/fontisan/cldr/downloader.rb +0 -79
- data/lib/fontisan/cldr/error.rb +0 -8
- data/lib/fontisan/cldr/index.rb +0 -64
- data/lib/fontisan/cldr/index_builder.rb +0 -72
- data/lib/fontisan/cldr/unicode_set_parser.rb +0 -189
- data/lib/fontisan/cldr/unknown_version_error.rb +0 -9
- data/lib/fontisan/cldr/version_resolver.rb +0 -91
- data/lib/fontisan/cldr.rb +0 -23
- data/lib/fontisan/cli/cldr_cli.rb +0 -85
- data/lib/fontisan/config/cldr.yml +0 -22
- data/lib/fontisan/models/cldr/language_coverage.rb +0 -31
- data/lib/fontisan/models/cldr.rb +0 -12
data/lib/fontisan/stitcher.rb
CHANGED
|
@@ -1,102 +1,123 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Fontisan
|
|
4
|
-
# Multi-source font stitcher
|
|
5
|
-
# source fonts (UFO or loaded TTF/OTF) into a single new font.
|
|
4
|
+
# Multi-source font stitcher with explicit subfont declaration.
|
|
6
5
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
6
|
+
# Every set of codepoints is explicitly assigned to a named subfont
|
|
7
|
+
# via the required `into:` keyword. The user controls the collection
|
|
8
|
+
# structure upfront — there are no defaults and no after-the-fact
|
|
9
|
+
# splitting.
|
|
11
10
|
#
|
|
12
|
-
#
|
|
11
|
+
# Single-font output: `write_to` requires a `subfont:` name.
|
|
12
|
+
# Collection output: `write_collection` writes all declared subfonts.
|
|
13
|
+
#
|
|
14
|
+
# @example Single font
|
|
13
15
|
# stitcher = Fontisan::Stitcher.new
|
|
14
16
|
# stitcher.add_source(:latin, Fontisan::Ufo::Font.open("latin.ufo"))
|
|
15
|
-
# stitcher.
|
|
16
|
-
# stitcher.
|
|
17
|
-
#
|
|
18
|
-
#
|
|
17
|
+
# stitcher.include_range(0x41..0x5A, from: :latin, into: :main)
|
|
18
|
+
# stitcher.write_to("out.ttf", format: :ttf, subfont: :main)
|
|
19
|
+
#
|
|
20
|
+
# @example Collection
|
|
21
|
+
# stitcher = Fontisan::Stitcher.new
|
|
22
|
+
# stitcher.add_source(:noto_sans, noto_sans)
|
|
23
|
+
# stitcher.add_source(:noto_cjk, noto_cjk)
|
|
24
|
+
# stitcher.include_range(0x41..0x5A, from: :noto_sans, into: :latin)
|
|
25
|
+
# stitcher.include_range(0x4E00..0x9FFF, from: :noto_cjk, into: :cjk)
|
|
26
|
+
# stitcher.write_collection("out.otc", format: :otf2)
|
|
19
27
|
class Stitcher
|
|
20
|
-
autoload :Source,
|
|
21
|
-
autoload :Selector,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
autoload :Source, "fontisan/stitcher/source"
|
|
29
|
+
autoload :Selector, "fontisan/stitcher/selector"
|
|
30
|
+
autoload :GlyphSignature, "fontisan/stitcher/glyph_signature"
|
|
31
|
+
autoload :Deduplicator, "fontisan/stitcher/deduplicator"
|
|
32
|
+
autoload :GlyphLimit, "fontisan/stitcher/glyph_limit"
|
|
33
|
+
autoload :CollectionResult, "fontisan/stitcher/collection_result"
|
|
34
|
+
autoload :SubfontStats, "fontisan/stitcher/collection_result"
|
|
35
|
+
autoload :PartitionStrategy, "fontisan/stitcher/partition_strategy"
|
|
36
|
+
|
|
37
|
+
# Internal: pairs a compiled loaded font with its stats so
|
|
38
|
+
# +write_collection+ can build the collection and the result from a
|
|
39
|
+
# single compilation pass.
|
|
40
|
+
CompiledSubfont = Struct.new(:name, :font, :stats, keyword_init: true)
|
|
41
|
+
private_constant :CompiledSubfont
|
|
42
|
+
|
|
43
|
+
DEFAULT_DEDUPLICATE = true
|
|
44
|
+
|
|
45
|
+
attr_reader :sources, :subfonts, :info
|
|
46
|
+
|
|
47
|
+
def initialize(deduplicate: DEFAULT_DEDUPLICATE)
|
|
26
48
|
@sources = {}
|
|
27
|
-
@
|
|
28
|
-
@
|
|
49
|
+
@subfonts = Hash.new { |h, k| h[k] = [] }
|
|
50
|
+
@info = nil
|
|
51
|
+
@deduplicate = deduplicate
|
|
29
52
|
end
|
|
30
53
|
|
|
31
|
-
# Register a named source font.
|
|
32
|
-
# @param label [Symbol, String] name to reference this source by
|
|
33
|
-
# @param font [Fontisan::Ufo::Font, Fontisan::SfntFont] the source
|
|
34
54
|
def add_source(label, font)
|
|
35
55
|
@sources[label.to_sym] = Source.new(font)
|
|
36
56
|
end
|
|
37
57
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# @param from [Symbol, String] source label
|
|
41
|
-
def include_range(range, from:)
|
|
42
|
-
Selector::Range.new(range).apply(source(from), @bindings)
|
|
58
|
+
def include_range(range, from:, into:)
|
|
59
|
+
Selector::Range.new(range).apply(source(from), @subfonts[into])
|
|
43
60
|
end
|
|
44
61
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
# @param from [Symbol, String] source label
|
|
48
|
-
def include_codepoints(codepoints, from:)
|
|
49
|
-
Selector::Codepoints.new(codepoints).apply(source(from), @bindings)
|
|
62
|
+
def include_codepoints(codepoints, from:, into:)
|
|
63
|
+
Selector::Codepoints.new(codepoints).apply(source(from), @subfonts[into])
|
|
50
64
|
end
|
|
51
65
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
Selector::Gid.new(donor_gid).apply(source(from), @bindings)
|
|
66
|
+
def include_codepoints_map(cp_map, into:)
|
|
67
|
+
cp_map.to_h
|
|
68
|
+
.group_by { |_cp, label| label }
|
|
69
|
+
.transform_values { |pairs| pairs.map(&:first).sort }
|
|
70
|
+
.each { |label, cps| include_codepoints(cps, from: label, into: into) }
|
|
58
71
|
end
|
|
59
72
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
def include_gid(donor_gid, from:, into:)
|
|
74
|
+
Selector::Gid.new(donor_gid).apply(source(from), @subfonts[into])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def include_notdef(from:, into:)
|
|
78
|
+
include_gid(0, from: from, into: into)
|
|
64
79
|
end
|
|
65
80
|
|
|
66
|
-
# Set font-wide metadata on the stitched font.
|
|
67
|
-
# @param info_hash [Hash] any subset of Fontisan::Ufo::Info fields
|
|
68
81
|
def set_info(info_hash)
|
|
69
|
-
@
|
|
82
|
+
@info = Ufo::Info.new(info_hash)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def subfont_names
|
|
86
|
+
@subfonts.keys
|
|
70
87
|
end
|
|
71
88
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
# @param path [String] output file path
|
|
81
|
-
# @param format [Symbol] :ttf or :otf
|
|
82
|
-
def write_to(path, format: :ttf)
|
|
83
|
-
build_target_font
|
|
89
|
+
def build_target_font(subfont:)
|
|
90
|
+
build_target_for(subfont)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def write_to(path, format:, subfont:)
|
|
94
|
+
target = build_target_for(subfont)
|
|
95
|
+
GlyphLimit.check!(target.glyphs.size, format: format)
|
|
96
|
+
|
|
84
97
|
compiler = compiler_for(format)
|
|
85
|
-
|
|
86
|
-
compiler_instance.compile(output_path: path)
|
|
98
|
+
compiler.new(target).compile(output_path: path)
|
|
87
99
|
|
|
88
100
|
propagate_cbdt_tables(path) if cbdt_source
|
|
89
|
-
|
|
90
101
|
path
|
|
91
102
|
end
|
|
92
103
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
def write_collection(path, format:)
|
|
105
|
+
raise ArgumentError, "no subfonts declared" if @subfonts.empty?
|
|
106
|
+
|
|
107
|
+
compiled = @subfonts.keys.map do |name|
|
|
108
|
+
compile_subfont_with_stats(name, format: format)
|
|
109
|
+
end
|
|
110
|
+
fonts = compiled.map(&:font)
|
|
111
|
+
|
|
112
|
+
collection_format = collection_format_for(format)
|
|
113
|
+
Collection::Builder.new(fonts, format: collection_format,
|
|
114
|
+
optimize: true).build_to_file(path)
|
|
115
|
+
|
|
116
|
+
CollectionResult.new(
|
|
117
|
+
path: path,
|
|
118
|
+
bytes: File.size(path),
|
|
119
|
+
subfonts: compiled.map(&:stats),
|
|
120
|
+
)
|
|
100
121
|
end
|
|
101
122
|
|
|
102
123
|
private
|
|
@@ -107,9 +128,6 @@ module Fontisan
|
|
|
107
128
|
end
|
|
108
129
|
end
|
|
109
130
|
|
|
110
|
-
# Find the single CBDT source among registered sources, if any.
|
|
111
|
-
# Raises if more than one CBDT source is present (merge not supported).
|
|
112
|
-
# @return [Source, nil]
|
|
113
131
|
def cbdt_source
|
|
114
132
|
cbdts = @sources.values.select { |s| s.bitmap_mode == :cbdt }
|
|
115
133
|
if cbdts.size > 1
|
|
@@ -120,87 +138,105 @@ module Fontisan
|
|
|
120
138
|
cbdts.first
|
|
121
139
|
end
|
|
122
140
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
compiled = Fontisan::FontLoader.load(path)
|
|
132
|
-
|
|
133
|
-
tables = {}
|
|
134
|
-
compiled.table_names.each do |tag|
|
|
135
|
-
raw = extract_raw_table(compiled, tag)
|
|
136
|
-
tables[tag] = raw if raw
|
|
141
|
+
def compiler_for(format)
|
|
142
|
+
case format.to_sym
|
|
143
|
+
when :ttf then Ufo::Compile::TtfCompiler
|
|
144
|
+
when :otf then Ufo::Compile::OtfCompiler
|
|
145
|
+
when :otf2 then Ufo::Compile::Otf2Compiler
|
|
146
|
+
else
|
|
147
|
+
raise ArgumentError, "unknown format: #{format.inspect}"
|
|
137
148
|
end
|
|
149
|
+
end
|
|
138
150
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
tables["CBDT"] = cbdt_bytes if cbdt_bytes
|
|
142
|
-
tables["CBLC"] = cblc_bytes if cblc_bytes
|
|
143
|
-
|
|
144
|
-
sfnt = tables.key?("CFF ") ? 0x4F54544F : 0x00010000
|
|
145
|
-
Fontisan::FontWriter.write_to_file(tables, path, sfnt_version: sfnt)
|
|
151
|
+
def collection_format_for(subfont_format)
|
|
152
|
+
subfont_format == :ttf ? :ttc : :otc
|
|
146
153
|
end
|
|
147
154
|
|
|
148
|
-
def
|
|
149
|
-
|
|
150
|
-
|
|
155
|
+
def build_target_for(subfont_name)
|
|
156
|
+
bindings = @subfonts[subfont_name] || []
|
|
157
|
+
target = Ufo::Font.new
|
|
158
|
+
target.info = @info ? @info.dup : Ufo::Info.new
|
|
159
|
+
dedup = @deduplicate ? Deduplicator.new : nil
|
|
160
|
+
assign_gids_and_copy_glyphs(bindings, target, dedup)
|
|
161
|
+
target
|
|
162
|
+
end
|
|
151
163
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
nil
|
|
164
|
+
def compile_subfont_to_loaded_font(subfont_name, format:)
|
|
165
|
+
compile_subfont_with_stats(subfont_name, format: format).font
|
|
155
166
|
end
|
|
156
167
|
|
|
157
|
-
def
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
168
|
+
def compile_subfont_with_stats(subfont_name, format:)
|
|
169
|
+
target = build_target_for(subfont_name)
|
|
170
|
+
GlyphLimit.check!(target.glyphs.size, format: format)
|
|
171
|
+
|
|
172
|
+
ext = format == :ttf ? ".ttf" : ".otf"
|
|
173
|
+
Dir.mktmpdir do |dir|
|
|
174
|
+
sub_path = File.join(dir, "sub#{subfont_name}#{ext}")
|
|
175
|
+
compiler = compiler_for(format)
|
|
176
|
+
compiler.new(target).compile(output_path: sub_path)
|
|
177
|
+
propagate_cbdt_tables(sub_path) if cbdt_source
|
|
178
|
+
|
|
179
|
+
loaded = Fontisan::FontLoader.load(sub_path)
|
|
180
|
+
stats = SubfontStats.new(
|
|
181
|
+
name: subfont_name,
|
|
182
|
+
glyph_count: loaded.table("maxp")&.num_glyphs || 0,
|
|
183
|
+
codepoint_count: (loaded.table("cmap")&.unicode_mappings || {}).size,
|
|
184
|
+
)
|
|
185
|
+
CompiledSubfont.new(name: subfont_name, font: loaded, stats: stats)
|
|
163
186
|
end
|
|
164
187
|
end
|
|
165
188
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
#
|
|
169
|
-
# When a CBDT source is present, its glyphs are added FIRST (in
|
|
170
|
-
# source GID order) so that the CBLC's GID references remain valid.
|
|
171
|
-
# Glyf-source bindings are processed AFTER, appending new glyphs.
|
|
172
|
-
def assign_gids_and_copy_glyphs
|
|
173
|
-
cbdt = cbdt_source
|
|
189
|
+
def assign_gids_and_copy_glyphs(bindings, target, deduplicator)
|
|
190
|
+
cbdt = safe_cbdt_source
|
|
174
191
|
|
|
175
192
|
if cbdt
|
|
176
|
-
add_all_cbdt_glyphs(cbdt)
|
|
193
|
+
add_all_cbdt_glyphs(cbdt, target)
|
|
177
194
|
else
|
|
178
|
-
|
|
195
|
+
add_notdef_from(bindings, target, deduplicator)
|
|
179
196
|
end
|
|
180
197
|
|
|
181
|
-
sorted_bindings.each do |binding|
|
|
198
|
+
sorted_bindings(bindings).each do |binding|
|
|
182
199
|
next if binding[:donor_gid].zero?
|
|
183
|
-
|
|
184
|
-
# Skip bindings from the CBDT source — its glyphs are already added.
|
|
185
200
|
next if cbdt && binding[:source].equal?(cbdt)
|
|
186
201
|
|
|
187
202
|
glyph = binding[:source].glyph_for_gid(binding[:donor_gid])
|
|
188
203
|
next unless glyph
|
|
189
204
|
|
|
190
|
-
|
|
191
|
-
|
|
205
|
+
canonical = deduplicator&.find(glyph)
|
|
206
|
+
if canonical && target.glyphs.key?(canonical)
|
|
207
|
+
add_extra_unicode(target, canonical, binding[:codepoint])
|
|
192
208
|
else
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
209
|
+
name = unique_target_name(target, glyph.name)
|
|
210
|
+
copy_glyph_into(target, name: name, source: binding[:source],
|
|
211
|
+
donor_gid: binding[:donor_gid],
|
|
212
|
+
codepoint: binding[:codepoint])
|
|
213
|
+
deduplicator&.register(glyph, name)
|
|
197
214
|
end
|
|
198
215
|
end
|
|
199
216
|
end
|
|
200
217
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
218
|
+
def safe_cbdt_source
|
|
219
|
+
cbdts = @sources.values.select { |s| s.bitmap_mode == :cbdt }
|
|
220
|
+
cbdts.size == 1 ? cbdts.first : nil
|
|
221
|
+
rescue MultipleCbdtSourcesError
|
|
222
|
+
nil
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def sorted_bindings(bindings)
|
|
226
|
+
bindings.sort_by { |b| [b[:codepoint] || Float::INFINITY, b[:donor_gid]] }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def add_notdef_from(bindings, target, deduplicator)
|
|
230
|
+
notdef_binding = bindings.find { |b| b[:donor_gid].zero? }
|
|
231
|
+
if notdef_binding
|
|
232
|
+
copy_glyph_into(target, name: ".notdef",
|
|
233
|
+
source: notdef_binding[:source],
|
|
234
|
+
donor_gid: 0)
|
|
235
|
+
else
|
|
236
|
+
target.layers.default_layer.add(Ufo::Glyph.new(name: ".notdef"))
|
|
237
|
+
end
|
|
238
|
+
dedup_target = target.glyphs[".notdef"]
|
|
239
|
+
deduplicator&.register(dedup_target, ".notdef") if dedup_target
|
|
204
240
|
end
|
|
205
241
|
|
|
206
242
|
def copy_glyph_into(target_font, name:, source:, donor_gid:, codepoint: nil)
|
|
@@ -212,46 +248,85 @@ module Fontisan
|
|
|
212
248
|
target_font.layers.default_layer.add(copy)
|
|
213
249
|
end
|
|
214
250
|
|
|
215
|
-
def add_extra_unicode(glyph_name, codepoint)
|
|
251
|
+
def add_extra_unicode(target_font, glyph_name, codepoint)
|
|
216
252
|
return unless codepoint
|
|
217
253
|
|
|
218
|
-
glyph =
|
|
254
|
+
glyph = target_font.glyph(glyph_name)
|
|
219
255
|
glyph.add_unicode(codepoint) unless glyph.unicodes.include?(codepoint)
|
|
220
256
|
end
|
|
221
257
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
@target.layers.default_layer.add(Ufo::Glyph.new(name: ".notdef"))
|
|
258
|
+
def unique_target_name(target_font, base_name)
|
|
259
|
+
return base_name unless target_font.glyphs.key?(base_name)
|
|
260
|
+
|
|
261
|
+
suffix = 1
|
|
262
|
+
loop do
|
|
263
|
+
candidate = "#{base_name}.#{suffix}"
|
|
264
|
+
return candidate unless target_font.glyphs.key?(candidate)
|
|
265
|
+
|
|
266
|
+
suffix += 1
|
|
232
267
|
end
|
|
233
268
|
end
|
|
234
269
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
270
|
+
def clone_glyph(original, name:)
|
|
271
|
+
copy = Ufo::Glyph.new(name: name)
|
|
272
|
+
copy.width = original.width
|
|
273
|
+
copy.height = original.height
|
|
274
|
+
original.contours.each { |c| copy.add_contour(clone_contour(c)) }
|
|
275
|
+
original.components.each { |c| copy.add_component(c) }
|
|
276
|
+
original.anchors.each { |a| copy.add_anchor(a) }
|
|
277
|
+
original.guidelines.each { |g| copy.add_guideline(g) }
|
|
278
|
+
copy
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def clone_contour(original)
|
|
282
|
+
points = original.points.map do |p|
|
|
283
|
+
Ufo::Point.new(x: p.x, y: p.y, type: p.type, smooth: p.smooth)
|
|
284
|
+
end
|
|
285
|
+
Ufo::Contour.new(points)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def propagate_cbdt_tables(path)
|
|
289
|
+
source = cbdt_source
|
|
290
|
+
return unless source
|
|
291
|
+
|
|
292
|
+
compiled = Fontisan::FontLoader.load(path)
|
|
293
|
+
|
|
294
|
+
tables = {}
|
|
295
|
+
compiled.table_names.each do |tag|
|
|
296
|
+
raw = extract_raw_table(compiled, tag)
|
|
297
|
+
tables[tag] = raw if raw
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
cbdt_bytes = source.raw_table_bytes("CBDT")
|
|
301
|
+
cblc_bytes = source.raw_table_bytes("CBLC")
|
|
302
|
+
tables["CBDT"] = cbdt_bytes if cbdt_bytes
|
|
303
|
+
tables["CBLC"] = cblc_bytes if cblc_bytes
|
|
304
|
+
|
|
305
|
+
sfnt = tables.key?("CFF ") || tables.key?("CFF2") ? 0x4F54544F : 0x00010000
|
|
306
|
+
Fontisan::FontWriter.write_to_file(tables, path, sfnt_version: sfnt)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def extract_raw_table(font, tag)
|
|
310
|
+
sfnt_table = font.table(tag)
|
|
311
|
+
return nil unless sfnt_table
|
|
312
|
+
|
|
313
|
+
sfnt_table.raw_data
|
|
314
|
+
rescue StandardError
|
|
315
|
+
nil
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def add_all_cbdt_glyphs(source, target)
|
|
240
319
|
ufo = source.font.is_a?(Ufo::Font) ? source.font : nil
|
|
241
320
|
if ufo
|
|
242
|
-
ufo.glyphs.each_value { |g|
|
|
321
|
+
ufo.glyphs.each_value { |g| target.layers.default_layer.add(clone_glyph(g, name: g.name)) }
|
|
243
322
|
return
|
|
244
323
|
end
|
|
245
324
|
|
|
246
|
-
# For loaded TTF/OTF sources, iterate via cmap to get glyph names.
|
|
247
|
-
# CBDT fonts (like NotoColorEmoji) may have thousands of glyphs;
|
|
248
|
-
# we add them all as placeholders.
|
|
249
325
|
maxp = source.font.table("maxp")
|
|
250
326
|
num_glyphs = maxp&.num_glyphs || 0
|
|
251
327
|
cmap = source.font.table("cmap")
|
|
252
328
|
mappings = cmap&.unicode_mappings || {}
|
|
253
329
|
|
|
254
|
-
# Build gid → [codepoints] from cmap
|
|
255
330
|
gid_cps = Hash.new { |h, k| h[k] = [] }
|
|
256
331
|
mappings.each { |cp, gid| gid_cps[gid] << cp }
|
|
257
332
|
|
|
@@ -260,28 +335,8 @@ module Fontisan
|
|
|
260
335
|
glyph = Ufo::Glyph.new(name: name)
|
|
261
336
|
glyph.width = 0
|
|
262
337
|
gid_cps[gid].each { |cp| glyph.add_unicode(cp) }
|
|
263
|
-
|
|
338
|
+
target.layers.default_layer.add(glyph)
|
|
264
339
|
end
|
|
265
340
|
end
|
|
266
|
-
|
|
267
|
-
# Deep-copy a glyph with a new name. Used so multiple target
|
|
268
|
-
# glyphs can share the same source outline without aliasing.
|
|
269
|
-
def clone_glyph(original, name:)
|
|
270
|
-
copy = Ufo::Glyph.new(name: name)
|
|
271
|
-
copy.width = original.width
|
|
272
|
-
copy.height = original.height
|
|
273
|
-
original.contours.each { |c| copy.add_contour(clone_contour(c)) }
|
|
274
|
-
original.components.each { |c| copy.add_component(c) }
|
|
275
|
-
original.anchors.each { |a| copy.add_anchor(a) }
|
|
276
|
-
original.guidelines.each { |g| copy.add_guideline(g) }
|
|
277
|
-
copy
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
def clone_contour(original)
|
|
281
|
-
points = original.points.map do |p|
|
|
282
|
-
Ufo::Point.new(x: p.x, y: p.y, type: p.type, smooth: p.smooth)
|
|
283
|
-
end
|
|
284
|
-
Ufo::Contour.new(points)
|
|
285
|
-
end
|
|
286
341
|
end
|
|
287
342
|
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module SvgToGlyf
|
|
5
|
+
# Orchestrates the full SVG → Ufo::Glyph pipeline:
|
|
6
|
+
#
|
|
7
|
+
# path data + transforms
|
|
8
|
+
# → Path::Parser.parse → [Command]
|
|
9
|
+
# → Path::ContourBuilder.build → [Ufo::Contour]
|
|
10
|
+
# → apply final transform (normalization · group transform)
|
|
11
|
+
# → round to Integer
|
|
12
|
+
# → Ufo::Glyph
|
|
13
|
+
#
|
|
14
|
+
# For SVG files and directories, the Document class extracts the
|
|
15
|
+
# viewBox, accumulated transforms, and path data; the Assembler
|
|
16
|
+
# composes the normalizer with the group transform and runs the
|
|
17
|
+
# pipeline once per path.
|
|
18
|
+
class Assembler
|
|
19
|
+
attr_reader :upm
|
|
20
|
+
|
|
21
|
+
# @param upm [Integer] font units-per-em
|
|
22
|
+
def initialize(upm: SvgToGlyf::DEFAULT_UPM)
|
|
23
|
+
@upm = upm.to_i
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Build a glyph directly from a path data string.
|
|
27
|
+
#
|
|
28
|
+
# @param path_data [String] SVG path d= attribute
|
|
29
|
+
# @param codepoint [Integer, nil] Unicode codepoint
|
|
30
|
+
# @param name [String, nil] glyph name
|
|
31
|
+
# @param viewbox [Hash{Symbol=>Float}, nil] :width, :height
|
|
32
|
+
# @param transform [Geometry::AffineTransform, nil] group transform
|
|
33
|
+
# @return [Fontisan::Ufo::Glyph]
|
|
34
|
+
def build_from_path_data(path_data, codepoint: nil, name: nil,
|
|
35
|
+
viewbox: nil, transform: nil)
|
|
36
|
+
viewbox ||= { width: @upm, height: @upm }
|
|
37
|
+
final = normalizer_for(**viewbox).final_transform(transform || Geometry::AffineTransform.identity)
|
|
38
|
+
contours = build_contours(path_data, final)
|
|
39
|
+
assemble_glyph(name || glyph_name_for(codepoint), contours, codepoint)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Build a glyph from an SVG file.
|
|
43
|
+
#
|
|
44
|
+
# @param file_path [String]
|
|
45
|
+
# @param codepoint [Integer, nil] override; otherwise derived from filename
|
|
46
|
+
# @return [Fontisan::Ufo::Glyph]
|
|
47
|
+
def build_from_file(file_path, codepoint: nil)
|
|
48
|
+
doc = Document.from_file(file_path)
|
|
49
|
+
codepoint ||= codepoint_from_filename(File.basename(file_path))
|
|
50
|
+
|
|
51
|
+
doc.each_path.with_object(nil) do |(data, transform), _|
|
|
52
|
+
return build_from_doc_path(data, transform, doc, codepoint)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
empty_glyph(codepoint)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Build a font from a directory of SVG files.
|
|
59
|
+
#
|
|
60
|
+
# @param dir [String]
|
|
61
|
+
# @return [Fontisan::Ufo::Font]
|
|
62
|
+
def build_from_directory(dir)
|
|
63
|
+
font = Fontisan::Ufo::Font.new
|
|
64
|
+
font.info.units_per_em = @upm
|
|
65
|
+
|
|
66
|
+
Dir.glob(File.join(dir, "*.svg")).each do |path|
|
|
67
|
+
glyph = build_from_file(path)
|
|
68
|
+
font.glyphs[glyph.name] = glyph
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
font
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def build_from_doc_path(data, group_transform, doc, codepoint)
|
|
77
|
+
final = normalizer_for(width: doc.viewbox_width, height: doc.viewbox_height)
|
|
78
|
+
.final_transform(group_transform)
|
|
79
|
+
contours = build_contours(data, final)
|
|
80
|
+
assemble_glyph(glyph_name_for(codepoint), contours, codepoint)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def build_contours(path_data, final_transform)
|
|
84
|
+
commands = Path::Parser.parse(path_data)
|
|
85
|
+
contours = Path::ContourBuilder.new.build(commands)
|
|
86
|
+
contours.map { |c| transform_contour(c, final_transform) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def transform_contour(contour, transform)
|
|
90
|
+
points = contour.points.map do |pt|
|
|
91
|
+
x, y = transform.apply(pt.x, pt.y)
|
|
92
|
+
Fontisan::Ufo::Point.new(x: x.round, y: y.round, type: pt.type, smooth: pt.smooth)
|
|
93
|
+
end
|
|
94
|
+
Fontisan::Ufo::Contour.new(points)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def normalizer_for(width:, height:)
|
|
98
|
+
Geometry::Normalizer.new(viewbox_width: width, viewbox_height: height, upm: @upm)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def assemble_glyph(name, contours, codepoint)
|
|
102
|
+
glyph = Fontisan::Ufo::Glyph.new(name: name)
|
|
103
|
+
glyph.width = @upm
|
|
104
|
+
contours.each { |c| glyph.add_contour(c) }
|
|
105
|
+
glyph.add_unicode(codepoint) if codepoint
|
|
106
|
+
glyph
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def empty_glyph(codepoint)
|
|
110
|
+
glyph = Fontisan::Ufo::Glyph.new(name: glyph_name_for(codepoint))
|
|
111
|
+
glyph.width = @upm
|
|
112
|
+
glyph.add_unicode(codepoint) if codepoint
|
|
113
|
+
glyph
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# UFO convention: uniXXXX for BMP, uXXXXX for supplementary planes.
|
|
117
|
+
def glyph_name_for(codepoint)
|
|
118
|
+
return "glyph" unless codepoint
|
|
119
|
+
|
|
120
|
+
codepoint < 0x10000 ? "uni%04X" % codepoint : "u%05X" % codepoint
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Derive a codepoint from a filename like "U+10940.svg" or "10940.svg".
|
|
124
|
+
def codepoint_from_filename(basename)
|
|
125
|
+
match = basename.match(/(?:U\+)?([0-9A-Fa-f]{4,6})\.svg\z/)
|
|
126
|
+
return nil unless match
|
|
127
|
+
|
|
128
|
+
match[1].to_i(16)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|