fontisan 0.4.43 → 0.4.44
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/TODO.bug-fixes/12-type1-generators-duck-typing.md +80 -0
- data/TODO.bug-fixes/13-hint-extractor-duck-typing.md +30 -0
- data/TODO.bug-fixes/14-font-interface-type-checks.md +82 -0
- data/TODO.bug-fixes/15-glyph-type-checks-and-simple-glyph-points-bug.md +49 -0
- data/TODO.bug-fixes/16-variation-table-checks.md +31 -0
- data/TODO.bug-fixes/17-ufo-and-stitcher-cleanup.md +32 -0
- data/TODO.bug-fixes/18-generic-value-and-io-checks.md +56 -0
- data/TODO.bug-fixes/19-serialization-migration.md +52 -0
- data/TODO.bug-fixes/20-variation-validator-type-checks.md +29 -0
- data/TODO.bug-fixes/21-xml-builder-send.md +24 -0
- data/TODO.bug-fixes/22-type1-converter-font-info.md +25 -0
- data/TODO.bug-fixes/23-boundary-type-checks.md +31 -0
- data/TODO.bug-fixes/24-validator-dynamic-field-key.md +25 -0
- data/TODO.bug-fixes/25-serialization-migration.md +49 -0
- data/TODO.bug-fixes/26-variation-preserver-spec-migration.md +25 -0
- data/TODO.bug-fixes/27-table-class-registry.md +50 -0
- data/TODO.bug-fixes/28-spec-doubles-cleanup.md +40 -0
- data/TODO.bug-fixes/29-strategy-registries.md +30 -0
- data/TODO.bug-fixes/README.md +24 -0
- data/lib/fontisan/binary/base_record.rb +13 -12
- data/lib/fontisan/collection/builder.rb +3 -5
- data/lib/fontisan/collection/dfont_builder.rb +4 -11
- data/lib/fontisan/collection/writer.rb +1 -1
- data/lib/fontisan/commands/pack_command.rb +4 -4
- data/lib/fontisan/commands/unpack_command.rb +1 -1
- data/lib/fontisan/commands/validate_command.rb +1 -1
- data/lib/fontisan/converters/format_converter.rb +2 -2
- data/lib/fontisan/converters/glyf_table_builder.rb +1 -1
- data/lib/fontisan/converters/outline_converter.rb +2 -6
- data/lib/fontisan/converters/table_copier.rb +4 -12
- data/lib/fontisan/converters/type1_converter.rb +12 -37
- data/lib/fontisan/converters/woff2_encoder.rb +8 -10
- data/lib/fontisan/converters/woff_writer.rb +3 -3
- data/lib/fontisan/export/exporter.rb +8 -9
- data/lib/fontisan/export/table_serializer.rb +71 -186
- data/lib/fontisan/export/transformers/font_to_ttx.rb +18 -18
- data/lib/fontisan/export/transformers/head_transformer.rb +1 -1
- data/lib/fontisan/export/transformers/hhea_transformer.rb +1 -1
- data/lib/fontisan/export/transformers/maxp_transformer.rb +1 -1
- data/lib/fontisan/export/transformers/name_transformer.rb +1 -1
- data/lib/fontisan/export/transformers/os2_transformer.rb +1 -1
- data/lib/fontisan/export/transformers/post_transformer.rb +1 -1
- data/lib/fontisan/export/ttx_generator.rb +36 -37
- data/lib/fontisan/font_writer.rb +1 -1
- data/lib/fontisan/glyph_accessor.rb +7 -9
- data/lib/fontisan/hints/postscript_hint_extractor.rb +24 -64
- data/lib/fontisan/hints/truetype_hint_extractor.rb +1 -1
- data/lib/fontisan/models/ttx/tables/binary_table.rb +1 -1
- data/lib/fontisan/optimizers/charstring_rewriter.rb +1 -1
- data/lib/fontisan/sfnt_font.rb +2 -0
- data/lib/fontisan/sfnt_source.rb +28 -0
- data/lib/fontisan/stitcher/source.rb +8 -10
- data/lib/fontisan/subset/builder.rb +2 -2
- data/lib/fontisan/subset/table_strategy/cff2.rb +2 -3
- data/lib/fontisan/svg_to_glyf/assembler.rb +59 -20
- data/lib/fontisan/svg_to_glyf/document.rb +28 -9
- data/lib/fontisan/svg_to_glyf/geometry/normalizer.rb +34 -17
- data/lib/fontisan/tables/cff/charstring.rb +3 -0
- data/lib/fontisan/tables/cff/dict.rb +6 -3
- data/lib/fontisan/tables/cff/index.rb +1 -1
- data/lib/fontisan/tables/cff/private_dict_writer.rb +2 -2
- data/lib/fontisan/tables/cff/table_builder.rb +1 -1
- data/lib/fontisan/tables/cff.rb +3 -0
- data/lib/fontisan/tables/cff2/table_builder.rb +2 -4
- data/lib/fontisan/tables/cff2.rb +3 -0
- data/lib/fontisan/tables/cmap.rb +3 -0
- data/lib/fontisan/tables/cvar.rb +3 -0
- data/lib/fontisan/tables/fvar.rb +3 -0
- data/lib/fontisan/tables/glyf/simple_glyph.rb +9 -0
- data/lib/fontisan/tables/glyf.rb +12 -8
- data/lib/fontisan/tables/glyf_table.rb +1 -1
- data/lib/fontisan/tables/gvar.rb +3 -0
- data/lib/fontisan/tables/head.rb +3 -0
- data/lib/fontisan/tables/hhea.rb +3 -0
- data/lib/fontisan/tables/hmtx.rb +3 -7
- data/lib/fontisan/tables/hvar.rb +4 -0
- data/lib/fontisan/tables/loca.rb +3 -0
- data/lib/fontisan/tables/maxp.rb +3 -0
- data/lib/fontisan/tables/mvar.rb +3 -0
- data/lib/fontisan/tables/name.rb +3 -0
- data/lib/fontisan/tables/os2.rb +3 -0
- data/lib/fontisan/tables/post.rb +3 -0
- data/lib/fontisan/tables/registry.rb +71 -0
- data/lib/fontisan/tables/vvar.rb +3 -0
- data/lib/fontisan/tables.rb +2 -0
- data/lib/fontisan/type1/afm_generator.rb +12 -65
- data/lib/fontisan/type1/charstring_converter.rb +2 -2
- data/lib/fontisan/type1/conversion_options.rb +3 -2
- data/lib/fontisan/type1/decryptor.rb +1 -1
- data/lib/fontisan/type1/generator.rb +14 -9
- data/lib/fontisan/type1/inf_generator.rb +10 -35
- data/lib/fontisan/type1/pfa_generator.rb +36 -26
- data/lib/fontisan/type1/pfb_generator.rb +24 -12
- data/lib/fontisan/type1/pfm_generator.rb +14 -31
- data/lib/fontisan/type1/ttf_to_type1_converter.rb +30 -24
- data/lib/fontisan/ufo/bounds.rb +91 -0
- data/lib/fontisan/ufo/compile/fvar.rb +2 -2
- data/lib/fontisan/ufo/contour.rb +5 -0
- data/lib/fontisan/ufo/convert/from_bin_data.rb +15 -29
- data/lib/fontisan/ufo/info.rb +4 -0
- data/lib/fontisan/ufo/point.rb +0 -7
- data/lib/fontisan/ufo.rb +1 -0
- data/lib/fontisan/utilities/brotli_wrapper.rb +3 -5
- data/lib/fontisan/utilities/padding.rb +9 -8
- data/lib/fontisan/validation/collection_validator.rb +2 -2
- data/lib/fontisan/validators/validator.rb +4 -4
- data/lib/fontisan/variable/delta_applicator.rb +4 -11
- data/lib/fontisan/variable/metric_delta_processor.rb +3 -3
- data/lib/fontisan/variable/static_font_builder.rb +19 -23
- data/lib/fontisan/variation/instance_font_wrapper.rb +41 -0
- data/lib/fontisan/variation/instance_writer.rb +19 -17
- data/lib/fontisan/variation/metrics_adjuster.rb +17 -20
- data/lib/fontisan/variation/validator.rb +26 -38
- data/lib/fontisan/variation/variable_svg_generator.rb +0 -48
- data/lib/fontisan/variation/variation_preserver.rb +2 -3
- data/lib/fontisan/variation.rb +1 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/sfnt_checksum.rb +1 -1
- data/lib/fontisan/woff2/table_transformer.rb +22 -29
- data/lib/fontisan/woff2_font.rb +9 -8
- data/lib/fontisan/woff_font.rb +2 -0
- data/lib/fontisan.rb +1 -0
- metadata +24 -2
|
@@ -10,6 +10,9 @@ module Fontisan
|
|
|
10
10
|
# across nine call sites before extraction; this module is the
|
|
11
11
|
# single source of truth.
|
|
12
12
|
#
|
|
13
|
+
# All public methods take an Integer size. Callers with a String
|
|
14
|
+
# compute `.bytesize` first.
|
|
15
|
+
#
|
|
13
16
|
# @example Get padding length
|
|
14
17
|
# Padding.boundary(13) # => 3
|
|
15
18
|
# Padding.boundary(13, boundary: 4) # => 3
|
|
@@ -21,14 +24,13 @@ module Fontisan
|
|
|
21
24
|
module Padding
|
|
22
25
|
# Count of trailing pad bytes needed to align `size` to `boundary`.
|
|
23
26
|
#
|
|
24
|
-
# @param size [Integer
|
|
25
|
-
#
|
|
27
|
+
# @param size [Integer] Size in bytes (callers with a String
|
|
28
|
+
# compute `.bytesize` first).
|
|
26
29
|
# @param boundary [Integer] Alignment boundary in bytes (default
|
|
27
30
|
# `Constants::TABLE_ALIGNMENT`).
|
|
28
31
|
# @return [Integer] Number of pad bytes in 0...boundary-1
|
|
29
32
|
def self.boundary(size, boundary: Constants::TABLE_ALIGNMENT)
|
|
30
|
-
|
|
31
|
-
(boundary - (length % boundary)) % boundary
|
|
33
|
+
(boundary - (size % boundary)) % boundary
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
# Return `bytes` followed by null padding to the requested boundary.
|
|
@@ -38,7 +40,7 @@ module Fontisan
|
|
|
38
40
|
# @return [String] The original string if already aligned, else a
|
|
39
41
|
# new binary string with trailing nulls.
|
|
40
42
|
def self.pad(bytes, boundary: Constants::TABLE_ALIGNMENT)
|
|
41
|
-
pad_count = boundary(bytes, boundary:)
|
|
43
|
+
pad_count = boundary(bytes.bytesize, boundary:)
|
|
42
44
|
return bytes if pad_count.zero?
|
|
43
45
|
|
|
44
46
|
out = bytes.dup.force_encoding(Encoding::BINARY)
|
|
@@ -48,12 +50,11 @@ module Fontisan
|
|
|
48
50
|
|
|
49
51
|
# Aligned size of `size` after padding to `boundary`.
|
|
50
52
|
#
|
|
51
|
-
# @param size [Integer
|
|
53
|
+
# @param size [Integer] Original size in bytes.
|
|
52
54
|
# @param boundary [Integer] Alignment boundary in bytes.
|
|
53
55
|
# @return [Integer] Size after alignment (multiple of `boundary`).
|
|
54
56
|
def self.aligned_size(size, boundary: Constants::TABLE_ALIGNMENT)
|
|
55
|
-
|
|
56
|
-
length + boundary(length, boundary:)
|
|
57
|
+
size + boundary(size, boundary:)
|
|
57
58
|
end
|
|
58
59
|
end
|
|
59
60
|
end
|
|
@@ -246,7 +246,7 @@ module Fontisan
|
|
|
246
246
|
# @param font [Object] Font object
|
|
247
247
|
# @return [Boolean] true if TrueType
|
|
248
248
|
def truetype_font?(font)
|
|
249
|
-
return false unless font.
|
|
249
|
+
return false unless font.is_a?(SfntSource)
|
|
250
250
|
|
|
251
251
|
font.has_table?("glyf")
|
|
252
252
|
end
|
|
@@ -256,7 +256,7 @@ module Fontisan
|
|
|
256
256
|
# @param font [Object] Font object
|
|
257
257
|
# @return [Boolean] true if CFF
|
|
258
258
|
def cff_font?(font)
|
|
259
|
-
return false unless font.
|
|
259
|
+
return false unless font.is_a?(SfntSource)
|
|
260
260
|
|
|
261
261
|
font.has_table?("CFF ") || font.has_table?("CFF2")
|
|
262
262
|
end
|
|
@@ -290,10 +290,10 @@ module Fontisan
|
|
|
290
290
|
}
|
|
291
291
|
end
|
|
292
292
|
|
|
293
|
-
# Get field value
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
293
|
+
# Get field value via the table's snapshot hash (BinData records
|
|
294
|
+
# expose all declared fields through #snapshot).
|
|
295
|
+
snapshot = table.is_a?(BinData::Record) ? table.snapshot : {}
|
|
296
|
+
value = snapshot[field_key.to_s] || snapshot[field_key.to_sym]
|
|
297
297
|
|
|
298
298
|
result = check_def[:block].call(table, value)
|
|
299
299
|
passed = result != false && !result.nil?
|
|
@@ -260,20 +260,13 @@ module Fontisan
|
|
|
260
260
|
# @param tag [String] Table tag
|
|
261
261
|
# @return [Object, nil] Table object or nil
|
|
262
262
|
def load_table(tag)
|
|
263
|
-
return nil unless @font.
|
|
263
|
+
return nil unless @font.is_a?(SfntSource)
|
|
264
264
|
|
|
265
265
|
data = @font.table_data(tag)
|
|
266
266
|
return nil if data.nil? || data.empty?
|
|
267
267
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
when "fvar" then Tables::Fvar
|
|
271
|
-
when "gvar" then Tables::Gvar
|
|
272
|
-
when "HVAR" then Tables::Hvar
|
|
273
|
-
when "VVAR" then Tables::Vvar
|
|
274
|
-
when "MVAR" then Tables::Mvar
|
|
275
|
-
else return nil
|
|
276
|
-
end
|
|
268
|
+
table_class = Tables::Registry.for(tag)
|
|
269
|
+
return nil unless table_class
|
|
277
270
|
|
|
278
271
|
table_class.read(data)
|
|
279
272
|
rescue StandardError => e
|
|
@@ -293,7 +286,7 @@ module Fontisan
|
|
|
293
286
|
end
|
|
294
287
|
|
|
295
288
|
# Try VVAR
|
|
296
|
-
if @vvar.
|
|
289
|
+
if @vvar.is_a?(Tables::Vvar) && @vvar.item_variation_store
|
|
297
290
|
return @vvar.item_variation_store.variation_region_list
|
|
298
291
|
end
|
|
299
292
|
|
|
@@ -221,17 +221,17 @@ module Fontisan
|
|
|
221
221
|
|
|
222
222
|
# Similar to HVAR but for vertical metrics
|
|
223
223
|
# VVAR has the same structure as HVAR
|
|
224
|
-
if
|
|
224
|
+
if (delta_set = @vvar.advance_height_delta_set(glyph_id))
|
|
225
225
|
accumulated = calculate_accumulated_delta(delta_set, region_scalars)
|
|
226
226
|
result[:advance_height] = apply_rounding(accumulated)
|
|
227
227
|
end
|
|
228
228
|
|
|
229
|
-
if
|
|
229
|
+
if (delta_set = @vvar.tsb_delta_set(glyph_id))
|
|
230
230
|
accumulated = calculate_accumulated_delta(delta_set, region_scalars)
|
|
231
231
|
result[:tsb] = apply_rounding(accumulated)
|
|
232
232
|
end
|
|
233
233
|
|
|
234
|
-
if
|
|
234
|
+
if (delta_set = @vvar.bsb_delta_set(glyph_id))
|
|
235
235
|
accumulated = calculate_accumulated_delta(delta_set, region_scalars)
|
|
236
236
|
result[:bsb] = apply_rounding(accumulated)
|
|
237
237
|
end
|
|
@@ -65,19 +65,22 @@ options = {})
|
|
|
65
65
|
File.binwrite(output_path, binary)
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
# Per-tag table update dispatch. Each entry maps a tag to
|
|
69
|
+
# [method_name, context_key] for looking up the right args.
|
|
70
|
+
TABLE_UPDATERS = {
|
|
71
|
+
"hmtx" => %i[update_hmtx_table varied_metrics],
|
|
72
|
+
"hhea" => %i[update_hhea_table font_metrics],
|
|
73
|
+
"OS/2" => %i[update_os2_table font_metrics],
|
|
74
|
+
"head" => %i[update_head_table options],
|
|
75
|
+
}.freeze
|
|
76
|
+
|
|
68
77
|
private
|
|
69
78
|
|
|
70
|
-
# Collect all tables for static font
|
|
71
|
-
#
|
|
72
|
-
# @param varied_metrics [Hash] Varied glyph metrics
|
|
73
|
-
# @param font_metrics [Hash] Varied font metrics
|
|
74
|
-
# @param options [Hash] Build options
|
|
75
|
-
# @return [Hash<String, String>] Map of table tag to binary data
|
|
76
79
|
def collect_tables(varied_metrics, font_metrics, options)
|
|
77
80
|
tables = {}
|
|
78
81
|
|
|
79
82
|
# Get all table tags from font
|
|
80
|
-
table_tags = @font.
|
|
83
|
+
table_tags = @font.is_a?(SfntSource) ? @font.table_names : []
|
|
81
84
|
|
|
82
85
|
table_tags.each do |tag|
|
|
83
86
|
# Skip variation tables
|
|
@@ -88,17 +91,14 @@ options = {})
|
|
|
88
91
|
next if original_data.nil? || original_data.empty?
|
|
89
92
|
|
|
90
93
|
# Update specific tables with varied data
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
when "head"
|
|
99
|
-
update_head_table(original_data, options)
|
|
94
|
+
updater = TABLE_UPDATERS[tag]
|
|
95
|
+
tables[tag] = if updater
|
|
96
|
+
method_name, ctx_key = updater
|
|
97
|
+
ctx = { varied_metrics: varied_metrics,
|
|
98
|
+
font_metrics: font_metrics,
|
|
99
|
+
options: options }
|
|
100
|
+
method(method_name).call(original_data, ctx[ctx_key])
|
|
100
101
|
else
|
|
101
|
-
# Copy unchanged
|
|
102
102
|
original_data
|
|
103
103
|
end
|
|
104
104
|
end
|
|
@@ -182,12 +182,8 @@ options = {})
|
|
|
182
182
|
data = @font.table_data(tag)
|
|
183
183
|
return nil if data.nil? || data.empty?
|
|
184
184
|
|
|
185
|
-
table_class =
|
|
186
|
-
|
|
187
|
-
when "maxp" then Tables::Maxp
|
|
188
|
-
when "head" then Tables::Head
|
|
189
|
-
else return nil
|
|
190
|
-
end
|
|
185
|
+
table_class = Tables::Registry.for(tag)
|
|
186
|
+
return nil unless table_class
|
|
191
187
|
|
|
192
188
|
table_class.read(data)
|
|
193
189
|
rescue StandardError
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "delegate"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Variation
|
|
7
|
+
# Wraps a variable font to substitute specific table tags with
|
|
8
|
+
# instance-generated equivalents while forwarding everything else
|
|
9
|
+
# to the original font. Used by VariableSvgGenerator to feed the
|
|
10
|
+
# SVG generator an instance-shaped font without rewriting it.
|
|
11
|
+
#
|
|
12
|
+
# Delegation uses SimpleDelegator so the wrapper is transparent:
|
|
13
|
+
# the original font's interface is preserved verbatim. Only
|
|
14
|
+
# #has_table? is overridden to also report instance-supplied tags.
|
|
15
|
+
#
|
|
16
|
+
# NOTE: #table currently delegates to the original font for any tag
|
|
17
|
+
# not in the instance map. The previous implementation always
|
|
18
|
+
# delegated regardless of @table_data contents, which SvgGenerator
|
|
19
|
+
# depended on (it expects parsed BinData records, not raw bytes).
|
|
20
|
+
# Until SvgGenerator can consume raw instance bytes, this wrapper
|
|
21
|
+
# preserves that behavior. The @table_data map is still exposed so
|
|
22
|
+
# downstream code that wants the raw bytes can read them.
|
|
23
|
+
class InstanceFontWrapper < SimpleDelegator
|
|
24
|
+
# @return [Hash<String, String>] instance-generated table bytes by tag
|
|
25
|
+
attr_reader :table_data
|
|
26
|
+
|
|
27
|
+
# @param original_font [TrueTypeFont, OpenTypeFont] the variable font
|
|
28
|
+
# @param instance_tables [Hash<String, String>] instance-generated bytes
|
|
29
|
+
def initialize(original_font, instance_tables)
|
|
30
|
+
super(original_font)
|
|
31
|
+
@table_data = instance_tables
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param tag [String] table tag
|
|
35
|
+
# @return [Boolean]
|
|
36
|
+
def has_table?(tag)
|
|
37
|
+
@table_data.key?(tag) || __getobj__.has_table?(tag)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -251,24 +251,26 @@ module Fontisan
|
|
|
251
251
|
# Parse table data into table object
|
|
252
252
|
#
|
|
253
253
|
# @param tag [String] Table tag
|
|
254
|
-
|
|
255
|
-
|
|
254
|
+
public
|
|
255
|
+
|
|
256
|
+
# Per-tag table parser dispatch. Each entry maps tag → [class, init_mode].
|
|
257
|
+
# :parse means call obj.parse(data); :data= means call obj.data = data.
|
|
258
|
+
TABLE_PARSERS = {
|
|
259
|
+
"head" => [Tables::Head, :parse],
|
|
260
|
+
"maxp" => [Tables::Maxp, :parse],
|
|
261
|
+
"loca" => [Tables::Loca, :data_assign],
|
|
262
|
+
"glyf" => [Tables::Glyf, :data_assign],
|
|
263
|
+
"CFF " => [Tables::Cff, :parse],
|
|
264
|
+
"CFF2" => [Tables::Cff2, :parse],
|
|
265
|
+
}.freeze
|
|
266
|
+
|
|
256
267
|
def parse_table(tag, data)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
Tables::Maxp.new.tap { |t| t.parse(data) }
|
|
264
|
-
when "loca"
|
|
265
|
-
Tables::Loca.new.tap { |t| t.data = data }
|
|
266
|
-
when "glyf"
|
|
267
|
-
Tables::Glyf.new.tap { |t| t.data = data }
|
|
268
|
-
when "CFF "
|
|
269
|
-
Tables::Cff.new.tap { |t| t.parse(data) }
|
|
270
|
-
when "CFF2"
|
|
271
|
-
Tables::Cff2.new.tap { |t| t.parse(data) }
|
|
268
|
+
entry = TABLE_PARSERS[tag]
|
|
269
|
+
if entry
|
|
270
|
+
klass, init_mode = entry
|
|
271
|
+
obj = klass.new
|
|
272
|
+
init_mode == :parse ? obj.parse(data) : obj.data = data
|
|
273
|
+
obj
|
|
272
274
|
else
|
|
273
275
|
# For other tables, return a simple object that just holds data
|
|
274
276
|
Object.new.tap do |obj|
|
|
@@ -213,26 +213,23 @@ module Fontisan
|
|
|
213
213
|
end
|
|
214
214
|
end
|
|
215
215
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
#
|
|
219
|
-
|
|
216
|
+
public
|
|
217
|
+
|
|
218
|
+
# Maps MVAR metric tags to [table_tag, method] for base value lookup.
|
|
219
|
+
METRIC_SOURCES = {
|
|
220
|
+
"hasc" => ["hhea", :ascender],
|
|
221
|
+
"hdsc" => ["hhea", :descender],
|
|
222
|
+
"hlgp" => ["hhea", :line_gap],
|
|
223
|
+
"xhgt" => ["OS/2", :sx_height],
|
|
224
|
+
"cpht" => ["OS/2", :s_cap_height],
|
|
225
|
+
}.freeze
|
|
226
|
+
|
|
220
227
|
def get_base_metric_value(tag)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
when "hlgp"
|
|
227
|
-
variation_table("hhea")&.line_gap
|
|
228
|
-
when "xhgt"
|
|
229
|
-
os2 = variation_table("OS/2")
|
|
230
|
-
os2&.s_x_height if os2.respond_to?(:s_x_height)
|
|
231
|
-
when "cpht"
|
|
232
|
-
os2 = variation_table("OS/2")
|
|
233
|
-
os2&.s_cap_height if os2.respond_to?(:s_cap_height)
|
|
234
|
-
# Add more metrics as needed
|
|
235
|
-
end
|
|
228
|
+
source = METRIC_SOURCES[tag]
|
|
229
|
+
return nil unless source
|
|
230
|
+
|
|
231
|
+
table_tag, method = source
|
|
232
|
+
variation_table(table_tag)&.public_send(method)
|
|
236
233
|
end
|
|
237
234
|
|
|
238
235
|
# Update font metric in appropriate table
|
|
@@ -307,7 +304,7 @@ module Fontisan
|
|
|
307
304
|
return unless hhea
|
|
308
305
|
|
|
309
306
|
# Update the field if hhea supports it
|
|
310
|
-
hhea.number_of_h_metrics = count
|
|
307
|
+
hhea.number_of_h_metrics = count
|
|
311
308
|
end
|
|
312
309
|
end
|
|
313
310
|
end
|
|
@@ -103,11 +103,9 @@ module Fontisan
|
|
|
103
103
|
# Check CFF2 if present
|
|
104
104
|
if @font.has_table?("CFF2")
|
|
105
105
|
cff2 = @font.table("CFF2")
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
@errors << "CFF2 axis count (#{cff2_axes}) doesn't match fvar (#{axis_count})"
|
|
110
|
-
end
|
|
106
|
+
cff2_axes = cff2&.num_axes || 0
|
|
107
|
+
if cff2_axes != axis_count && cff2_axes.positive?
|
|
108
|
+
@errors << "CFF2 axis count (#{cff2_axes}) doesn't match fvar (#{axis_count})"
|
|
111
109
|
end
|
|
112
110
|
end
|
|
113
111
|
|
|
@@ -133,20 +131,17 @@ module Fontisan
|
|
|
133
131
|
return unless @font.has_table?(table_tag)
|
|
134
132
|
|
|
135
133
|
table = @font.table(table_tag)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
store = table.item_variation_store
|
|
134
|
+
store = table&.item_variation_store
|
|
139
135
|
return unless store
|
|
140
136
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
end
|
|
137
|
+
region_list = store.variation_region_list
|
|
138
|
+
return unless region_list
|
|
139
|
+
|
|
140
|
+
region_axes = region_list.axis_count
|
|
141
|
+
return unless region_axes
|
|
142
|
+
|
|
143
|
+
if region_axes != expected_axes
|
|
144
|
+
@errors << "#{table_tag} region axis count (#{region_axes}) doesn't match fvar (#{expected_axes})"
|
|
150
145
|
end
|
|
151
146
|
end
|
|
152
147
|
|
|
@@ -195,24 +190,15 @@ module Fontisan
|
|
|
195
190
|
hvar = @font.table("HVAR")
|
|
196
191
|
return unless hvar
|
|
197
192
|
|
|
198
|
-
# Check for item_variation_store
|
|
199
|
-
unless hvar.respond_to?(:item_variation_store)
|
|
200
|
-
@warnings << "HVAR table doesn't support item_variation_store"
|
|
201
|
-
return
|
|
202
|
-
end
|
|
203
|
-
|
|
204
193
|
store = hvar.item_variation_store
|
|
205
194
|
unless store
|
|
206
195
|
@warnings << "HVAR has no item variation store"
|
|
207
196
|
return
|
|
208
197
|
end
|
|
209
198
|
|
|
210
|
-
|
|
211
|
-
if
|
|
212
|
-
|
|
213
|
-
if data.nil? || data.empty?
|
|
214
|
-
@warnings << "HVAR has no variation data"
|
|
215
|
-
end
|
|
199
|
+
data = store.item_variation_data_entries
|
|
200
|
+
if data.nil? || data.empty?
|
|
201
|
+
@warnings << "HVAR has no variation data"
|
|
216
202
|
end
|
|
217
203
|
rescue StandardError => e
|
|
218
204
|
@warnings << "Failed to check HVAR delta integrity: #{e.message}"
|
|
@@ -272,24 +258,26 @@ module Fontisan
|
|
|
272
258
|
# @param axes [Array] Variation axes
|
|
273
259
|
def check_metrics_region_coverage(table_tag, axes)
|
|
274
260
|
table = @font.table(table_tag)
|
|
275
|
-
return unless table
|
|
261
|
+
return unless table
|
|
276
262
|
|
|
277
263
|
store = table.item_variation_store
|
|
278
|
-
return unless store
|
|
264
|
+
return unless store
|
|
279
265
|
|
|
280
|
-
region_list = store.
|
|
281
|
-
return unless region_list
|
|
266
|
+
region_list = store.variation_region_list
|
|
267
|
+
return unless region_list
|
|
282
268
|
|
|
283
|
-
# Check each region
|
|
284
269
|
regions = region_list.regions
|
|
285
|
-
|
|
286
|
-
|
|
270
|
+
return unless regions
|
|
271
|
+
|
|
272
|
+
# Each region is an Array<RegionAxisCoordinates>; check coords
|
|
273
|
+
# are within the valid [-1, 1] normalized range.
|
|
274
|
+
regions.each_with_index do |region_axis_coords, idx|
|
|
275
|
+
next unless region_axis_coords.is_a?(Array)
|
|
287
276
|
|
|
288
|
-
|
|
277
|
+
region_axis_coords.each_with_index do |reg_axis, axis_idx|
|
|
289
278
|
next if axis_idx >= axes.length
|
|
290
279
|
next unless reg_axis
|
|
291
280
|
|
|
292
|
-
# Check coordinates are in valid range [-1, 1]
|
|
293
281
|
coords = {
|
|
294
282
|
start_coord: reg_axis.start_coord,
|
|
295
283
|
peak_coord: reg_axis.peak_coord,
|
|
@@ -178,8 +178,6 @@ module Fontisan
|
|
|
178
178
|
# @param tables [Hash<String, String>] Font tables
|
|
179
179
|
# @return [Object] Font-like object
|
|
180
180
|
def build_font_from_tables(tables)
|
|
181
|
-
# Create a simple font wrapper that implements the minimal
|
|
182
|
-
# interface needed by SvgGenerator
|
|
183
181
|
InstanceFontWrapper.new(@font, tables)
|
|
184
182
|
end
|
|
185
183
|
|
|
@@ -214,52 +212,6 @@ module Fontisan
|
|
|
214
212
|
end
|
|
215
213
|
coords
|
|
216
214
|
end
|
|
217
|
-
|
|
218
|
-
# Wrapper class for instance font tables
|
|
219
|
-
#
|
|
220
|
-
# Provides minimal interface needed by SvgGenerator while using
|
|
221
|
-
# instance tables instead of original font tables.
|
|
222
|
-
class InstanceFontWrapper
|
|
223
|
-
# @return [Hash<String, String>] Font tables
|
|
224
|
-
attr_reader :table_data
|
|
225
|
-
|
|
226
|
-
# Initialize wrapper
|
|
227
|
-
#
|
|
228
|
-
# @param original_font [Object] Original variable font
|
|
229
|
-
# @param instance_tables [Hash<String, String>] Instance tables
|
|
230
|
-
def initialize(original_font, instance_tables)
|
|
231
|
-
@original_font = original_font
|
|
232
|
-
@table_data = instance_tables
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
# Get table by tag
|
|
236
|
-
#
|
|
237
|
-
# @param tag [String] Table tag
|
|
238
|
-
# @return [Object, nil] Table or nil
|
|
239
|
-
def table(tag)
|
|
240
|
-
# Use instance table if available, otherwise fall back to original
|
|
241
|
-
if @table_data.key?(tag)
|
|
242
|
-
end
|
|
243
|
-
@original_font.table(tag)
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
# Check if table exists
|
|
247
|
-
#
|
|
248
|
-
# @param tag [String] Table tag
|
|
249
|
-
# @return [Boolean] True if table exists
|
|
250
|
-
def has_table?(tag)
|
|
251
|
-
@table_data.key?(tag) || @original_font.has_table?(tag)
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
# Forward other methods to original font
|
|
255
|
-
def method_missing(method, ...)
|
|
256
|
-
@original_font.send(method, ...)
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
def respond_to_missing?(method, include_private = false)
|
|
260
|
-
@original_font.respond_to?(method, include_private) || super
|
|
261
|
-
end
|
|
262
|
-
end
|
|
263
215
|
end
|
|
264
216
|
end
|
|
265
217
|
end
|
|
@@ -148,10 +148,9 @@ module Fontisan
|
|
|
148
148
|
def validate_source!
|
|
149
149
|
raise ArgumentError, "Source font cannot be nil" if @source_font.nil?
|
|
150
150
|
|
|
151
|
-
unless @source_font.
|
|
152
|
-
@source_font.respond_to?(:table_data)
|
|
151
|
+
unless @source_font.is_a?(SfntSource)
|
|
153
152
|
raise ArgumentError,
|
|
154
|
-
"Source font must
|
|
153
|
+
"Source font must be an SfntSource instance"
|
|
155
154
|
end
|
|
156
155
|
|
|
157
156
|
if @target_tables.nil?
|
data/lib/fontisan/variation.rb
CHANGED
|
@@ -13,6 +13,7 @@ module Fontisan
|
|
|
13
13
|
autoload :DeltaParser, "fontisan/variation/delta_parser"
|
|
14
14
|
autoload :Inspector, "fontisan/variation/inspector"
|
|
15
15
|
autoload :InstanceGenerator, "fontisan/variation/instance_generator"
|
|
16
|
+
autoload :InstanceFontWrapper, "fontisan/variation/instance_font_wrapper"
|
|
16
17
|
autoload :InstanceWriter, "fontisan/variation/instance_writer"
|
|
17
18
|
autoload :Interpolator, "fontisan/variation/interpolator"
|
|
18
19
|
autoload :MetricsAdjuster, "fontisan/variation/metrics_adjuster"
|
data/lib/fontisan/version.rb
CHANGED
|
@@ -32,41 +32,34 @@ module Fontisan
|
|
|
32
32
|
#
|
|
33
33
|
# @param tag [String] Table tag
|
|
34
34
|
# @return [String, nil] Transformed table data
|
|
35
|
+
# Per-tag transform method dispatch.
|
|
36
|
+
TRANSFORM_DISPATCH = {
|
|
37
|
+
"glyf" => :transform_glyf,
|
|
38
|
+
"loca" => :transform_loca,
|
|
39
|
+
"hmtx" => :transform_hmtx,
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
# Per-tag transform version constant.
|
|
43
|
+
TRANSFORM_VERSIONS = {
|
|
44
|
+
"glyf" => Directory::TRANSFORM_GLYF_LOCA,
|
|
45
|
+
"loca" => Directory::TRANSFORM_GLYF_LOCA,
|
|
46
|
+
"hmtx" => Directory::TRANSFORM_HMTX,
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
35
49
|
def transform_table(tag)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
when "hmtx"
|
|
42
|
-
transform_hmtx
|
|
43
|
-
else
|
|
44
|
-
# No transformation, return original data
|
|
45
|
-
get_table_data(tag)
|
|
46
|
-
end
|
|
50
|
+
transform_method = TRANSFORM_DISPATCH[tag]
|
|
51
|
+
return method(transform_method).call if transform_method
|
|
52
|
+
|
|
53
|
+
# No transformation, return original data
|
|
54
|
+
get_table_data(tag)
|
|
47
55
|
end
|
|
48
56
|
|
|
49
|
-
# Check if a table can be transformed
|
|
50
|
-
#
|
|
51
|
-
# @param tag [String] Table tag
|
|
52
|
-
# @return [Boolean] True if table supports transformation
|
|
53
57
|
def transformable?(tag)
|
|
54
|
-
|
|
58
|
+
TRANSFORM_DISPATCH.key?(tag)
|
|
55
59
|
end
|
|
56
60
|
|
|
57
|
-
# Determine transformation version for a table
|
|
58
|
-
#
|
|
59
|
-
# @param tag [String] Table tag
|
|
60
|
-
# @return [Integer] Transformation version
|
|
61
61
|
def transformation_version(tag)
|
|
62
|
-
|
|
63
|
-
when "glyf", "loca"
|
|
64
|
-
Directory::TRANSFORM_GLYF_LOCA
|
|
65
|
-
when "hmtx"
|
|
66
|
-
Directory::TRANSFORM_HMTX
|
|
67
|
-
else
|
|
68
|
-
Directory::TRANSFORM_NONE
|
|
69
|
-
end
|
|
62
|
+
TRANSFORM_VERSIONS.fetch(tag, Directory::TRANSFORM_NONE)
|
|
70
63
|
end
|
|
71
64
|
|
|
72
65
|
private
|
|
@@ -150,7 +143,7 @@ module Fontisan
|
|
|
150
143
|
# @param tag [String] Table tag
|
|
151
144
|
# @return [String, nil] Table data or nil if not found
|
|
152
145
|
def get_table_data(tag)
|
|
153
|
-
return nil unless font.
|
|
146
|
+
return nil unless font.is_a?(SfntSource)
|
|
154
147
|
|
|
155
148
|
font.table_data[tag]
|
|
156
149
|
end
|