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,344 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "delta_applicator"
|
|
4
|
+
require_relative "static_font_builder"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Variable
|
|
8
|
+
# Main entry point for variable font instancing
|
|
9
|
+
#
|
|
10
|
+
# This class orchestrates the complete process of generating a static
|
|
11
|
+
# font instance from a variable font at specified coordinates:
|
|
12
|
+
#
|
|
13
|
+
# 1. Validates the font is a variable font (has fvar table)
|
|
14
|
+
# 2. Normalizes user coordinates using AxisNormalizer
|
|
15
|
+
# 3. Calculates region scalars using RegionMatcher
|
|
16
|
+
# 4. Applies deltas using DeltaApplicator
|
|
17
|
+
# 5. Builds static font using StaticFontBuilder
|
|
18
|
+
#
|
|
19
|
+
# @example Generate instance at specific coordinates
|
|
20
|
+
# instancer = Instancer.new(variable_font)
|
|
21
|
+
# static_binary = instancer.instance({ "wght" => 700 })
|
|
22
|
+
# File.binwrite("bold.ttf", static_binary)
|
|
23
|
+
#
|
|
24
|
+
# @example Generate instance for named instance
|
|
25
|
+
# instancer = Instancer.new(variable_font)
|
|
26
|
+
# static_binary = instancer.instance_named("Bold")
|
|
27
|
+
class Instancer
|
|
28
|
+
# @return [Object] The variable font object
|
|
29
|
+
attr_reader :font
|
|
30
|
+
|
|
31
|
+
# @return [DeltaApplicator] Delta applicator
|
|
32
|
+
attr_reader :delta_applicator
|
|
33
|
+
|
|
34
|
+
# @return [StaticFontBuilder] Static font builder
|
|
35
|
+
attr_reader :static_font_builder
|
|
36
|
+
|
|
37
|
+
# Initialize the instancer
|
|
38
|
+
#
|
|
39
|
+
# @param font [TrueTypeFont, OpenTypeFont] Variable font object
|
|
40
|
+
# @raise [ArgumentError] If font is not a variable font
|
|
41
|
+
def initialize(font)
|
|
42
|
+
@font = font
|
|
43
|
+
validate_variable_font!
|
|
44
|
+
|
|
45
|
+
@delta_applicator = DeltaApplicator.new(font)
|
|
46
|
+
@static_font_builder = StaticFontBuilder.new(font)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Generate static font instance at specified coordinates
|
|
50
|
+
#
|
|
51
|
+
# @param user_coords [Hash<String, Numeric>] User coordinates
|
|
52
|
+
# { "wght" => 700, "wdth" => 100 }
|
|
53
|
+
# @param options [Hash] Instance options
|
|
54
|
+
# @option options [Boolean] :update_modified Update head modified timestamp
|
|
55
|
+
# @return [String] Complete static font binary
|
|
56
|
+
#
|
|
57
|
+
# @example
|
|
58
|
+
# binary = instancer.instance({ "wght" => 700, "wdth" => 100 })
|
|
59
|
+
def instance(user_coords, options = {})
|
|
60
|
+
# Apply deltas to get all varied data
|
|
61
|
+
delta_result = @delta_applicator.apply(user_coords)
|
|
62
|
+
|
|
63
|
+
# Collect varied metrics for all glyphs
|
|
64
|
+
varied_metrics = collect_varied_glyph_metrics(
|
|
65
|
+
delta_result[:normalized_coords],
|
|
66
|
+
delta_result[:region_scalars],
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Extract font-level metrics
|
|
70
|
+
font_metrics = delta_result[:font_metrics]
|
|
71
|
+
|
|
72
|
+
# Build static font
|
|
73
|
+
@static_font_builder.build(varied_metrics, font_metrics, options)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Generate static font instance and write to file
|
|
77
|
+
#
|
|
78
|
+
# @param output_path [String] Output file path
|
|
79
|
+
# @param user_coords [Hash<String, Numeric>] User coordinates
|
|
80
|
+
# @param options [Hash] Instance options
|
|
81
|
+
# @return [Integer] Number of bytes written
|
|
82
|
+
#
|
|
83
|
+
# @example
|
|
84
|
+
# instancer.instance_to_file("bold.ttf", { "wght" => 700 })
|
|
85
|
+
def instance_to_file(output_path, user_coords, options = {})
|
|
86
|
+
binary = instance(user_coords, options)
|
|
87
|
+
File.binwrite(output_path, binary)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Generate static font instance for a named instance
|
|
91
|
+
#
|
|
92
|
+
# @param instance_name [String] Named instance name (from fvar)
|
|
93
|
+
# @param options [Hash] Instance options
|
|
94
|
+
# @return [String] Complete static font binary
|
|
95
|
+
# @raise [ArgumentError] If named instance not found
|
|
96
|
+
#
|
|
97
|
+
# @example
|
|
98
|
+
# binary = instancer.instance_named("Bold")
|
|
99
|
+
def instance_named(instance_name, options = {})
|
|
100
|
+
coords = find_named_instance_coords(instance_name)
|
|
101
|
+
instance(coords, options)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Generate static font instance for a named instance and write to file
|
|
105
|
+
#
|
|
106
|
+
# @param output_path [String] Output file path
|
|
107
|
+
# @param instance_name [String] Named instance name
|
|
108
|
+
# @param options [Hash] Instance options
|
|
109
|
+
# @return [Integer] Number of bytes written
|
|
110
|
+
#
|
|
111
|
+
# @example
|
|
112
|
+
# instancer.instance_named_to_file("bold.ttf", "Bold")
|
|
113
|
+
def instance_named_to_file(output_path, instance_name, options = {})
|
|
114
|
+
binary = instance_named(instance_name, options)
|
|
115
|
+
File.binwrite(output_path, binary)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Get list of available named instances
|
|
119
|
+
#
|
|
120
|
+
# @return [Array<Hash>] Array of named instance information
|
|
121
|
+
# [{ name: "Bold", coords: { "wght" => 700 } }, ...]
|
|
122
|
+
def named_instances
|
|
123
|
+
fvar = load_fvar_table
|
|
124
|
+
return [] unless fvar
|
|
125
|
+
|
|
126
|
+
# Get name table for instance names
|
|
127
|
+
name_table = load_name_table
|
|
128
|
+
|
|
129
|
+
fvar.instances.map do |instance|
|
|
130
|
+
coords = extract_instance_coords(instance, fvar)
|
|
131
|
+
name = if name_table
|
|
132
|
+
get_instance_name(instance[:name_id],
|
|
133
|
+
name_table)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
{
|
|
137
|
+
name_id: instance[:name_id],
|
|
138
|
+
name: name || "Instance #{instance[:name_id]}",
|
|
139
|
+
coordinates: coords,
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Check if font is a variable font
|
|
145
|
+
#
|
|
146
|
+
# @return [Boolean] True if variable font
|
|
147
|
+
def variable_font?
|
|
148
|
+
@delta_applicator.variable_font?
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Get available axis information
|
|
152
|
+
#
|
|
153
|
+
# @return [Hash] Axis information
|
|
154
|
+
def axes
|
|
155
|
+
@delta_applicator.axes
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Get available axis tags
|
|
159
|
+
#
|
|
160
|
+
# @return [Array<String>] Array of axis tags
|
|
161
|
+
def axis_tags
|
|
162
|
+
@delta_applicator.axis_tags
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
# Validate that font is a variable font
|
|
168
|
+
#
|
|
169
|
+
# @raise [ArgumentError] If not a variable font
|
|
170
|
+
def validate_variable_font!
|
|
171
|
+
fvar_data = @font.table_data("fvar")
|
|
172
|
+
return unless fvar_data.nil? || fvar_data.empty?
|
|
173
|
+
|
|
174
|
+
raise ArgumentError, "Font is not a variable font (missing fvar table)"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Collect varied metrics for all glyphs
|
|
178
|
+
#
|
|
179
|
+
# @param normalized_coords [Hash] Normalized coordinates
|
|
180
|
+
# @param region_scalars [Array<Float>] Region scalars
|
|
181
|
+
# @return [Hash<Integer, Hash>] Varied metrics by glyph ID
|
|
182
|
+
def collect_varied_glyph_metrics(normalized_coords, _region_scalars)
|
|
183
|
+
varied_metrics = {}
|
|
184
|
+
|
|
185
|
+
# Get number of glyphs
|
|
186
|
+
maxp = load_maxp_table
|
|
187
|
+
return varied_metrics unless maxp
|
|
188
|
+
|
|
189
|
+
num_glyphs = maxp.num_glyphs
|
|
190
|
+
|
|
191
|
+
# Get original hmtx metrics
|
|
192
|
+
hmtx = load_hmtx_table
|
|
193
|
+
return varied_metrics unless hmtx
|
|
194
|
+
|
|
195
|
+
# For each glyph, calculate varied metrics
|
|
196
|
+
num_glyphs.times do |glyph_id|
|
|
197
|
+
original_metric = hmtx.metric_for(glyph_id)
|
|
198
|
+
next unless original_metric
|
|
199
|
+
|
|
200
|
+
# Get deltas from delta applicator
|
|
201
|
+
deltas = @delta_applicator.apply_glyph(glyph_id, normalized_coords)
|
|
202
|
+
metric_deltas = deltas[:metric_deltas]
|
|
203
|
+
|
|
204
|
+
# Calculate varied values
|
|
205
|
+
varied_metric = {
|
|
206
|
+
advance_width: original_metric[:advance_width],
|
|
207
|
+
lsb: original_metric[:lsb],
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# Apply horizontal deltas if present
|
|
211
|
+
if metric_deltas[:horizontal]
|
|
212
|
+
if metric_deltas[:horizontal][:advance_width]
|
|
213
|
+
varied_metric[:advance_width] += metric_deltas[:horizontal][:advance_width]
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
if metric_deltas[:horizontal][:lsb]
|
|
217
|
+
varied_metric[:lsb] += metric_deltas[:horizontal][:lsb]
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
varied_metrics[glyph_id] = varied_metric
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
varied_metrics
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Find coordinates for a named instance
|
|
228
|
+
#
|
|
229
|
+
# @param instance_name [String] Instance name
|
|
230
|
+
# @return [Hash<String, Float>] Coordinates
|
|
231
|
+
# @raise [ArgumentError] If instance not found
|
|
232
|
+
def find_named_instance_coords(instance_name)
|
|
233
|
+
instances = named_instances
|
|
234
|
+
instance = instances.find { |inst| inst[:name] == instance_name }
|
|
235
|
+
|
|
236
|
+
unless instance
|
|
237
|
+
raise ArgumentError,
|
|
238
|
+
"Named instance '#{instance_name}' not found"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
instance[:coordinates]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Extract coordinates from instance record
|
|
245
|
+
#
|
|
246
|
+
# @param instance [Hash] Instance record
|
|
247
|
+
# @param fvar [Fvar] fvar table
|
|
248
|
+
# @return [Hash<String, Float>] Coordinates by axis tag
|
|
249
|
+
def extract_instance_coords(instance, fvar)
|
|
250
|
+
coords = {}
|
|
251
|
+
instance[:coordinates].each_with_index do |value, index|
|
|
252
|
+
axis = fvar.axes[index]
|
|
253
|
+
coords[axis.axis_tag.to_s] = value if axis
|
|
254
|
+
end
|
|
255
|
+
coords
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Get instance name from name table
|
|
259
|
+
#
|
|
260
|
+
# @param name_id [Integer] Name table ID
|
|
261
|
+
# @param name_table [Name] name table
|
|
262
|
+
# @return [String, nil] Instance name
|
|
263
|
+
def get_instance_name(name_id, name_table)
|
|
264
|
+
# Try to get English name
|
|
265
|
+
record = name_table.records.find do |r|
|
|
266
|
+
r.name_id == name_id && r.language_id == 0x0409 # English (US)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
record ||= name_table.records.find { |r| r.name_id == name_id }
|
|
270
|
+
record&.value
|
|
271
|
+
rescue StandardError
|
|
272
|
+
nil
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Load fvar table
|
|
276
|
+
#
|
|
277
|
+
# @return [Fvar, nil] fvar table or nil
|
|
278
|
+
def load_fvar_table
|
|
279
|
+
data = @font.table_data("fvar")
|
|
280
|
+
return nil if data.nil? || data.empty?
|
|
281
|
+
|
|
282
|
+
Tables::Fvar.read(data)
|
|
283
|
+
rescue StandardError
|
|
284
|
+
nil
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Load name table
|
|
288
|
+
#
|
|
289
|
+
# @return [Name, nil] name table or nil
|
|
290
|
+
def load_name_table
|
|
291
|
+
data = @font.table_data("name")
|
|
292
|
+
return nil if data.nil? || data.empty?
|
|
293
|
+
|
|
294
|
+
Tables::Name.read(data)
|
|
295
|
+
rescue StandardError
|
|
296
|
+
nil
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Load maxp table
|
|
300
|
+
#
|
|
301
|
+
# @return [Maxp, nil] maxp table or nil
|
|
302
|
+
def load_maxp_table
|
|
303
|
+
data = @font.table_data("maxp")
|
|
304
|
+
return nil if data.nil? || data.empty?
|
|
305
|
+
|
|
306
|
+
Tables::Maxp.read(data)
|
|
307
|
+
rescue StandardError
|
|
308
|
+
nil
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Load hmtx table
|
|
312
|
+
#
|
|
313
|
+
# @return [Hmtx, nil] hmtx table or nil
|
|
314
|
+
def load_hmtx_table
|
|
315
|
+
data = @font.table_data("hmtx")
|
|
316
|
+
return nil if data.nil? || data.empty?
|
|
317
|
+
|
|
318
|
+
hmtx = Tables::Hmtx.read(data)
|
|
319
|
+
|
|
320
|
+
# Parse with context
|
|
321
|
+
hhea = load_hhea_table
|
|
322
|
+
maxp = load_maxp_table
|
|
323
|
+
return nil unless hhea && maxp
|
|
324
|
+
|
|
325
|
+
hmtx.parse_with_context(hhea.number_of_h_metrics, maxp.num_glyphs)
|
|
326
|
+
hmtx
|
|
327
|
+
rescue StandardError
|
|
328
|
+
nil
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Load hhea table
|
|
332
|
+
#
|
|
333
|
+
# @return [Hhea, nil] hhea table or nil
|
|
334
|
+
def load_hhea_table
|
|
335
|
+
data = @font.table_data("hhea")
|
|
336
|
+
return nil if data.nil? || data.empty?
|
|
337
|
+
|
|
338
|
+
Tables::Hhea.read(data)
|
|
339
|
+
rescue StandardError
|
|
340
|
+
nil
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Variable
|
|
7
|
+
# Applies metric deltas from HVAR, VVAR, and MVAR tables
|
|
8
|
+
#
|
|
9
|
+
# Processes variation data for font metrics including:
|
|
10
|
+
# - Horizontal metrics (advance widths, LSB, RSB) via HVAR
|
|
11
|
+
# - Vertical metrics (advance heights, TSB, BSB) via VVAR
|
|
12
|
+
# - Font-level metrics (ascent, descent, line gap, etc.) via MVAR
|
|
13
|
+
#
|
|
14
|
+
# Uses ItemVariationStore and region scalars to calculate accumulated
|
|
15
|
+
# deltas which are then applied to original metric values.
|
|
16
|
+
#
|
|
17
|
+
# @example Apply metric deltas
|
|
18
|
+
# processor = MetricDeltaProcessor.new(hvar, vvar, mvar)
|
|
19
|
+
# deltas = processor.apply_deltas(glyph_id, region_scalars)
|
|
20
|
+
# # => { advance_width: 10, lsb: -2, ... }
|
|
21
|
+
class MetricDeltaProcessor
|
|
22
|
+
# @return [Hash] Configuration settings
|
|
23
|
+
attr_reader :config
|
|
24
|
+
|
|
25
|
+
# Initialize the processor
|
|
26
|
+
#
|
|
27
|
+
# @param hvar [Fontisan::Tables::Hvar, nil] Horizontal variations table
|
|
28
|
+
# @param vvar [Fontisan::Tables::Vvar, nil] Vertical variations table
|
|
29
|
+
# @param mvar [Fontisan::Tables::Mvar, nil] Metrics variations table
|
|
30
|
+
# @param config [Hash] Optional configuration overrides
|
|
31
|
+
def initialize(hvar: nil, vvar: nil, mvar: nil, config: {})
|
|
32
|
+
@hvar = hvar
|
|
33
|
+
@vvar = vvar
|
|
34
|
+
@mvar = mvar
|
|
35
|
+
@config = load_config.merge(config)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Apply all metric deltas for a glyph
|
|
39
|
+
#
|
|
40
|
+
# @param glyph_id [Integer] Glyph ID
|
|
41
|
+
# @param region_scalars [Array<Float>] Scalar for each region
|
|
42
|
+
# @return [Hash] Metric deltas
|
|
43
|
+
def apply_deltas(glyph_id, region_scalars)
|
|
44
|
+
result = {}
|
|
45
|
+
|
|
46
|
+
# Apply horizontal metric deltas if HVAR present
|
|
47
|
+
if @hvar && @config.dig(:metric_deltas, :apply_hvar)
|
|
48
|
+
result[:horizontal] = apply_hvar_deltas(glyph_id, region_scalars)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Apply vertical metric deltas if VVAR present
|
|
52
|
+
if @vvar && @config.dig(:metric_deltas, :apply_vvar)
|
|
53
|
+
result[:vertical] = apply_vvar_deltas(glyph_id, region_scalars)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
result
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Apply font-level metric deltas
|
|
60
|
+
#
|
|
61
|
+
# @param region_scalars [Array<Float>] Scalar for each region
|
|
62
|
+
# @return [Hash] Font-level metric deltas
|
|
63
|
+
def apply_font_metrics(region_scalars)
|
|
64
|
+
return {} unless @mvar && @config.dig(:metric_deltas, :apply_mvar)
|
|
65
|
+
|
|
66
|
+
result = {}
|
|
67
|
+
|
|
68
|
+
# Process each metric tag in MVAR
|
|
69
|
+
@mvar.metric_tags.each do |tag|
|
|
70
|
+
delta_set = @mvar.metric_delta_set(tag)
|
|
71
|
+
next unless delta_set
|
|
72
|
+
|
|
73
|
+
# Calculate accumulated delta
|
|
74
|
+
accumulated = calculate_accumulated_delta(delta_set, region_scalars)
|
|
75
|
+
result[tag] = apply_rounding(accumulated)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
result
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Get advance width delta for a glyph
|
|
82
|
+
#
|
|
83
|
+
# @param glyph_id [Integer] Glyph ID
|
|
84
|
+
# @param region_scalars [Array<Float>] Region scalars
|
|
85
|
+
# @return [Integer] Advance width delta
|
|
86
|
+
def advance_width_delta(glyph_id, region_scalars)
|
|
87
|
+
return 0 unless @hvar
|
|
88
|
+
|
|
89
|
+
delta_set = @hvar.advance_width_delta_set(glyph_id)
|
|
90
|
+
return 0 unless delta_set
|
|
91
|
+
|
|
92
|
+
accumulated = calculate_accumulated_delta(delta_set, region_scalars)
|
|
93
|
+
apply_rounding(accumulated)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Get LSB delta for a glyph
|
|
97
|
+
#
|
|
98
|
+
# @param glyph_id [Integer] Glyph ID
|
|
99
|
+
# @param region_scalars [Array<Float>] Region scalars
|
|
100
|
+
# @return [Integer] LSB delta
|
|
101
|
+
def lsb_delta(glyph_id, region_scalars)
|
|
102
|
+
return 0 unless @hvar
|
|
103
|
+
|
|
104
|
+
delta_set = @hvar.lsb_delta_set(glyph_id)
|
|
105
|
+
return 0 unless delta_set
|
|
106
|
+
|
|
107
|
+
accumulated = calculate_accumulated_delta(delta_set, region_scalars)
|
|
108
|
+
apply_rounding(accumulated)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Get RSB delta for a glyph
|
|
112
|
+
#
|
|
113
|
+
# @param glyph_id [Integer] Glyph ID
|
|
114
|
+
# @param region_scalars [Array<Float>] Region scalars
|
|
115
|
+
# @return [Integer] RSB delta
|
|
116
|
+
def rsb_delta(glyph_id, region_scalars)
|
|
117
|
+
return 0 unless @hvar
|
|
118
|
+
|
|
119
|
+
delta_set = @hvar.rsb_delta_set(glyph_id)
|
|
120
|
+
return 0 unless delta_set
|
|
121
|
+
|
|
122
|
+
accumulated = calculate_accumulated_delta(delta_set, region_scalars)
|
|
123
|
+
apply_rounding(accumulated)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Check if horizontal variations are present
|
|
127
|
+
#
|
|
128
|
+
# @return [Boolean] True if HVAR present
|
|
129
|
+
def has_hvar?
|
|
130
|
+
!@hvar.nil?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Check if vertical variations are present
|
|
134
|
+
#
|
|
135
|
+
# @return [Boolean] True if VVAR present
|
|
136
|
+
def has_vvar?
|
|
137
|
+
!@vvar.nil?
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Check if font metric variations are present
|
|
141
|
+
#
|
|
142
|
+
# @return [Boolean] True if MVAR present
|
|
143
|
+
def has_mvar?
|
|
144
|
+
!@mvar.nil?
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
# Load configuration from YAML file
|
|
150
|
+
#
|
|
151
|
+
# @return [Hash] Configuration hash
|
|
152
|
+
def load_config
|
|
153
|
+
config_path = File.join(__dir__, "..", "config",
|
|
154
|
+
"variable_settings.yml")
|
|
155
|
+
loaded = YAML.load_file(config_path)
|
|
156
|
+
# Convert string keys to symbol keys for consistency
|
|
157
|
+
deep_symbolize_keys(loaded)
|
|
158
|
+
rescue StandardError
|
|
159
|
+
# Return default config
|
|
160
|
+
{
|
|
161
|
+
metric_deltas: {
|
|
162
|
+
apply_hvar: true,
|
|
163
|
+
apply_vvar: true,
|
|
164
|
+
apply_mvar: true,
|
|
165
|
+
update_dependent_metrics: true,
|
|
166
|
+
},
|
|
167
|
+
delta_application: {
|
|
168
|
+
rounding_mode: "round",
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Recursively convert hash keys to symbols
|
|
174
|
+
#
|
|
175
|
+
# @param hash [Hash] Hash with string keys
|
|
176
|
+
# @return [Hash] Hash with symbol keys
|
|
177
|
+
def deep_symbolize_keys(hash)
|
|
178
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
179
|
+
new_key = key.to_sym
|
|
180
|
+
new_value = value.is_a?(Hash) ? deep_symbolize_keys(value) : value
|
|
181
|
+
result[new_key] = new_value
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Apply HVAR deltas for a glyph
|
|
186
|
+
#
|
|
187
|
+
# @param glyph_id [Integer] Glyph ID
|
|
188
|
+
# @param region_scalars [Array<Float>] Region scalars
|
|
189
|
+
# @return [Hash] Horizontal metric deltas
|
|
190
|
+
def apply_hvar_deltas(glyph_id, region_scalars)
|
|
191
|
+
result = {}
|
|
192
|
+
|
|
193
|
+
# Advance width delta
|
|
194
|
+
if (delta_set = @hvar.advance_width_delta_set(glyph_id))
|
|
195
|
+
accumulated = calculate_accumulated_delta(delta_set, region_scalars)
|
|
196
|
+
result[:advance_width] = apply_rounding(accumulated)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# LSB delta
|
|
200
|
+
if (delta_set = @hvar.lsb_delta_set(glyph_id))
|
|
201
|
+
accumulated = calculate_accumulated_delta(delta_set, region_scalars)
|
|
202
|
+
result[:lsb] = apply_rounding(accumulated)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# RSB delta
|
|
206
|
+
if (delta_set = @hvar.rsb_delta_set(glyph_id))
|
|
207
|
+
accumulated = calculate_accumulated_delta(delta_set, region_scalars)
|
|
208
|
+
result[:rsb] = apply_rounding(accumulated)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
result
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Apply VVAR deltas for a glyph
|
|
215
|
+
#
|
|
216
|
+
# @param glyph_id [Integer] Glyph ID
|
|
217
|
+
# @param region_scalars [Array<Float>] Region scalars
|
|
218
|
+
# @return [Hash] Vertical metric deltas
|
|
219
|
+
def apply_vvar_deltas(glyph_id, region_scalars)
|
|
220
|
+
result = {}
|
|
221
|
+
|
|
222
|
+
# Similar to HVAR but for vertical metrics
|
|
223
|
+
# VVAR has the same structure as HVAR
|
|
224
|
+
if @vvar.respond_to?(:advance_height_delta_set) && (delta_set = @vvar.advance_height_delta_set(glyph_id))
|
|
225
|
+
accumulated = calculate_accumulated_delta(delta_set, region_scalars)
|
|
226
|
+
result[:advance_height] = apply_rounding(accumulated)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
if @vvar.respond_to?(:tsb_delta_set) && (delta_set = @vvar.tsb_delta_set(glyph_id))
|
|
230
|
+
accumulated = calculate_accumulated_delta(delta_set, region_scalars)
|
|
231
|
+
result[:tsb] = apply_rounding(accumulated)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
if @vvar.respond_to?(:bsb_delta_set) && (delta_set = @vvar.bsb_delta_set(glyph_id))
|
|
235
|
+
accumulated = calculate_accumulated_delta(delta_set, region_scalars)
|
|
236
|
+
result[:bsb] = apply_rounding(accumulated)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
result
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Calculate accumulated delta from delta set and region scalars
|
|
243
|
+
#
|
|
244
|
+
# @param delta_set [Array<Integer>] Delta values for each region
|
|
245
|
+
# @param region_scalars [Array<Float>] Scalar for each region
|
|
246
|
+
# @return [Float] Accumulated delta
|
|
247
|
+
def calculate_accumulated_delta(delta_set, region_scalars)
|
|
248
|
+
accumulated = 0.0
|
|
249
|
+
|
|
250
|
+
delta_set.each_with_index do |delta, index|
|
|
251
|
+
next if index >= region_scalars.length
|
|
252
|
+
|
|
253
|
+
scalar = region_scalars[index]
|
|
254
|
+
accumulated += delta * scalar
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
accumulated
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Apply rounding to delta value
|
|
261
|
+
#
|
|
262
|
+
# @param delta [Float] Delta value
|
|
263
|
+
# @return [Integer] Rounded delta
|
|
264
|
+
def apply_rounding(delta)
|
|
265
|
+
mode = @config.dig(:delta_application, :rounding_mode) || "round"
|
|
266
|
+
|
|
267
|
+
case mode
|
|
268
|
+
when "round"
|
|
269
|
+
delta.round
|
|
270
|
+
when "floor"
|
|
271
|
+
delta.floor
|
|
272
|
+
when "ceil"
|
|
273
|
+
delta.ceil
|
|
274
|
+
when "truncate"
|
|
275
|
+
delta.to_i
|
|
276
|
+
else
|
|
277
|
+
delta.round
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|