fontisan 0.1.0 → 0.2.0
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 +529 -65
- data/Gemfile +1 -0
- data/LICENSE +5 -1
- data/README.adoc +1301 -275
- data/Rakefile +27 -2
- data/benchmark/variation_quick_bench.rb +47 -0
- data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
- data/fontisan.gemspec +4 -1
- data/lib/fontisan/binary/base_record.rb +22 -1
- data/lib/fontisan/cli.rb +309 -0
- data/lib/fontisan/collection/builder.rb +260 -0
- data/lib/fontisan/collection/offset_calculator.rb +227 -0
- data/lib/fontisan/collection/table_analyzer.rb +204 -0
- data/lib/fontisan/collection/table_deduplicator.rb +241 -0
- data/lib/fontisan/collection/writer.rb +306 -0
- data/lib/fontisan/commands/base_command.rb +8 -1
- data/lib/fontisan/commands/convert_command.rb +291 -0
- data/lib/fontisan/commands/export_command.rb +161 -0
- data/lib/fontisan/commands/info_command.rb +40 -6
- data/lib/fontisan/commands/instance_command.rb +295 -0
- data/lib/fontisan/commands/ls_command.rb +113 -0
- data/lib/fontisan/commands/pack_command.rb +241 -0
- data/lib/fontisan/commands/subset_command.rb +245 -0
- data/lib/fontisan/commands/unpack_command.rb +338 -0
- data/lib/fontisan/commands/validate_command.rb +178 -0
- data/lib/fontisan/commands/variable_command.rb +30 -1
- data/lib/fontisan/config/collection_settings.yml +56 -0
- data/lib/fontisan/config/conversion_matrix.yml +212 -0
- data/lib/fontisan/config/export_settings.yml +66 -0
- data/lib/fontisan/config/subset_profiles.yml +100 -0
- data/lib/fontisan/config/svg_settings.yml +60 -0
- data/lib/fontisan/config/validation_rules.yml +149 -0
- data/lib/fontisan/config/variable_settings.yml +99 -0
- data/lib/fontisan/config/woff2_settings.yml +77 -0
- data/lib/fontisan/constants.rb +69 -0
- data/lib/fontisan/converters/conversion_strategy.rb +96 -0
- data/lib/fontisan/converters/format_converter.rb +259 -0
- data/lib/fontisan/converters/outline_converter.rb +936 -0
- data/lib/fontisan/converters/svg_generator.rb +244 -0
- data/lib/fontisan/converters/table_copier.rb +117 -0
- data/lib/fontisan/converters/woff2_encoder.rb +416 -0
- data/lib/fontisan/converters/woff_writer.rb +391 -0
- data/lib/fontisan/error.rb +203 -0
- data/lib/fontisan/export/exporter.rb +262 -0
- data/lib/fontisan/export/table_serializer.rb +255 -0
- data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
- data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
- data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
- data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
- data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
- data/lib/fontisan/export/ttx_generator.rb +527 -0
- data/lib/fontisan/export/ttx_parser.rb +300 -0
- data/lib/fontisan/font_loader.rb +121 -12
- data/lib/fontisan/font_writer.rb +301 -0
- data/lib/fontisan/formatters/text_formatter.rb +102 -0
- data/lib/fontisan/glyph_accessor.rb +503 -0
- data/lib/fontisan/hints/hint_converter.rb +177 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
- data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
- data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
- data/lib/fontisan/loading_modes.rb +113 -0
- data/lib/fontisan/metrics_calculator.rb +277 -0
- data/lib/fontisan/models/collection_font_summary.rb +52 -0
- data/lib/fontisan/models/collection_info.rb +76 -0
- data/lib/fontisan/models/collection_list_info.rb +37 -0
- data/lib/fontisan/models/font_export.rb +158 -0
- data/lib/fontisan/models/font_summary.rb +48 -0
- data/lib/fontisan/models/glyph_outline.rb +343 -0
- data/lib/fontisan/models/hint.rb +233 -0
- data/lib/fontisan/models/outline.rb +664 -0
- data/lib/fontisan/models/table_sharing_info.rb +40 -0
- data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
- data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
- data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
- data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
- data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
- data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
- data/lib/fontisan/models/ttx/ttfont.rb +49 -0
- data/lib/fontisan/models/validation_report.rb +203 -0
- data/lib/fontisan/open_type_collection.rb +156 -2
- data/lib/fontisan/open_type_font.rb +296 -10
- data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
- data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
- data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
- data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
- data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
- data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
- data/lib/fontisan/outline_extractor.rb +423 -0
- data/lib/fontisan/subset/builder.rb +268 -0
- data/lib/fontisan/subset/glyph_mapping.rb +215 -0
- data/lib/fontisan/subset/options.rb +142 -0
- data/lib/fontisan/subset/profile.rb +152 -0
- data/lib/fontisan/subset/table_subsetter.rb +461 -0
- data/lib/fontisan/svg/font_face_generator.rb +278 -0
- data/lib/fontisan/svg/font_generator.rb +264 -0
- data/lib/fontisan/svg/glyph_generator.rb +168 -0
- data/lib/fontisan/svg/view_box_calculator.rb +137 -0
- data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
- data/lib/fontisan/tables/cff/charset.rb +282 -0
- data/lib/fontisan/tables/cff/charstring.rb +905 -0
- data/lib/fontisan/tables/cff/charstring_builder.rb +322 -0
- data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
- data/lib/fontisan/tables/cff/dict.rb +351 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +242 -0
- data/lib/fontisan/tables/cff/encoding.rb +274 -0
- data/lib/fontisan/tables/cff/header.rb +102 -0
- data/lib/fontisan/tables/cff/index.rb +237 -0
- data/lib/fontisan/tables/cff/index_builder.rb +170 -0
- data/lib/fontisan/tables/cff/private_dict.rb +284 -0
- data/lib/fontisan/tables/cff/top_dict.rb +236 -0
- data/lib/fontisan/tables/cff.rb +487 -0
- data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
- data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
- data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
- data/lib/fontisan/tables/cff2.rb +341 -0
- data/lib/fontisan/tables/cvar.rb +242 -0
- data/lib/fontisan/tables/fvar.rb +2 -2
- data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
- data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
- data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
- data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
- data/lib/fontisan/tables/glyf.rb +235 -0
- data/lib/fontisan/tables/gvar.rb +270 -0
- data/lib/fontisan/tables/hhea.rb +124 -0
- data/lib/fontisan/tables/hmtx.rb +287 -0
- data/lib/fontisan/tables/hvar.rb +191 -0
- data/lib/fontisan/tables/loca.rb +322 -0
- data/lib/fontisan/tables/maxp.rb +192 -0
- data/lib/fontisan/tables/mvar.rb +185 -0
- data/lib/fontisan/tables/name.rb +99 -30
- data/lib/fontisan/tables/variation_common.rb +346 -0
- data/lib/fontisan/tables/vvar.rb +234 -0
- data/lib/fontisan/true_type_collection.rb +156 -2
- data/lib/fontisan/true_type_font.rb +297 -11
- data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +18 -0
- data/lib/fontisan/utils/thread_pool.rb +134 -0
- data/lib/fontisan/validation/checksum_validator.rb +170 -0
- data/lib/fontisan/validation/consistency_validator.rb +197 -0
- data/lib/fontisan/validation/structure_validator.rb +198 -0
- data/lib/fontisan/validation/table_validator.rb +158 -0
- data/lib/fontisan/validation/validator.rb +152 -0
- data/lib/fontisan/variable/axis_normalizer.rb +215 -0
- data/lib/fontisan/variable/delta_applicator.rb +313 -0
- data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
- data/lib/fontisan/variable/instancer.rb +344 -0
- data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
- data/lib/fontisan/variable/region_matcher.rb +208 -0
- data/lib/fontisan/variable/static_font_builder.rb +213 -0
- data/lib/fontisan/variable/table_updater.rb +219 -0
- data/lib/fontisan/variation/blend_applier.rb +199 -0
- data/lib/fontisan/variation/cache.rb +298 -0
- data/lib/fontisan/variation/cache_key_builder.rb +162 -0
- data/lib/fontisan/variation/converter.rb +268 -0
- data/lib/fontisan/variation/data_extractor.rb +86 -0
- data/lib/fontisan/variation/delta_applier.rb +266 -0
- data/lib/fontisan/variation/delta_parser.rb +228 -0
- data/lib/fontisan/variation/inspector.rb +275 -0
- data/lib/fontisan/variation/instance_generator.rb +273 -0
- data/lib/fontisan/variation/interpolator.rb +231 -0
- data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
- data/lib/fontisan/variation/optimizer.rb +418 -0
- data/lib/fontisan/variation/parallel_generator.rb +150 -0
- data/lib/fontisan/variation/region_matcher.rb +221 -0
- data/lib/fontisan/variation/subsetter.rb +463 -0
- data/lib/fontisan/variation/table_accessor.rb +105 -0
- data/lib/fontisan/variation/validator.rb +345 -0
- data/lib/fontisan/variation/variation_context.rb +211 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/directory.rb +257 -0
- data/lib/fontisan/woff2/header.rb +101 -0
- data/lib/fontisan/woff2/table_transformer.rb +163 -0
- data/lib/fontisan/woff2_font.rb +712 -0
- data/lib/fontisan/woff_font.rb +483 -0
- data/lib/fontisan.rb +120 -0
- data/scripts/compare_stack_aware.rb +187 -0
- data/scripts/measure_optimization.rb +141 -0
- metadata +205 -4
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "interpolator"
|
|
4
|
+
require_relative "region_matcher"
|
|
5
|
+
require_relative "metrics_adjuster"
|
|
6
|
+
require_relative "variation_context"
|
|
7
|
+
require_relative "table_accessor"
|
|
8
|
+
|
|
9
|
+
module Fontisan
|
|
10
|
+
module Variation
|
|
11
|
+
# Generates static font instances from variable fonts
|
|
12
|
+
#
|
|
13
|
+
# This class creates static font instances by applying variation deltas
|
|
14
|
+
# at specific design space coordinates. It supports both TrueType (gvar)
|
|
15
|
+
# and PostScript (CFF2) variable fonts.
|
|
16
|
+
#
|
|
17
|
+
# Process:
|
|
18
|
+
# 1. Extract variation data (axes, deltas, regions)
|
|
19
|
+
# 2. Calculate interpolation scalars for given coordinates
|
|
20
|
+
# 3. Apply deltas to outlines (gvar or CFF2)
|
|
21
|
+
# 4. Apply deltas to metrics (HVAR, VVAR, MVAR)
|
|
22
|
+
# 5. Remove variation tables to create static font
|
|
23
|
+
#
|
|
24
|
+
# @example Generating an instance at specific coordinates
|
|
25
|
+
# generator = Fontisan::Variation::InstanceGenerator.new(font, { "wght" => 700.0 })
|
|
26
|
+
# instance_tables = generator.generate
|
|
27
|
+
#
|
|
28
|
+
# @example Generating a named instance
|
|
29
|
+
# generator = Fontisan::Variation::InstanceGenerator.new(font)
|
|
30
|
+
# instance_tables = generator.generate_named_instance(0)
|
|
31
|
+
class InstanceGenerator
|
|
32
|
+
include TableAccessor
|
|
33
|
+
|
|
34
|
+
# @return [TrueTypeFont, OpenTypeFont] Variable font
|
|
35
|
+
attr_reader :font
|
|
36
|
+
|
|
37
|
+
# @return [Hash<String, Float>] Design space coordinates
|
|
38
|
+
attr_reader :coordinates
|
|
39
|
+
|
|
40
|
+
# @return [VariationContext] Variation context
|
|
41
|
+
attr_reader :context
|
|
42
|
+
|
|
43
|
+
# Initialize generator with font and optional coordinates
|
|
44
|
+
#
|
|
45
|
+
# @param font [TrueTypeFont, OpenTypeFont] Variable font
|
|
46
|
+
# @param coordinates [Hash<String, Float>] Design space coordinates (axis tag => value)
|
|
47
|
+
# @param options [Hash] Options
|
|
48
|
+
# @option options [Boolean] :skip_validation Skip context validation (default: false)
|
|
49
|
+
def initialize(font, coordinates = {}, options = {})
|
|
50
|
+
@font = font
|
|
51
|
+
@coordinates = coordinates
|
|
52
|
+
|
|
53
|
+
# Initialize variation context
|
|
54
|
+
@context = VariationContext.new(@font)
|
|
55
|
+
@context.validate! unless options[:skip_validation]
|
|
56
|
+
|
|
57
|
+
# Initialize table cache for lazy loading
|
|
58
|
+
@variation_tables = {}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Generate static font instance
|
|
62
|
+
#
|
|
63
|
+
# Applies variation deltas and returns static font tables.
|
|
64
|
+
#
|
|
65
|
+
# @return [Hash<String, String>] Map of table tags to binary data
|
|
66
|
+
def generate
|
|
67
|
+
# Start with base font tables
|
|
68
|
+
tables = @font.table_data.dup
|
|
69
|
+
|
|
70
|
+
# Determine variation type
|
|
71
|
+
if has_variation_table?("gvar")
|
|
72
|
+
# TrueType outlines with gvar
|
|
73
|
+
apply_gvar_deltas(tables)
|
|
74
|
+
elsif has_variation_table?("CFF2")
|
|
75
|
+
# PostScript outlines with CFF2 blend
|
|
76
|
+
apply_cff2_blend(tables)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Apply metrics variations if present
|
|
80
|
+
apply_metrics_deltas(tables) if @context.has_metrics_variations?
|
|
81
|
+
|
|
82
|
+
# Remove variation-specific tables to create static font
|
|
83
|
+
remove_variation_tables(tables)
|
|
84
|
+
|
|
85
|
+
tables
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Generate a named instance
|
|
89
|
+
#
|
|
90
|
+
# @param instance_index [Integer] Index of named instance in fvar table
|
|
91
|
+
# @return [Hash<String, String>] Map of table tags to binary data
|
|
92
|
+
def generate_named_instance(instance_index)
|
|
93
|
+
# Extract instance coordinates from fvar
|
|
94
|
+
return generate if instance_index.nil? || !@context.fvar
|
|
95
|
+
|
|
96
|
+
instances = @context.fvar.instances
|
|
97
|
+
return generate if instance_index >= instances.length
|
|
98
|
+
|
|
99
|
+
instance = instances[instance_index]
|
|
100
|
+
@coordinates = build_coordinates_from_instance(instance, @context.axes)
|
|
101
|
+
|
|
102
|
+
generate
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Apply gvar deltas to TrueType outlines
|
|
106
|
+
#
|
|
107
|
+
# @param tables [Hash<String, String>] Font tables
|
|
108
|
+
def apply_gvar_deltas(_tables)
|
|
109
|
+
gvar = variation_table("gvar")
|
|
110
|
+
glyf = @font.table("glyf")
|
|
111
|
+
return unless gvar && glyf
|
|
112
|
+
|
|
113
|
+
# Get glyph count
|
|
114
|
+
maxp = @font.table("maxp")
|
|
115
|
+
glyph_count = maxp ? maxp.num_glyphs : gvar.glyph_count
|
|
116
|
+
|
|
117
|
+
# Process each glyph
|
|
118
|
+
glyph_count.times do |glyph_id|
|
|
119
|
+
apply_glyph_deltas(glyph_id, gvar, glyf)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Rebuild glyf and loca tables with adjusted outlines
|
|
123
|
+
# This is a placeholder - full implementation would reconstruct tables
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Apply deltas to a specific glyph
|
|
127
|
+
#
|
|
128
|
+
# @param glyph_id [Integer] Glyph ID
|
|
129
|
+
# @param gvar [Gvar] Gvar table
|
|
130
|
+
# @param glyf [Glyf] Glyf table
|
|
131
|
+
def apply_glyph_deltas(glyph_id, gvar, _glyf)
|
|
132
|
+
# Get tuple variations for this glyph
|
|
133
|
+
tuple_data = gvar.glyph_tuple_variations(glyph_id)
|
|
134
|
+
return unless tuple_data
|
|
135
|
+
|
|
136
|
+
# Match tuples to current coordinates
|
|
137
|
+
matches = @context.region_matcher.match_tuples(
|
|
138
|
+
coordinates: @coordinates,
|
|
139
|
+
tuples: tuple_data[:tuples],
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
nil if matches.empty?
|
|
143
|
+
|
|
144
|
+
# Get base glyph outline
|
|
145
|
+
# Apply matched deltas with their scalars
|
|
146
|
+
# This is a placeholder - full implementation would:
|
|
147
|
+
# 1. Parse glyph outline points
|
|
148
|
+
# 2. Parse delta data for each tuple
|
|
149
|
+
# 3. Apply: new_point = base_point + Σ(delta * scalar)
|
|
150
|
+
# 4. Update glyph outline
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Apply CFF2 blend operators
|
|
154
|
+
#
|
|
155
|
+
# @param tables [Hash<String, String>] Font tables
|
|
156
|
+
def apply_cff2_blend(_tables)
|
|
157
|
+
cff2 = variation_table("CFF2")
|
|
158
|
+
return unless cff2
|
|
159
|
+
|
|
160
|
+
# Set number of axes for CFF2
|
|
161
|
+
cff2.num_axes = @context.axis_count
|
|
162
|
+
|
|
163
|
+
# Process each glyph's CharString
|
|
164
|
+
glyph_count = cff2.glyph_count
|
|
165
|
+
return if glyph_count.zero?
|
|
166
|
+
|
|
167
|
+
# Calculate variation scalars once
|
|
168
|
+
calculate_variation_scalars
|
|
169
|
+
|
|
170
|
+
# Apply blend to each glyph
|
|
171
|
+
# This is a placeholder - full implementation would:
|
|
172
|
+
# 1. Parse CharString with blend operators
|
|
173
|
+
# 2. Apply scalars to blend operands
|
|
174
|
+
# 3. Rebuild CharStrings without blend operators
|
|
175
|
+
# 4. Update CFF2 table
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Calculate variation scalars for current coordinates
|
|
179
|
+
#
|
|
180
|
+
# @return [Array<Float>] Scalars for each axis
|
|
181
|
+
def calculate_variation_scalars
|
|
182
|
+
@context.axes.map do |axis|
|
|
183
|
+
coord = @coordinates[axis.axis_tag] || axis.default_value
|
|
184
|
+
@context.interpolator.normalize_coordinate(coord, axis.axis_tag)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Apply metrics variations
|
|
189
|
+
#
|
|
190
|
+
# @param tables [Hash<String, String>] Font tables
|
|
191
|
+
def apply_metrics_deltas(tables)
|
|
192
|
+
# Apply HVAR (horizontal metrics)
|
|
193
|
+
apply_hvar_deltas(tables) if has_variation_table?("HVAR")
|
|
194
|
+
|
|
195
|
+
# Apply VVAR (vertical metrics)
|
|
196
|
+
apply_vvar_deltas(tables) if has_variation_table?("VVAR")
|
|
197
|
+
|
|
198
|
+
# Apply MVAR (font-wide metrics)
|
|
199
|
+
apply_mvar_deltas(tables) if has_variation_table?("MVAR")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Apply HVAR deltas to horizontal metrics
|
|
203
|
+
#
|
|
204
|
+
# @param tables [Hash<String, String>] Font tables
|
|
205
|
+
def apply_hvar_deltas(_tables)
|
|
206
|
+
adjuster = MetricsAdjuster.new(@font, @context.interpolator)
|
|
207
|
+
adjuster.apply_hvar_deltas(@coordinates)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Apply VVAR deltas to vertical metrics
|
|
211
|
+
#
|
|
212
|
+
# @param tables [Hash<String, String>] Font tables
|
|
213
|
+
def apply_vvar_deltas(_tables)
|
|
214
|
+
adjuster = MetricsAdjuster.new(@font, @context.interpolator)
|
|
215
|
+
adjuster.apply_vvar_deltas(@coordinates)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Apply MVAR deltas to font-wide metrics
|
|
219
|
+
#
|
|
220
|
+
# @param tables [Hash<String, String>] Font tables
|
|
221
|
+
def apply_mvar_deltas(_tables)
|
|
222
|
+
adjuster = MetricsAdjuster.new(@font, @context.interpolator)
|
|
223
|
+
adjuster.apply_mvar_deltas(@coordinates)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Remove variation tables from static font
|
|
227
|
+
#
|
|
228
|
+
# @param tables [Hash<String, String>] Font tables
|
|
229
|
+
def remove_variation_tables(tables)
|
|
230
|
+
variation_tables = %w[fvar gvar cvar HVAR VVAR MVAR avar STAT]
|
|
231
|
+
variation_tables.each { |tag| tables.delete(tag) }
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Interpolate a single value
|
|
235
|
+
#
|
|
236
|
+
# @param base_value [Numeric] Base value
|
|
237
|
+
# @param deltas [Array<Numeric>] Delta values
|
|
238
|
+
# @param scalars [Array<Float>] Region scalars
|
|
239
|
+
# @return [Float] Interpolated value
|
|
240
|
+
def interpolate_value(base_value, deltas, scalars)
|
|
241
|
+
@context.interpolator.interpolate_value(base_value, deltas, scalars)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Interpolate a point
|
|
245
|
+
#
|
|
246
|
+
# @param base_point [Hash] Base point with :x and :y
|
|
247
|
+
# @param delta_points [Array<Hash>] Delta points
|
|
248
|
+
# @param scalars [Array<Float>] Region scalars
|
|
249
|
+
# @return [Hash] Interpolated point
|
|
250
|
+
def interpolate_point(base_point, delta_points, scalars)
|
|
251
|
+
@context.interpolator.interpolate_point(base_point, delta_points, scalars)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
private
|
|
255
|
+
|
|
256
|
+
# Build coordinates hash from instance
|
|
257
|
+
#
|
|
258
|
+
# @param instance [Hash] Instance data from fvar
|
|
259
|
+
# @param axes [Array<VariationAxisRecord>] Variation axes
|
|
260
|
+
# @return [Hash<String, Float>] Coordinates hash
|
|
261
|
+
def build_coordinates_from_instance(instance, axes)
|
|
262
|
+
coordinates = {}
|
|
263
|
+
instance[:coordinates].each_with_index do |value, index|
|
|
264
|
+
next if index >= axes.length
|
|
265
|
+
|
|
266
|
+
axis = axes[index]
|
|
267
|
+
coordinates[axis.axis_tag] = value
|
|
268
|
+
end
|
|
269
|
+
coordinates
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Variation
|
|
5
|
+
# Coordinate interpolator for variable fonts
|
|
6
|
+
#
|
|
7
|
+
# This class interpolates values in the variation design space by
|
|
8
|
+
# calculating scalars based on the current coordinates and variation
|
|
9
|
+
# regions/tuples.
|
|
10
|
+
#
|
|
11
|
+
# Interpolation Process:
|
|
12
|
+
# 1. Normalize user coordinates to [-1, 1] range based on axis min/default/max
|
|
13
|
+
# 2. For each variation region, calculate a scalar that represents how much
|
|
14
|
+
# that region contributes at the current coordinates
|
|
15
|
+
# 3. Apply the scalars to deltas to get the final interpolated value
|
|
16
|
+
#
|
|
17
|
+
# Region Scalar Calculation:
|
|
18
|
+
# For each axis, given a region [start, peak, end] and coordinate c:
|
|
19
|
+
# - If c < start or c > end: scalar = 0 (outside region)
|
|
20
|
+
# - If c in [start, peak]: scalar = (c - start) / (peak - start)
|
|
21
|
+
# - If c in [peak, end]: scalar = (end - c) / (end - peak)
|
|
22
|
+
# - If c == peak: scalar = 1 (at peak)
|
|
23
|
+
#
|
|
24
|
+
# For multi-axis regions, multiply the per-axis scalars together.
|
|
25
|
+
#
|
|
26
|
+
# Reference: OpenType Font Variations specification
|
|
27
|
+
#
|
|
28
|
+
# @example Interpolating a coordinate
|
|
29
|
+
# interpolator = Interpolator.new(axes)
|
|
30
|
+
# scalar = interpolator.calculate_scalar(
|
|
31
|
+
# coordinates: { "wght" => 600.0 },
|
|
32
|
+
# region: { "wght" => { start: 400, peak: 700, end: 900 } }
|
|
33
|
+
# )
|
|
34
|
+
# # => 0.666... (normalized position between 400 and 700)
|
|
35
|
+
class Interpolator
|
|
36
|
+
# @return [Array<VariationAxisRecord>] Variation axes
|
|
37
|
+
attr_reader :axes
|
|
38
|
+
|
|
39
|
+
# Initialize interpolator
|
|
40
|
+
#
|
|
41
|
+
# @param axes [Array<VariationAxisRecord>] Variation axes from fvar table
|
|
42
|
+
def initialize(axes)
|
|
43
|
+
@axes = axes || []
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Normalize a coordinate value to [-1, 1] range
|
|
47
|
+
#
|
|
48
|
+
# @param value [Float] User-space coordinate value
|
|
49
|
+
# @param axis_tag [String] Axis tag (e.g., "wght", "wdth")
|
|
50
|
+
# @return [Float] Normalized coordinate in [-1, 1]
|
|
51
|
+
def normalize_coordinate(value, axis_tag)
|
|
52
|
+
axis = find_axis(axis_tag)
|
|
53
|
+
return 0.0 unless axis
|
|
54
|
+
|
|
55
|
+
# Clamp to axis range
|
|
56
|
+
value = [[value, axis.min_value].max, axis.max_value].min
|
|
57
|
+
|
|
58
|
+
# Normalize to [-1, 1]
|
|
59
|
+
if value < axis.default_value
|
|
60
|
+
# Normalize between min and default (maps to -1..0)
|
|
61
|
+
range = axis.default_value - axis.min_value
|
|
62
|
+
return -1.0 if range.zero?
|
|
63
|
+
|
|
64
|
+
(value - axis.default_value) / range
|
|
65
|
+
elsif value > axis.default_value
|
|
66
|
+
# Normalize between default and max (maps to 0..1)
|
|
67
|
+
range = axis.max_value - axis.default_value
|
|
68
|
+
return 1.0 if range.zero?
|
|
69
|
+
|
|
70
|
+
(value - axis.default_value) / range
|
|
71
|
+
else
|
|
72
|
+
# At default value
|
|
73
|
+
0.0
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Normalize all coordinates
|
|
78
|
+
#
|
|
79
|
+
# @param coordinates [Hash<String, Float>] User-space coordinates
|
|
80
|
+
# @return [Hash<String, Float>] Normalized coordinates
|
|
81
|
+
def normalize_coordinates(coordinates)
|
|
82
|
+
result = {}
|
|
83
|
+
@axes.each do |axis|
|
|
84
|
+
tag = axis.axis_tag
|
|
85
|
+
value = coordinates[tag] || axis.default_value
|
|
86
|
+
result[tag] = normalize_coordinate(value, tag)
|
|
87
|
+
end
|
|
88
|
+
result
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Calculate scalar for a single axis region
|
|
92
|
+
#
|
|
93
|
+
# @param coord [Float] Normalized coordinate value [-1, 1]
|
|
94
|
+
# @param region [Hash] Region definition with :start, :peak, :end
|
|
95
|
+
# @return [Float] Scalar value [0, 1]
|
|
96
|
+
def calculate_axis_scalar(coord, region)
|
|
97
|
+
start_val = region[:start] || -1.0
|
|
98
|
+
peak = region[:peak] || 0.0
|
|
99
|
+
end_val = region[:end] || 1.0
|
|
100
|
+
|
|
101
|
+
# Outside region
|
|
102
|
+
return 0.0 if coord < start_val || coord > end_val
|
|
103
|
+
|
|
104
|
+
# At or beyond peak
|
|
105
|
+
return 1.0 if coord == peak
|
|
106
|
+
|
|
107
|
+
# Between start and peak
|
|
108
|
+
if coord < peak
|
|
109
|
+
range = peak - start_val
|
|
110
|
+
return 1.0 if range.zero?
|
|
111
|
+
|
|
112
|
+
(coord - start_val) / range
|
|
113
|
+
else
|
|
114
|
+
# Between peak and end
|
|
115
|
+
range = end_val - peak
|
|
116
|
+
return 1.0 if range.zero?
|
|
117
|
+
|
|
118
|
+
(end_val - coord) / range
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Calculate scalar for a multi-axis region
|
|
123
|
+
#
|
|
124
|
+
# For multi-axis regions, the final scalar is the product of per-axis scalars.
|
|
125
|
+
#
|
|
126
|
+
# @param coordinates [Hash<String, Float>] Normalized coordinates
|
|
127
|
+
# @param region [Hash<String, Hash>] Region definition per axis
|
|
128
|
+
# @return [Float] Combined scalar [0, 1]
|
|
129
|
+
def calculate_region_scalar(coordinates, region)
|
|
130
|
+
scalar = 1.0
|
|
131
|
+
|
|
132
|
+
region.each do |axis_tag, axis_region|
|
|
133
|
+
coord = coordinates[axis_tag] || 0.0
|
|
134
|
+
axis_scalar = calculate_axis_scalar(coord, axis_region)
|
|
135
|
+
|
|
136
|
+
# If any axis has zero scalar, entire region has zero contribution
|
|
137
|
+
return 0.0 if axis_scalar.zero?
|
|
138
|
+
|
|
139
|
+
scalar *= axis_scalar
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
scalar
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Calculate scalars for all regions
|
|
146
|
+
#
|
|
147
|
+
# @param coordinates [Hash<String, Float>] User-space coordinates
|
|
148
|
+
# @param regions [Array<Hash>] Array of region definitions
|
|
149
|
+
# @return [Array<Float>] Scalars for each region
|
|
150
|
+
def calculate_scalars(coordinates, regions)
|
|
151
|
+
# Normalize coordinates first
|
|
152
|
+
normalized = normalize_coordinates(coordinates)
|
|
153
|
+
|
|
154
|
+
# Calculate scalar for each region
|
|
155
|
+
regions.map do |region|
|
|
156
|
+
calculate_region_scalar(normalized, region)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Interpolate a value using deltas
|
|
161
|
+
#
|
|
162
|
+
# @param base_value [Numeric] Base value
|
|
163
|
+
# @param deltas [Array<Numeric>] Delta values (one per region)
|
|
164
|
+
# @param scalars [Array<Float>] Region scalars (one per region)
|
|
165
|
+
# @return [Float] Interpolated value
|
|
166
|
+
def interpolate_value(base_value, deltas, scalars)
|
|
167
|
+
result = base_value.to_f
|
|
168
|
+
|
|
169
|
+
deltas.each_with_index do |delta, index|
|
|
170
|
+
scalar = scalars[index] || 0.0
|
|
171
|
+
result += delta.to_f * scalar
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
result
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Interpolate a point (x, y coordinates)
|
|
178
|
+
#
|
|
179
|
+
# @param base_point [Hash] Base point with :x and :y
|
|
180
|
+
# @param delta_points [Array<Hash>] Delta points (one per region)
|
|
181
|
+
# @param scalars [Array<Float>] Region scalars
|
|
182
|
+
# @return [Hash] Interpolated point with :x and :y
|
|
183
|
+
def interpolate_point(base_point, delta_points, scalars)
|
|
184
|
+
x = base_point[:x].to_f
|
|
185
|
+
y = base_point[:y].to_f
|
|
186
|
+
|
|
187
|
+
delta_points.each_with_index do |delta_point, index|
|
|
188
|
+
scalar = scalars[index] || 0.0
|
|
189
|
+
x += delta_point[:x].to_f * scalar
|
|
190
|
+
y += delta_point[:y].to_f * scalar
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
{ x: x, y: y }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Build region from tuple variation data
|
|
197
|
+
#
|
|
198
|
+
# Converts gvar tuple data to the region format used by interpolator
|
|
199
|
+
#
|
|
200
|
+
# @param tuple [Hash] Tuple variation data with :peak, :start, :end
|
|
201
|
+
# @return [Hash<String, Hash>] Region definition per axis
|
|
202
|
+
def build_region_from_tuple(tuple)
|
|
203
|
+
region = {}
|
|
204
|
+
|
|
205
|
+
@axes.each_with_index do |axis, axis_index|
|
|
206
|
+
peak = tuple[:peak] ? tuple[:peak][axis_index] : 0.0
|
|
207
|
+
start_val = tuple[:start] ? tuple[:start][axis_index] : -1.0
|
|
208
|
+
end_val = tuple[:end] ? tuple[:end][axis_index] : 1.0
|
|
209
|
+
|
|
210
|
+
region[axis.axis_tag] = {
|
|
211
|
+
start: start_val,
|
|
212
|
+
peak: peak,
|
|
213
|
+
end: end_val,
|
|
214
|
+
}
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
region
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
private
|
|
221
|
+
|
|
222
|
+
# Find axis by tag
|
|
223
|
+
#
|
|
224
|
+
# @param axis_tag [String] Axis tag
|
|
225
|
+
# @return [VariationAxisRecord, nil] Axis or nil
|
|
226
|
+
def find_axis(axis_tag)
|
|
227
|
+
@axes.find { |axis| axis.axis_tag == axis_tag }
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|