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,313 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require_relative "axis_normalizer"
|
|
5
|
+
require_relative "region_matcher"
|
|
6
|
+
require_relative "glyph_delta_processor"
|
|
7
|
+
require_relative "metric_delta_processor"
|
|
8
|
+
|
|
9
|
+
module Fontisan
|
|
10
|
+
module Variable
|
|
11
|
+
# Main orchestrator for delta application in variable fonts
|
|
12
|
+
#
|
|
13
|
+
# Coordinates the entire delta application pipeline:
|
|
14
|
+
# 1. Normalizes user coordinates to design space
|
|
15
|
+
# 2. Calculates region scalars based on normalized coordinates
|
|
16
|
+
# 3. Applies glyph outline deltas via GlyphDeltaProcessor
|
|
17
|
+
# 4. Applies metric deltas via MetricDeltaProcessor
|
|
18
|
+
#
|
|
19
|
+
# This is the primary interface for applying variation deltas to fonts.
|
|
20
|
+
#
|
|
21
|
+
# @example Apply deltas at specific coordinates
|
|
22
|
+
# applicator = DeltaApplicator.new(font)
|
|
23
|
+
# result = applicator.apply({ "wght" => 700, "wdth" => 100 })
|
|
24
|
+
# # => { normalized_coords: {...}, region_scalars: [...],
|
|
25
|
+
# # glyph_deltas: {...}, metric_deltas: {...} }
|
|
26
|
+
class DeltaApplicator
|
|
27
|
+
# @return [Hash] Configuration settings
|
|
28
|
+
attr_reader :config
|
|
29
|
+
|
|
30
|
+
# @return [AxisNormalizer] Axis normalizer
|
|
31
|
+
attr_reader :axis_normalizer
|
|
32
|
+
|
|
33
|
+
# @return [RegionMatcher] Region matcher
|
|
34
|
+
attr_reader :region_matcher
|
|
35
|
+
|
|
36
|
+
# @return [GlyphDeltaProcessor] Glyph delta processor
|
|
37
|
+
attr_reader :glyph_delta_processor
|
|
38
|
+
|
|
39
|
+
# @return [MetricDeltaProcessor] Metric delta processor
|
|
40
|
+
attr_reader :metric_delta_processor
|
|
41
|
+
|
|
42
|
+
# Initialize the delta applicator
|
|
43
|
+
#
|
|
44
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font object
|
|
45
|
+
# @param config [Hash] Optional configuration overrides
|
|
46
|
+
def initialize(font, config = {})
|
|
47
|
+
@font = font
|
|
48
|
+
@config = load_config.merge(config)
|
|
49
|
+
|
|
50
|
+
# Load variation tables
|
|
51
|
+
@fvar = load_table("fvar")
|
|
52
|
+
@gvar = load_table("gvar")
|
|
53
|
+
@hvar = load_table("HVAR")
|
|
54
|
+
@vvar = load_table("VVAR")
|
|
55
|
+
@mvar = load_table("MVAR")
|
|
56
|
+
|
|
57
|
+
# Initialize components
|
|
58
|
+
initialize_components
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Apply deltas at specified user coordinates
|
|
62
|
+
#
|
|
63
|
+
# @param user_coords [Hash<String, Numeric>] User coordinates
|
|
64
|
+
# @return [Hash] Delta application results
|
|
65
|
+
def apply(user_coords)
|
|
66
|
+
# Validate we have required tables
|
|
67
|
+
unless @fvar
|
|
68
|
+
raise ArgumentError,
|
|
69
|
+
"Font does not have fvar table (not a variable font)"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Step 1: Normalize coordinates
|
|
73
|
+
normalized_coords = @axis_normalizer.normalize(user_coords)
|
|
74
|
+
|
|
75
|
+
# Step 2: Calculate region scalars
|
|
76
|
+
region_scalars = @region_matcher.match(normalized_coords)
|
|
77
|
+
|
|
78
|
+
# Step 3: Prepare result structure
|
|
79
|
+
result = {
|
|
80
|
+
user_coords: user_coords,
|
|
81
|
+
normalized_coords: normalized_coords,
|
|
82
|
+
region_scalars: region_scalars,
|
|
83
|
+
glyph_deltas: {},
|
|
84
|
+
metric_deltas: {},
|
|
85
|
+
font_metrics: {},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Step 4: Apply font-level metrics if MVAR present
|
|
89
|
+
if @metric_delta_processor.has_mvar?
|
|
90
|
+
result[:font_metrics] =
|
|
91
|
+
@metric_delta_processor.apply_font_metrics(region_scalars)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
result
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Apply deltas to a specific glyph
|
|
98
|
+
#
|
|
99
|
+
# @param glyph_id [Integer] Glyph ID
|
|
100
|
+
# @param user_coords [Hash<String, Numeric>] User coordinates
|
|
101
|
+
# @return [Hash] Glyph delta result
|
|
102
|
+
def apply_glyph(glyph_id, user_coords)
|
|
103
|
+
# Normalize and get region scalars
|
|
104
|
+
normalized_coords = @axis_normalizer.normalize(user_coords)
|
|
105
|
+
region_scalars = @region_matcher.match(normalized_coords)
|
|
106
|
+
|
|
107
|
+
result = {
|
|
108
|
+
glyph_id: glyph_id,
|
|
109
|
+
normalized_coords: normalized_coords,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Apply glyph outline deltas if gvar present
|
|
113
|
+
if @glyph_delta_processor
|
|
114
|
+
result[:outline_deltas] = @glyph_delta_processor.apply_deltas(
|
|
115
|
+
glyph_id,
|
|
116
|
+
region_scalars,
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Apply metric deltas
|
|
121
|
+
result[:metric_deltas] = @metric_delta_processor.apply_deltas(
|
|
122
|
+
glyph_id,
|
|
123
|
+
region_scalars,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
result
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Apply deltas to multiple glyphs
|
|
130
|
+
#
|
|
131
|
+
# @param glyph_ids [Array<Integer>] Glyph IDs
|
|
132
|
+
# @param user_coords [Hash<String, Numeric>] User coordinates
|
|
133
|
+
# @return [Hash<Integer, Hash>] Results by glyph ID
|
|
134
|
+
def apply_glyphs(glyph_ids, user_coords)
|
|
135
|
+
# Normalize once for all glyphs
|
|
136
|
+
normalized_coords = @axis_normalizer.normalize(user_coords)
|
|
137
|
+
region_scalars = @region_matcher.match(normalized_coords)
|
|
138
|
+
|
|
139
|
+
glyph_ids.each_with_object({}) do |glyph_id, results|
|
|
140
|
+
results[glyph_id] = {
|
|
141
|
+
outline_deltas: @glyph_delta_processor&.apply_deltas(glyph_id,
|
|
142
|
+
region_scalars),
|
|
143
|
+
metric_deltas: @metric_delta_processor.apply_deltas(glyph_id,
|
|
144
|
+
region_scalars),
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Get advance width delta for a glyph
|
|
150
|
+
#
|
|
151
|
+
# @param glyph_id [Integer] Glyph ID
|
|
152
|
+
# @param user_coords [Hash<String, Numeric>] User coordinates
|
|
153
|
+
# @return [Integer] Advance width delta
|
|
154
|
+
def advance_width_delta(glyph_id, user_coords)
|
|
155
|
+
normalized_coords = @axis_normalizer.normalize(user_coords)
|
|
156
|
+
region_scalars = @region_matcher.match(normalized_coords)
|
|
157
|
+
|
|
158
|
+
@metric_delta_processor.advance_width_delta(glyph_id, region_scalars)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Check if font is a variable font
|
|
162
|
+
#
|
|
163
|
+
# @return [Boolean] True if variable font
|
|
164
|
+
def variable_font?
|
|
165
|
+
!@fvar.nil?
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Get axis information
|
|
169
|
+
#
|
|
170
|
+
# @return [Hash] Axis information from fvar
|
|
171
|
+
def axes
|
|
172
|
+
return {} unless @fvar
|
|
173
|
+
|
|
174
|
+
@fvar.axes.each_with_object({}) do |axis, hash|
|
|
175
|
+
# Convert BinData::String to regular Ruby String
|
|
176
|
+
tag = axis.axis_tag.to_s
|
|
177
|
+
hash[tag] = {
|
|
178
|
+
min: axis.min_value,
|
|
179
|
+
default: axis.default_value,
|
|
180
|
+
max: axis.max_value,
|
|
181
|
+
name_id: axis.axis_name_id,
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Get available axis tags
|
|
187
|
+
#
|
|
188
|
+
# @return [Array<String>] Axis tags
|
|
189
|
+
def axis_tags
|
|
190
|
+
@axis_normalizer.axis_tags
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Get number of variation regions
|
|
194
|
+
#
|
|
195
|
+
# @return [Integer] Region count
|
|
196
|
+
def region_count
|
|
197
|
+
@region_matcher.region_count
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private
|
|
201
|
+
|
|
202
|
+
# Load configuration from YAML file
|
|
203
|
+
#
|
|
204
|
+
# @return [Hash] Configuration hash
|
|
205
|
+
def load_config
|
|
206
|
+
config_path = File.join(__dir__, "..", "config",
|
|
207
|
+
"variable_settings.yml")
|
|
208
|
+
loaded = YAML.load_file(config_path)
|
|
209
|
+
# Convert string keys to symbol keys for consistency
|
|
210
|
+
deep_symbolize_keys(loaded)
|
|
211
|
+
rescue StandardError
|
|
212
|
+
# Return default config
|
|
213
|
+
{
|
|
214
|
+
validation: {
|
|
215
|
+
validate_tables: true,
|
|
216
|
+
check_required_tables: true,
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Recursively convert hash keys to symbols
|
|
222
|
+
#
|
|
223
|
+
# @param hash [Hash] Hash with string keys
|
|
224
|
+
# @return [Hash] Hash with symbol keys
|
|
225
|
+
def deep_symbolize_keys(hash)
|
|
226
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
227
|
+
new_key = key.to_sym
|
|
228
|
+
new_value = value.is_a?(Hash) ? deep_symbolize_keys(value) : value
|
|
229
|
+
result[new_key] = new_value
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Initialize all components
|
|
234
|
+
def initialize_components
|
|
235
|
+
# Initialize axis normalizer
|
|
236
|
+
@axis_normalizer = AxisNormalizer.new(@fvar, @config)
|
|
237
|
+
|
|
238
|
+
# Get axis tags for region matcher - convert BinData::String to String
|
|
239
|
+
axis_tags = @fvar ? @fvar.axes.map { |axis| axis.axis_tag.to_s } : []
|
|
240
|
+
|
|
241
|
+
# Initialize region matcher
|
|
242
|
+
# Get variation region list from one of the tables
|
|
243
|
+
variation_region_list = get_variation_region_list
|
|
244
|
+
@region_matcher = RegionMatcher.new(variation_region_list, axis_tags,
|
|
245
|
+
@config)
|
|
246
|
+
|
|
247
|
+
# Initialize glyph delta processor
|
|
248
|
+
@glyph_delta_processor = if @gvar
|
|
249
|
+
GlyphDeltaProcessor.new(@gvar,
|
|
250
|
+
@config)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Initialize metric delta processor
|
|
254
|
+
@metric_delta_processor = MetricDeltaProcessor.new(
|
|
255
|
+
hvar: @hvar,
|
|
256
|
+
vvar: @vvar,
|
|
257
|
+
mvar: @mvar,
|
|
258
|
+
config: @config,
|
|
259
|
+
)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Load a font table
|
|
263
|
+
#
|
|
264
|
+
# @param tag [String] Table tag
|
|
265
|
+
# @return [Object, nil] Table object or nil
|
|
266
|
+
def load_table(tag)
|
|
267
|
+
return nil unless @font.respond_to?(:table_data)
|
|
268
|
+
|
|
269
|
+
data = @font.table_data(tag)
|
|
270
|
+
return nil if data.nil? || data.empty?
|
|
271
|
+
|
|
272
|
+
# Map tag to table class
|
|
273
|
+
table_class = case tag
|
|
274
|
+
when "fvar" then Tables::Fvar
|
|
275
|
+
when "gvar" then Tables::Gvar
|
|
276
|
+
when "HVAR" then Tables::Hvar
|
|
277
|
+
when "VVAR" then Tables::Vvar
|
|
278
|
+
when "MVAR" then Tables::Mvar
|
|
279
|
+
else return nil
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
table_class.read(data)
|
|
283
|
+
rescue StandardError => e
|
|
284
|
+
warn "Failed to load #{tag} table: #{e.message}" if @config.dig(
|
|
285
|
+
:validation, :validate_tables
|
|
286
|
+
)
|
|
287
|
+
nil
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Get variation region list from available tables
|
|
291
|
+
#
|
|
292
|
+
# @return [VariationCommon::VariationRegionList, nil] Region list
|
|
293
|
+
def get_variation_region_list
|
|
294
|
+
# Try to get from HVAR first (most common)
|
|
295
|
+
if @hvar&.item_variation_store
|
|
296
|
+
return @hvar.item_variation_store.variation_region_list
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Try VVAR
|
|
300
|
+
if @vvar.respond_to?(:item_variation_store) && @vvar.item_variation_store
|
|
301
|
+
return @vvar.item_variation_store.variation_region_list
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Try MVAR
|
|
305
|
+
if @mvar&.item_variation_store
|
|
306
|
+
return @mvar.item_variation_store.variation_region_list
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
nil
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Variable
|
|
7
|
+
# Applies glyph outline deltas from gvar table
|
|
8
|
+
#
|
|
9
|
+
# Processes delta values for glyph control points and phantom points,
|
|
10
|
+
# applying them based on region scalars to modify glyph outlines.
|
|
11
|
+
#
|
|
12
|
+
# Handles both simple and compound glyphs, and processes phantom points
|
|
13
|
+
# which affect glyph metrics.
|
|
14
|
+
#
|
|
15
|
+
# @example Apply deltas to a glyph
|
|
16
|
+
# processor = GlyphDeltaProcessor.new(gvar_table, shared_tuples)
|
|
17
|
+
# modified = processor.apply_deltas(glyph_id, region_scalars)
|
|
18
|
+
# # => { x_deltas: [...], y_deltas: [...], phantom_deltas: [...] }
|
|
19
|
+
class GlyphDeltaProcessor
|
|
20
|
+
# @return [Hash] Configuration settings
|
|
21
|
+
attr_reader :config
|
|
22
|
+
|
|
23
|
+
# Initialize the processor
|
|
24
|
+
#
|
|
25
|
+
# @param gvar [Fontisan::Tables::Gvar] Glyph variations table
|
|
26
|
+
# @param config [Hash] Optional configuration overrides
|
|
27
|
+
def initialize(gvar, config = {})
|
|
28
|
+
@gvar = gvar
|
|
29
|
+
@config = load_config.merge(config)
|
|
30
|
+
@shared_tuples = gvar&.shared_tuples || []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Apply deltas to a glyph
|
|
34
|
+
#
|
|
35
|
+
# @param glyph_id [Integer] Glyph ID
|
|
36
|
+
# @param region_scalars [Array<Float>] Scalar for each region
|
|
37
|
+
# @return [Hash, nil] Delta information or nil
|
|
38
|
+
def apply_deltas(glyph_id, region_scalars)
|
|
39
|
+
return nil unless @gvar
|
|
40
|
+
|
|
41
|
+
# Get tuple variations for this glyph
|
|
42
|
+
tuple_info = @gvar.glyph_tuple_variations(glyph_id)
|
|
43
|
+
return nil unless tuple_info
|
|
44
|
+
|
|
45
|
+
# Calculate accumulated deltas
|
|
46
|
+
calculate_accumulated_deltas(tuple_info, region_scalars)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if glyph has variation data
|
|
50
|
+
#
|
|
51
|
+
# @param glyph_id [Integer] Glyph ID
|
|
52
|
+
# @return [Boolean] True if glyph has variations
|
|
53
|
+
def has_variations?(glyph_id)
|
|
54
|
+
return false unless @gvar
|
|
55
|
+
|
|
56
|
+
data = @gvar.glyph_variation_data(glyph_id)
|
|
57
|
+
!data.nil? && !data.empty?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get number of glyphs with variations
|
|
61
|
+
#
|
|
62
|
+
# @return [Integer] Glyph count
|
|
63
|
+
def glyph_count
|
|
64
|
+
@gvar&.glyph_count || 0
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Load configuration from YAML file
|
|
70
|
+
#
|
|
71
|
+
# @return [Hash] Configuration hash
|
|
72
|
+
def load_config
|
|
73
|
+
config_path = File.join(__dir__, "..", "config",
|
|
74
|
+
"variable_settings.yml")
|
|
75
|
+
loaded = YAML.load_file(config_path)
|
|
76
|
+
# Convert string keys to symbol keys for consistency
|
|
77
|
+
deep_symbolize_keys(loaded)
|
|
78
|
+
rescue StandardError
|
|
79
|
+
# Return default config
|
|
80
|
+
{
|
|
81
|
+
glyph_deltas: {
|
|
82
|
+
apply_to_simple: true,
|
|
83
|
+
apply_to_compound: true,
|
|
84
|
+
process_phantom_points: true,
|
|
85
|
+
phantom_point_count: 4,
|
|
86
|
+
},
|
|
87
|
+
delta_application: {
|
|
88
|
+
rounding_mode: "round",
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Recursively convert hash keys to symbols
|
|
94
|
+
#
|
|
95
|
+
# @param hash [Hash] Hash with string keys
|
|
96
|
+
# @return [Hash] Hash with symbol keys
|
|
97
|
+
def deep_symbolize_keys(hash)
|
|
98
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
99
|
+
new_key = key.to_sym
|
|
100
|
+
new_value = value.is_a?(Hash) ? deep_symbolize_keys(value) : value
|
|
101
|
+
result[new_key] = new_value
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Calculate accumulated deltas for a glyph
|
|
106
|
+
#
|
|
107
|
+
# @param tuple_info [Hash] Tuple variation information
|
|
108
|
+
# @param region_scalars [Array<Float>] Region scalars
|
|
109
|
+
# @return [Hash] Accumulated deltas
|
|
110
|
+
def calculate_accumulated_deltas(tuple_info, region_scalars)
|
|
111
|
+
tuples = tuple_info[:tuples]
|
|
112
|
+
return nil if tuples.nil? || tuples.empty?
|
|
113
|
+
|
|
114
|
+
# Result structure
|
|
115
|
+
result = {
|
|
116
|
+
x_deltas: [],
|
|
117
|
+
y_deltas: [],
|
|
118
|
+
phantom_deltas: [],
|
|
119
|
+
point_count: 0,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Process each tuple
|
|
123
|
+
tuples.each_with_index do |tuple, tuple_index|
|
|
124
|
+
# Get peak coordinates for this tuple
|
|
125
|
+
peak_coords = if tuple[:embedded_peak]
|
|
126
|
+
tuple[:peak]
|
|
127
|
+
else
|
|
128
|
+
@shared_tuples[tuple[:shared_index]]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
next unless peak_coords
|
|
132
|
+
|
|
133
|
+
# Calculate scalar for this tuple
|
|
134
|
+
scalar = calculate_tuple_scalar(tuple, peak_coords, region_scalars)
|
|
135
|
+
next if scalar.zero?
|
|
136
|
+
|
|
137
|
+
# This is a simplified version - actual implementation would need
|
|
138
|
+
# to unpack the delta data from the gvar table
|
|
139
|
+
# For now, we just indicate which tuples contribute
|
|
140
|
+
result[:contributing_tuples] ||= []
|
|
141
|
+
result[:contributing_tuples] << {
|
|
142
|
+
index: tuple_index,
|
|
143
|
+
scalar: scalar,
|
|
144
|
+
peak: peak_coords,
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
result
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Calculate scalar for a tuple
|
|
152
|
+
#
|
|
153
|
+
# @param tuple [Hash] Tuple information
|
|
154
|
+
# @param peak_coords [Array<Float>] Peak coordinates
|
|
155
|
+
# @param region_scalars [Array<Float>] Region scalars
|
|
156
|
+
# @return [Float] Tuple scalar
|
|
157
|
+
def calculate_tuple_scalar(tuple, peak_coords, region_scalars)
|
|
158
|
+
# For embedded tuples, calculate scalar based on peak/start/end
|
|
159
|
+
if tuple[:embedded_peak]
|
|
160
|
+
return calculate_embedded_tuple_scalar(tuple, peak_coords)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# For shared tuples, use the corresponding region scalar
|
|
164
|
+
shared_index = tuple[:shared_index]
|
|
165
|
+
return 0.0 if shared_index >= region_scalars.length
|
|
166
|
+
|
|
167
|
+
region_scalars[shared_index]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Calculate scalar for embedded tuple
|
|
171
|
+
#
|
|
172
|
+
# @param tuple [Hash] Tuple information
|
|
173
|
+
# @param peak_coords [Array<Float>] Peak coordinates
|
|
174
|
+
# @return [Float] Tuple scalar
|
|
175
|
+
def calculate_embedded_tuple_scalar(_tuple, peak_coords)
|
|
176
|
+
# Simplified - would need current normalized coordinates
|
|
177
|
+
# For now, return 1.0 if peak coords are present
|
|
178
|
+
peak_coords.any? { |c| c.abs > Float::EPSILON } ? 1.0 : 0.0
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Apply rounding to delta value
|
|
182
|
+
#
|
|
183
|
+
# @param delta [Float] Delta value
|
|
184
|
+
# @return [Integer] Rounded delta
|
|
185
|
+
def apply_rounding(delta)
|
|
186
|
+
mode = @config.dig(:delta_application, :rounding_mode) || "round"
|
|
187
|
+
|
|
188
|
+
case mode
|
|
189
|
+
when "round"
|
|
190
|
+
delta.round
|
|
191
|
+
when "floor"
|
|
192
|
+
delta.floor
|
|
193
|
+
when "ceil"
|
|
194
|
+
delta.ceil
|
|
195
|
+
when "truncate"
|
|
196
|
+
delta.to_i
|
|
197
|
+
else
|
|
198
|
+
delta.round
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Unpack point deltas from packed data
|
|
203
|
+
#
|
|
204
|
+
# This is a complex operation that requires understanding the
|
|
205
|
+
# gvar delta encoding format. Simplified placeholder.
|
|
206
|
+
#
|
|
207
|
+
# @param data [String] Packed delta data
|
|
208
|
+
# @param point_count [Integer] Number of points
|
|
209
|
+
# @return [Hash] X and Y deltas
|
|
210
|
+
def unpack_point_deltas(_data, point_count)
|
|
211
|
+
{
|
|
212
|
+
x_deltas: Array.new(point_count, 0),
|
|
213
|
+
y_deltas: Array.new(point_count, 0),
|
|
214
|
+
}
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|