fontisan 0.2.0 → 0.2.2
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 +119 -308
- data/README.adoc +1525 -1323
- data/Rakefile +45 -47
- data/benchmark/variation_quick_bench.rb +4 -4
- data/docs/FONT_HINTING.adoc +562 -0
- data/docs/VARIABLE_FONT_OPERATIONS.adoc +599 -0
- data/lib/fontisan/cli.rb +92 -34
- data/lib/fontisan/collection/builder.rb +82 -0
- data/lib/fontisan/collection/offset_calculator.rb +2 -0
- data/lib/fontisan/collection/table_deduplicator.rb +76 -0
- data/lib/fontisan/commands/base_command.rb +21 -2
- data/lib/fontisan/commands/convert_command.rb +96 -165
- data/lib/fontisan/commands/info_command.rb +111 -5
- data/lib/fontisan/commands/instance_command.rb +77 -85
- data/lib/fontisan/commands/validate_command.rb +28 -0
- data/lib/fontisan/config/validation_rules.yml +1 -1
- data/lib/fontisan/constants.rb +34 -24
- data/lib/fontisan/converters/format_converter.rb +154 -1
- data/lib/fontisan/converters/outline_converter.rb +101 -34
- data/lib/fontisan/converters/woff_writer.rb +9 -4
- data/lib/fontisan/font_loader.rb +14 -9
- data/lib/fontisan/font_writer.rb +9 -6
- data/lib/fontisan/formatters/text_formatter.rb +45 -1
- data/lib/fontisan/hints/hint_converter.rb +131 -2
- data/lib/fontisan/hints/hint_validator.rb +284 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +219 -140
- data/lib/fontisan/hints/postscript_hint_extractor.rb +151 -16
- data/lib/fontisan/hints/truetype_hint_applier.rb +90 -44
- data/lib/fontisan/hints/truetype_hint_extractor.rb +134 -11
- data/lib/fontisan/hints/truetype_instruction_analyzer.rb +261 -0
- data/lib/fontisan/hints/truetype_instruction_generator.rb +266 -0
- data/lib/fontisan/loading_modes.rb +6 -4
- data/lib/fontisan/models/collection_brief_info.rb +31 -0
- data/lib/fontisan/models/font_info.rb +3 -30
- data/lib/fontisan/models/hint.rb +183 -12
- data/lib/fontisan/models/outline.rb +4 -1
- data/lib/fontisan/open_type_font.rb +28 -10
- data/lib/fontisan/open_type_font_extensions.rb +54 -0
- data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
- data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
- data/lib/fontisan/pipeline/format_detector.rb +249 -0
- data/lib/fontisan/pipeline/output_writer.rb +159 -0
- data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
- data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
- data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
- data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +416 -0
- data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
- data/lib/fontisan/subset/table_subsetter.rb +5 -5
- data/lib/fontisan/tables/cff/charstring.rb +58 -3
- data/lib/fontisan/tables/cff/charstring_builder.rb +34 -0
- data/lib/fontisan/tables/cff/charstring_parser.rb +249 -0
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +19 -1
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +209 -0
- data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
- data/lib/fontisan/tables/cff/private_dict_writer.rb +131 -0
- data/lib/fontisan/tables/cff/table_builder.rb +221 -0
- data/lib/fontisan/tables/cff.rb +2 -0
- data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +247 -0
- data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
- data/lib/fontisan/tables/cff2/table_builder.rb +580 -0
- data/lib/fontisan/tables/cff2/table_reader.rb +421 -0
- data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
- data/lib/fontisan/tables/cff2.rb +10 -5
- data/lib/fontisan/tables/cvar.rb +2 -41
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +2 -1
- data/lib/fontisan/tables/glyf/curve_converter.rb +10 -4
- data/lib/fontisan/tables/glyf/glyph_builder.rb +27 -10
- data/lib/fontisan/tables/gvar.rb +2 -41
- data/lib/fontisan/tables/name.rb +4 -4
- data/lib/fontisan/true_type_font.rb +27 -10
- data/lib/fontisan/true_type_font_extensions.rb +54 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +42 -0
- data/lib/fontisan/validation/checksum_validator.rb +2 -2
- data/lib/fontisan/validation/table_validator.rb +1 -1
- data/lib/fontisan/validation/variable_font_validator.rb +218 -0
- data/lib/fontisan/variation/cache.rb +3 -1
- data/lib/fontisan/variation/converter.rb +121 -13
- data/lib/fontisan/variation/delta_applier.rb +2 -1
- data/lib/fontisan/variation/inspector.rb +2 -1
- data/lib/fontisan/variation/instance_generator.rb +2 -1
- data/lib/fontisan/variation/instance_writer.rb +341 -0
- data/lib/fontisan/variation/optimizer.rb +6 -3
- data/lib/fontisan/variation/subsetter.rb +32 -10
- data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
- data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
- data/lib/fontisan/variation/variation_preserver.rb +291 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/version.rb.orig +9 -0
- data/lib/fontisan/woff2/glyf_transformer.rb +693 -0
- data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
- data/lib/fontisan/woff2_font.rb +489 -468
- data/lib/fontisan/woff_font.rb +16 -11
- data/lib/fontisan.rb +54 -2
- data/scripts/measure_optimization.rb +15 -7
- metadata +37 -2
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Validation
|
|
5
|
+
# VariableFontValidator validates variable font structure
|
|
6
|
+
#
|
|
7
|
+
# Validates:
|
|
8
|
+
# - fvar table structure
|
|
9
|
+
# - Axis definitions and ranges
|
|
10
|
+
# - Instance definitions
|
|
11
|
+
# - Variation table consistency
|
|
12
|
+
# - Metrics variation tables
|
|
13
|
+
#
|
|
14
|
+
# @example Validate a variable font
|
|
15
|
+
# validator = VariableFontValidator.new(font)
|
|
16
|
+
# errors = validator.validate
|
|
17
|
+
# puts "Found #{errors.length} errors" if errors.any?
|
|
18
|
+
class VariableFontValidator
|
|
19
|
+
# Initialize validator with font
|
|
20
|
+
#
|
|
21
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font to validate
|
|
22
|
+
def initialize(font)
|
|
23
|
+
@font = font
|
|
24
|
+
@errors = []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Validate variable font
|
|
28
|
+
#
|
|
29
|
+
# @return [Array<String>] Array of error messages
|
|
30
|
+
def validate
|
|
31
|
+
return [] unless @font.has_table?("fvar")
|
|
32
|
+
|
|
33
|
+
validate_fvar_structure
|
|
34
|
+
validate_axes
|
|
35
|
+
validate_instances
|
|
36
|
+
validate_variation_tables
|
|
37
|
+
validate_metrics_variation
|
|
38
|
+
|
|
39
|
+
@errors
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Validate fvar table structure
|
|
45
|
+
#
|
|
46
|
+
# @return [void]
|
|
47
|
+
def validate_fvar_structure
|
|
48
|
+
fvar = @font.table("fvar")
|
|
49
|
+
return unless fvar
|
|
50
|
+
|
|
51
|
+
if !fvar.respond_to?(:axes) || fvar.axes.nil? || fvar.axes.empty?
|
|
52
|
+
@errors << "fvar: No axes defined"
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if fvar.respond_to?(:axis_count) && fvar.axis_count != fvar.axes.length
|
|
57
|
+
@errors << "fvar: Axis count mismatch (expected #{fvar.axis_count}, got #{fvar.axes.length})"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Validate all axes
|
|
62
|
+
#
|
|
63
|
+
# @return [void]
|
|
64
|
+
def validate_axes
|
|
65
|
+
fvar = @font.table("fvar")
|
|
66
|
+
return unless fvar.respond_to?(:axes)
|
|
67
|
+
|
|
68
|
+
fvar.axes.each_with_index do |axis, index|
|
|
69
|
+
validate_axis_range(axis, index)
|
|
70
|
+
validate_axis_tag(axis, index)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Validate axis range values
|
|
75
|
+
#
|
|
76
|
+
# @param axis [Object] Axis object
|
|
77
|
+
# @param index [Integer] Axis index
|
|
78
|
+
# @return [void]
|
|
79
|
+
def validate_axis_range(axis, index)
|
|
80
|
+
return unless axis.respond_to?(:min_value) && axis.respond_to?(:max_value)
|
|
81
|
+
|
|
82
|
+
if axis.min_value > axis.max_value
|
|
83
|
+
tag = axis.respond_to?(:axis_tag) ? axis.axis_tag : "axis #{index}"
|
|
84
|
+
@errors << "Axis #{tag}: min_value (#{axis.min_value}) > max_value (#{axis.max_value})"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if axis.respond_to?(:default_value) && (axis.default_value < axis.min_value || axis.default_value > axis.max_value)
|
|
88
|
+
tag = axis.respond_to?(:axis_tag) ? axis.axis_tag : "axis #{index}"
|
|
89
|
+
@errors << "Axis #{tag}: default_value (#{axis.default_value}) out of range [#{axis.min_value}, #{axis.max_value}]"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Validate axis tag format
|
|
94
|
+
#
|
|
95
|
+
# @param axis [Object] Axis object
|
|
96
|
+
# @param index [Integer] Axis index
|
|
97
|
+
# @return [void]
|
|
98
|
+
def validate_axis_tag(axis, index)
|
|
99
|
+
return unless axis.respond_to?(:axis_tag)
|
|
100
|
+
|
|
101
|
+
tag = axis.axis_tag
|
|
102
|
+
unless tag.is_a?(String) && tag.length == 4 && tag =~ /^[a-zA-Z]{4}$/
|
|
103
|
+
@errors << "Axis #{index}: invalid tag '#{tag}' (must be 4 ASCII letters)"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Validate named instances
|
|
108
|
+
#
|
|
109
|
+
# @return [void]
|
|
110
|
+
def validate_instances
|
|
111
|
+
fvar = @font.table("fvar")
|
|
112
|
+
return unless fvar.respond_to?(:instances)
|
|
113
|
+
return unless fvar.instances
|
|
114
|
+
|
|
115
|
+
fvar.instances.each_with_index do |instance, idx|
|
|
116
|
+
validate_instance_coordinates(instance, idx, fvar)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Validate instance coordinates
|
|
121
|
+
#
|
|
122
|
+
# @param instance [Object] Instance object
|
|
123
|
+
# @param idx [Integer] Instance index
|
|
124
|
+
# @param fvar [Object] fvar table
|
|
125
|
+
# @return [void]
|
|
126
|
+
def validate_instance_coordinates(instance, idx, fvar)
|
|
127
|
+
return unless instance.is_a?(Hash) && instance[:coordinates]
|
|
128
|
+
|
|
129
|
+
coords = instance[:coordinates]
|
|
130
|
+
axis_count = fvar.respond_to?(:axis_count) ? fvar.axis_count : fvar.axes.length
|
|
131
|
+
|
|
132
|
+
if coords.length != axis_count
|
|
133
|
+
@errors << "Instance #{idx}: coordinate count mismatch (expected #{axis_count}, got #{coords.length})"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
coords.each_with_index do |value, axis_idx|
|
|
137
|
+
next if axis_idx >= fvar.axes.length
|
|
138
|
+
|
|
139
|
+
axis = fvar.axes[axis_idx]
|
|
140
|
+
next unless axis.respond_to?(:min_value) && axis.respond_to?(:max_value)
|
|
141
|
+
|
|
142
|
+
if value < axis.min_value || value > axis.max_value
|
|
143
|
+
tag = axis.respond_to?(:axis_tag) ? axis.axis_tag : "axis #{axis_idx}"
|
|
144
|
+
@errors << "Instance #{idx}: coordinate for #{tag} (#{value}) out of range [#{axis.min_value}, #{axis.max_value}]"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Validate variation tables
|
|
150
|
+
#
|
|
151
|
+
# @return [void]
|
|
152
|
+
def validate_variation_tables
|
|
153
|
+
has_gvar = @font.has_table?("gvar")
|
|
154
|
+
has_cff2 = @font.has_table?("CFF2")
|
|
155
|
+
has_glyf = @font.has_table?("glyf")
|
|
156
|
+
has_cff = @font.has_table?("CFF ")
|
|
157
|
+
|
|
158
|
+
# TrueType variable fonts should have gvar
|
|
159
|
+
if has_glyf && !has_gvar
|
|
160
|
+
@errors << "TrueType variable font missing gvar table"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# CFF variable fonts should have CFF2
|
|
164
|
+
if has_cff && !has_cff2
|
|
165
|
+
@errors << "CFF variable font missing CFF2 table"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Can't have both gvar and CFF2
|
|
169
|
+
if has_gvar && has_cff2
|
|
170
|
+
@errors << "Font has both gvar and CFF2 tables (incompatible)"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Validate metrics variation tables
|
|
175
|
+
#
|
|
176
|
+
# @return [void]
|
|
177
|
+
def validate_metrics_variation
|
|
178
|
+
validate_hvar if @font.has_table?("HVAR")
|
|
179
|
+
validate_vvar if @font.has_table?("VVAR")
|
|
180
|
+
validate_mvar if @font.has_table?("MVAR")
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Validate HVAR table
|
|
184
|
+
#
|
|
185
|
+
# @return [void]
|
|
186
|
+
def validate_hvar
|
|
187
|
+
# HVAR validation would go here
|
|
188
|
+
# For now, just check it exists
|
|
189
|
+
hvar = @font.table_data["HVAR"]
|
|
190
|
+
if hvar.nil? || hvar.empty?
|
|
191
|
+
@errors << "HVAR table is empty"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Validate VVAR table
|
|
196
|
+
#
|
|
197
|
+
# @return [void]
|
|
198
|
+
def validate_vvar
|
|
199
|
+
# VVAR validation would go here
|
|
200
|
+
vvar = @font.table_data["VVAR"]
|
|
201
|
+
if vvar.nil? || vvar.empty?
|
|
202
|
+
@errors << "VVAR table is empty"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Validate MVAR table
|
|
207
|
+
#
|
|
208
|
+
# @return [void]
|
|
209
|
+
def validate_mvar
|
|
210
|
+
# MVAR validation would go here
|
|
211
|
+
mvar = @font.table_data["MVAR"]
|
|
212
|
+
if mvar.nil? || mvar.empty?
|
|
213
|
+
@errors << "MVAR table is empty"
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -44,6 +44,7 @@ module Fontisan
|
|
|
44
44
|
@ttl = ttl
|
|
45
45
|
@cache = {}
|
|
46
46
|
@access_times = {}
|
|
47
|
+
@access_counter = 0
|
|
47
48
|
@stats = {
|
|
48
49
|
hits: 0,
|
|
49
50
|
misses: 0,
|
|
@@ -219,7 +220,8 @@ module Fontisan
|
|
|
219
220
|
#
|
|
220
221
|
# @param key [String] Cache key
|
|
221
222
|
def touch(key)
|
|
222
|
-
@
|
|
223
|
+
@access_counter += 1
|
|
224
|
+
@access_times[key] = @access_counter
|
|
223
225
|
end
|
|
224
226
|
|
|
225
227
|
# Evict entries if cache is full
|
|
@@ -24,13 +24,13 @@ module Fontisan
|
|
|
24
24
|
# 4. Build gvar table structure
|
|
25
25
|
#
|
|
26
26
|
# @example Converting gvar to CFF2 blend
|
|
27
|
-
# converter =
|
|
27
|
+
# converter = Converter.new(font, axes)
|
|
28
28
|
# blend_data = converter.gvar_to_blend(glyph_id)
|
|
29
29
|
#
|
|
30
30
|
# @example Converting CFF2 blend to gvar
|
|
31
|
-
# converter =
|
|
31
|
+
# converter = Converter.new(font, axes)
|
|
32
32
|
# tuple_data = converter.blend_to_gvar(glyph_id)
|
|
33
|
-
class
|
|
33
|
+
class Converter
|
|
34
34
|
include TableAccessor
|
|
35
35
|
|
|
36
36
|
# @return [TrueTypeFont, OpenTypeFont] Font instance
|
|
@@ -72,19 +72,49 @@ module Fontisan
|
|
|
72
72
|
#
|
|
73
73
|
# @param glyph_id [Integer] Glyph ID
|
|
74
74
|
# @return [Hash, nil] Tuple data or nil
|
|
75
|
-
def blend_to_gvar(
|
|
75
|
+
def blend_to_gvar(glyph_id)
|
|
76
76
|
return nil unless has_variation_table?("CFF2")
|
|
77
77
|
|
|
78
78
|
cff2 = variation_table("CFF2")
|
|
79
79
|
return nil unless cff2
|
|
80
80
|
|
|
81
81
|
# Get CharString with blend operators
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
charstring = cff2.charstring_for_glyph(glyph_id)
|
|
83
|
+
return nil unless charstring
|
|
84
84
|
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
# Parse CharString to extract blend data
|
|
86
|
+
charstring.parse unless charstring.instance_variable_get(:@parsed)
|
|
87
|
+
blend_data = charstring.blend_data
|
|
88
|
+
return nil if blend_data.nil? || blend_data.empty?
|
|
89
|
+
|
|
90
|
+
# Convert blend data to tuple format
|
|
91
|
+
convert_blend_to_tuples_for_glyph(blend_data)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Convert all glyphs from gvar to blend format
|
|
95
|
+
#
|
|
96
|
+
# @param glyph_count [Integer] Number of glyphs
|
|
97
|
+
# @return [Hash<Integer, Hash>] Map of glyph_id to blend data
|
|
98
|
+
def convert_all_gvar_to_blend(glyph_count)
|
|
99
|
+
return {} unless can_convert?
|
|
100
|
+
|
|
101
|
+
(0...glyph_count).each_with_object({}) do |glyph_id, result|
|
|
102
|
+
blend_data = gvar_to_blend(glyph_id)
|
|
103
|
+
result[glyph_id] = blend_data if blend_data
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Convert all glyphs from blend to gvar format
|
|
108
|
+
#
|
|
109
|
+
# @param glyph_count [Integer] Number of glyphs
|
|
110
|
+
# @return [Hash<Integer, Hash>] Map of glyph_id to tuple data
|
|
111
|
+
def convert_all_blend_to_gvar(glyph_count)
|
|
112
|
+
return {} unless can_convert?
|
|
113
|
+
|
|
114
|
+
(0...glyph_count).each_with_object({}) do |glyph_id, result|
|
|
115
|
+
tuple_data = blend_to_gvar(glyph_id)
|
|
116
|
+
result[glyph_id] = tuple_data if tuple_data
|
|
117
|
+
end
|
|
88
118
|
end
|
|
89
119
|
|
|
90
120
|
# Check if variation data can be converted
|
|
@@ -99,6 +129,77 @@ module Fontisan
|
|
|
99
129
|
|
|
100
130
|
private
|
|
101
131
|
|
|
132
|
+
# Convert blend data from a glyph to tuple format
|
|
133
|
+
#
|
|
134
|
+
# @param blend_data [Array<Hash>] Array of blend operations
|
|
135
|
+
# @return [Hash] Tuple variation data
|
|
136
|
+
def convert_blend_to_tuples_for_glyph(blend_data)
|
|
137
|
+
# Each blend operation represents variation at different points
|
|
138
|
+
# We need to aggregate these into region-based tuples
|
|
139
|
+
|
|
140
|
+
# Extract all regions from blend operations
|
|
141
|
+
regions_map = {}
|
|
142
|
+
point_count = 0
|
|
143
|
+
|
|
144
|
+
blend_data.each_with_index do |blend_op, idx|
|
|
145
|
+
blend_op[:blends].each do |blend|
|
|
146
|
+
# Track the maximum point index we've seen
|
|
147
|
+
point_count = [point_count, idx + 1].max
|
|
148
|
+
|
|
149
|
+
# For each delta axis, we need to create or update a region
|
|
150
|
+
blend[:deltas].each_with_index do |delta, axis_index|
|
|
151
|
+
next if delta.zero? # Skip zero deltas
|
|
152
|
+
|
|
153
|
+
# Create region key based on unique delta pattern
|
|
154
|
+
region_key = "region_#{axis_index}"
|
|
155
|
+
|
|
156
|
+
regions_map[region_key] ||= {
|
|
157
|
+
axis_index: axis_index,
|
|
158
|
+
deltas_per_point: Array.new(point_count) { { x: 0, y: 0 } },
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Store this delta for this point
|
|
162
|
+
# Note: CFF2 blend deltas are per-coordinate, we need to map to x/y
|
|
163
|
+
# This is a simplified mapping - full implementation would track
|
|
164
|
+
# which coordinates are being varied
|
|
165
|
+
regions_map[region_key][:deltas_per_point][idx / 2] ||= { x: 0,
|
|
166
|
+
y: 0 }
|
|
167
|
+
if idx.even?
|
|
168
|
+
regions_map[region_key][:deltas_per_point][idx / 2][:x] = delta
|
|
169
|
+
else
|
|
170
|
+
regions_map[region_key][:deltas_per_point][idx / 2][:y] = delta
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Convert regions to tuples
|
|
177
|
+
tuples = []
|
|
178
|
+
regions_map.each_value do |region_data|
|
|
179
|
+
axis_index = region_data[:axis_index]
|
|
180
|
+
|
|
181
|
+
# Build peak coordinates (one per axis)
|
|
182
|
+
peak = Array.new(@axes.length, 0.0)
|
|
183
|
+
peak[axis_index] = 1.0 if axis_index < @axes.length
|
|
184
|
+
|
|
185
|
+
# Build start/end (default full range)
|
|
186
|
+
start_vals = Array.new(@axes.length, -1.0)
|
|
187
|
+
end_vals = Array.new(@axes.length, 1.0)
|
|
188
|
+
|
|
189
|
+
tuples << {
|
|
190
|
+
peak: peak,
|
|
191
|
+
start: start_vals,
|
|
192
|
+
end: end_vals,
|
|
193
|
+
deltas: region_data[:deltas_per_point],
|
|
194
|
+
}
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
{
|
|
198
|
+
tuples: tuples,
|
|
199
|
+
point_count: point_count,
|
|
200
|
+
}
|
|
201
|
+
end
|
|
202
|
+
|
|
102
203
|
# Convert tuple variations to blend format
|
|
103
204
|
#
|
|
104
205
|
# @param tuple_data [Hash] Tuple variation data from gvar
|
|
@@ -172,12 +273,19 @@ module Fontisan
|
|
|
172
273
|
# @param tuple [Hash] Tuple data
|
|
173
274
|
# @param point_count [Integer] Number of points
|
|
174
275
|
# @return [Array<Hash>] Deltas with :x and :y
|
|
175
|
-
def parse_tuple_deltas(
|
|
176
|
-
#
|
|
177
|
-
|
|
276
|
+
def parse_tuple_deltas(tuple, point_count)
|
|
277
|
+
# If tuple has deltas array, use it
|
|
278
|
+
if tuple[:deltas].is_a?(Array)
|
|
279
|
+
return tuple[:deltas].map do |delta|
|
|
280
|
+
{ x: delta[:x] || 0, y: delta[:y] || 0 }
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Otherwise return zeros (placeholder for parsing raw delta data)
|
|
285
|
+
# Full implementation would:
|
|
286
|
+
# 1. Parse delta data from tuple[:data]
|
|
178
287
|
# 2. Decompress if needed
|
|
179
288
|
# 3. Return array of { x: dx, y: dy } for each point
|
|
180
|
-
|
|
181
289
|
Array.new(point_count) { { x: 0, y: 0 } }
|
|
182
290
|
end
|
|
183
291
|
|
|
@@ -79,7 +79,8 @@ module Fontisan
|
|
|
79
79
|
# Apply each active tuple's deltas
|
|
80
80
|
adjusted_points = base_points.dup
|
|
81
81
|
matches.each do |match|
|
|
82
|
-
apply_tuple_deltas(adjusted_points, match, tuple_data,
|
|
82
|
+
apply_tuple_deltas(adjusted_points, match, tuple_data,
|
|
83
|
+
base_points.length)
|
|
83
84
|
end
|
|
84
85
|
|
|
85
86
|
adjusted_points
|
|
@@ -107,7 +107,8 @@ module Fontisan
|
|
|
107
107
|
index: index,
|
|
108
108
|
name: instance_name(instance[:subfamily_name_id]),
|
|
109
109
|
postscript_name: instance_name(instance[:postscript_name_id]),
|
|
110
|
-
coordinates: instance_coordinates(instance[:coordinates],
|
|
110
|
+
coordinates: instance_coordinates(instance[:coordinates],
|
|
111
|
+
@context.axes),
|
|
111
112
|
}
|
|
112
113
|
end
|
|
113
114
|
end
|
|
@@ -248,7 +248,8 @@ module Fontisan
|
|
|
248
248
|
# @param scalars [Array<Float>] Region scalars
|
|
249
249
|
# @return [Hash] Interpolated point
|
|
250
250
|
def interpolate_point(base_point, delta_points, scalars)
|
|
251
|
-
@context.interpolator.interpolate_point(base_point, delta_points,
|
|
251
|
+
@context.interpolator.interpolate_point(base_point, delta_points,
|
|
252
|
+
scalars)
|
|
252
253
|
end
|
|
253
254
|
|
|
254
255
|
private
|