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,268 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "interpolator"
|
|
4
|
+
require_relative "table_accessor"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Variation
|
|
8
|
+
# Converts variation data between TrueType (gvar) and CFF2 (blend) formats
|
|
9
|
+
#
|
|
10
|
+
# This class enables format conversion while preserving variation data:
|
|
11
|
+
# - gvar tuples → CFF2 blend operators
|
|
12
|
+
# - CFF2 blend operators → gvar tuples
|
|
13
|
+
#
|
|
14
|
+
# Process for gvar → blend:
|
|
15
|
+
# 1. Extract tuple variations from gvar
|
|
16
|
+
# 2. Map tuple regions to blend regions
|
|
17
|
+
# 3. Embed blend operators in CharStrings at control points
|
|
18
|
+
# 4. Encode delta values in blend format
|
|
19
|
+
#
|
|
20
|
+
# Process for blend → gvar:
|
|
21
|
+
# 1. Parse CharStrings with blend operators
|
|
22
|
+
# 2. Extract blend deltas and regions
|
|
23
|
+
# 3. Map to gvar tuple format
|
|
24
|
+
# 4. Build gvar table structure
|
|
25
|
+
#
|
|
26
|
+
# @example Converting gvar to CFF2 blend
|
|
27
|
+
# converter = VariationConverter.new(font, axes)
|
|
28
|
+
# blend_data = converter.gvar_to_blend(glyph_id)
|
|
29
|
+
#
|
|
30
|
+
# @example Converting CFF2 blend to gvar
|
|
31
|
+
# converter = VariationConverter.new(font, axes)
|
|
32
|
+
# tuple_data = converter.blend_to_gvar(glyph_id)
|
|
33
|
+
class VariationConverter
|
|
34
|
+
include TableAccessor
|
|
35
|
+
|
|
36
|
+
# @return [TrueTypeFont, OpenTypeFont] Font instance
|
|
37
|
+
attr_reader :font
|
|
38
|
+
|
|
39
|
+
# @return [Array<VariationAxisRecord>] Variation axes
|
|
40
|
+
attr_reader :axes
|
|
41
|
+
|
|
42
|
+
# Initialize converter
|
|
43
|
+
#
|
|
44
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font instance
|
|
45
|
+
# @param axes [Array<VariationAxisRecord>] Variation axes from fvar
|
|
46
|
+
def initialize(font, axes)
|
|
47
|
+
@font = font
|
|
48
|
+
@axes = axes || []
|
|
49
|
+
@variation_tables = {}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Convert gvar tuples to CFF2 blend format for a glyph
|
|
53
|
+
#
|
|
54
|
+
# @param glyph_id [Integer] Glyph ID
|
|
55
|
+
# @return [Hash, nil] Blend data or nil
|
|
56
|
+
def gvar_to_blend(glyph_id)
|
|
57
|
+
return nil unless has_variation_table?("gvar")
|
|
58
|
+
return nil unless has_variation_table?("glyf")
|
|
59
|
+
|
|
60
|
+
gvar = variation_table("gvar")
|
|
61
|
+
return nil unless gvar
|
|
62
|
+
|
|
63
|
+
# Get tuple variations for this glyph
|
|
64
|
+
tuple_data = gvar.glyph_tuple_variations(glyph_id)
|
|
65
|
+
return nil unless tuple_data
|
|
66
|
+
|
|
67
|
+
# Convert tuples to blend format
|
|
68
|
+
convert_tuples_to_blend(tuple_data)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Convert CFF2 blend operators to gvar tuple format for a glyph
|
|
72
|
+
#
|
|
73
|
+
# @param glyph_id [Integer] Glyph ID
|
|
74
|
+
# @return [Hash, nil] Tuple data or nil
|
|
75
|
+
def blend_to_gvar(_glyph_id)
|
|
76
|
+
return nil unless has_variation_table?("CFF2")
|
|
77
|
+
|
|
78
|
+
cff2 = variation_table("CFF2")
|
|
79
|
+
return nil unless cff2
|
|
80
|
+
|
|
81
|
+
# Get CharString with blend operators
|
|
82
|
+
# This is a placeholder - full implementation would parse CharString
|
|
83
|
+
# and extract blend operator data
|
|
84
|
+
|
|
85
|
+
# Convert blend data to tuples
|
|
86
|
+
# Placeholder for full implementation
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Check if variation data can be converted
|
|
91
|
+
#
|
|
92
|
+
# @return [Boolean] True if conversion possible
|
|
93
|
+
def can_convert?
|
|
94
|
+
!@axes.empty? && (
|
|
95
|
+
has_variation_table?("gvar") ||
|
|
96
|
+
has_variation_table?("CFF2")
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
# Convert tuple variations to blend format
|
|
103
|
+
#
|
|
104
|
+
# @param tuple_data [Hash] Tuple variation data from gvar
|
|
105
|
+
# @return [Hash] Blend format data
|
|
106
|
+
def convert_tuples_to_blend(tuple_data)
|
|
107
|
+
tuples = tuple_data[:tuples] || []
|
|
108
|
+
point_count = tuple_data[:point_count] || 0
|
|
109
|
+
|
|
110
|
+
# Build blend regions from tuples
|
|
111
|
+
regions = tuples.map { |tuple| build_region_from_tuple(tuple) }
|
|
112
|
+
|
|
113
|
+
# Extract deltas for each point
|
|
114
|
+
point_deltas = extract_point_deltas(tuples, point_count)
|
|
115
|
+
|
|
116
|
+
{
|
|
117
|
+
regions: regions,
|
|
118
|
+
point_deltas: point_deltas,
|
|
119
|
+
num_regions: regions.length,
|
|
120
|
+
num_axes: @axes.length,
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Build region from tuple peak/start/end coordinates
|
|
125
|
+
#
|
|
126
|
+
# @param tuple [Hash] Tuple data with :peak, :start, :end
|
|
127
|
+
# @return [Hash] Region definition
|
|
128
|
+
def build_region_from_tuple(tuple)
|
|
129
|
+
region = {}
|
|
130
|
+
|
|
131
|
+
@axes.each_with_index do |axis, axis_index|
|
|
132
|
+
# Extract coordinates for this axis
|
|
133
|
+
peak = tuple[:peak] ? tuple[:peak][axis_index] : 0.0
|
|
134
|
+
start_val = tuple[:start] ? tuple[:start][axis_index] : -1.0
|
|
135
|
+
end_val = tuple[:end] ? tuple[:end][axis_index] : 1.0
|
|
136
|
+
|
|
137
|
+
region[axis.axis_tag] = {
|
|
138
|
+
start: start_val,
|
|
139
|
+
peak: peak,
|
|
140
|
+
end: end_val,
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
region
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Extract point deltas from all tuples
|
|
148
|
+
#
|
|
149
|
+
# @param tuples [Array<Hash>] Tuple variations
|
|
150
|
+
# @param point_count [Integer] Number of points
|
|
151
|
+
# @return [Array<Array<Hash>>] Deltas per point per tuple
|
|
152
|
+
def extract_point_deltas(tuples, point_count)
|
|
153
|
+
return [] if point_count.zero?
|
|
154
|
+
|
|
155
|
+
# Initialize deltas array
|
|
156
|
+
point_deltas = Array.new(point_count) { [] }
|
|
157
|
+
|
|
158
|
+
# For each tuple, extract deltas for all points
|
|
159
|
+
tuples.each do |tuple|
|
|
160
|
+
deltas = parse_tuple_deltas(tuple, point_count)
|
|
161
|
+
|
|
162
|
+
deltas.each_with_index do |delta, point_index|
|
|
163
|
+
point_deltas[point_index] << delta
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
point_deltas
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Parse deltas from a tuple
|
|
171
|
+
#
|
|
172
|
+
# @param tuple [Hash] Tuple data
|
|
173
|
+
# @param point_count [Integer] Number of points
|
|
174
|
+
# @return [Array<Hash>] Deltas with :x and :y
|
|
175
|
+
def parse_tuple_deltas(_tuple, point_count)
|
|
176
|
+
# This is a placeholder - full implementation would:
|
|
177
|
+
# 1. Parse delta data from tuple
|
|
178
|
+
# 2. Decompress if needed
|
|
179
|
+
# 3. Return array of { x: dx, y: dy } for each point
|
|
180
|
+
|
|
181
|
+
Array.new(point_count) { { x: 0, y: 0 } }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Convert blend data to tuple format
|
|
185
|
+
#
|
|
186
|
+
# @param blend_data [Hash] Blend format data
|
|
187
|
+
# @return [Hash] Tuple variation data
|
|
188
|
+
def convert_blend_to_tuples(blend_data)
|
|
189
|
+
regions = blend_data[:regions] || []
|
|
190
|
+
point_deltas = blend_data[:point_deltas] || []
|
|
191
|
+
|
|
192
|
+
# Build tuples from regions
|
|
193
|
+
tuples = regions.map.with_index do |region, region_index|
|
|
194
|
+
build_tuple_from_region(region, point_deltas, region_index)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
{
|
|
198
|
+
tuples: tuples,
|
|
199
|
+
point_count: point_deltas.length,
|
|
200
|
+
}
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Build tuple from region and deltas
|
|
204
|
+
#
|
|
205
|
+
# @param region [Hash] Region definition
|
|
206
|
+
# @param point_deltas [Array<Array<Hash>>] Deltas per point
|
|
207
|
+
# @param region_index [Integer] Region index
|
|
208
|
+
# @return [Hash] Tuple data
|
|
209
|
+
def build_tuple_from_region(region, point_deltas, region_index)
|
|
210
|
+
# Extract peak, start, end for all axes
|
|
211
|
+
peak = Array.new(@axes.length, 0.0)
|
|
212
|
+
start_vals = Array.new(@axes.length, -1.0)
|
|
213
|
+
end_vals = Array.new(@axes.length, 1.0)
|
|
214
|
+
|
|
215
|
+
@axes.each_with_index do |axis, axis_index|
|
|
216
|
+
axis_region = region[axis.axis_tag]
|
|
217
|
+
next unless axis_region
|
|
218
|
+
|
|
219
|
+
peak[axis_index] = axis_region[:peak]
|
|
220
|
+
start_vals[axis_index] = axis_region[:start]
|
|
221
|
+
end_vals[axis_index] = axis_region[:end]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Extract deltas for this region
|
|
225
|
+
deltas = point_deltas.map do |point_delta_set|
|
|
226
|
+
point_delta_set[region_index] || { x: 0, y: 0 }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
{
|
|
230
|
+
peak: peak,
|
|
231
|
+
start: start_vals,
|
|
232
|
+
end: end_vals,
|
|
233
|
+
deltas: deltas,
|
|
234
|
+
}
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Encode deltas in CharString blend format
|
|
238
|
+
#
|
|
239
|
+
# @param base_value [Numeric] Base value
|
|
240
|
+
# @param deltas [Array<Numeric>] Delta values
|
|
241
|
+
# @return [Array<Numeric>] Blend operator arguments
|
|
242
|
+
def encode_blend_operator(base_value, deltas)
|
|
243
|
+
# CFF2 blend format: base_value delta1 delta2 ... K N blend
|
|
244
|
+
# Where K = number of deltas, N = number of blend operations
|
|
245
|
+
[base_value] + deltas + [deltas.length, 1]
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Decode blend operator arguments to base and deltas
|
|
249
|
+
#
|
|
250
|
+
# @param args [Array<Numeric>] Blend operator arguments
|
|
251
|
+
# @return [Hash] Base value and deltas
|
|
252
|
+
def decode_blend_operator(args)
|
|
253
|
+
return { base: 0, deltas: [] } if args.length < 3
|
|
254
|
+
|
|
255
|
+
# Last two values are K and N
|
|
256
|
+
k = args[-2]
|
|
257
|
+
_n = args[-1]
|
|
258
|
+
|
|
259
|
+
# Before K and N: base + deltas
|
|
260
|
+
values = args[0...-2]
|
|
261
|
+
base = values[0] || 0
|
|
262
|
+
deltas = values[1, k] || []
|
|
263
|
+
|
|
264
|
+
{ base: base, deltas: deltas }
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "variation_context"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Variation
|
|
7
|
+
# Extracts variation data from OpenType variable fonts
|
|
8
|
+
#
|
|
9
|
+
# This class provides a unified interface to extract variation information
|
|
10
|
+
# from variable fonts, including:
|
|
11
|
+
# - Variation axes (from fvar table)
|
|
12
|
+
# - Named instances (from fvar table)
|
|
13
|
+
# - Variation type (TrueType gvar or PostScript CFF2)
|
|
14
|
+
#
|
|
15
|
+
# @example Extracting variation data
|
|
16
|
+
# extractor = Fontisan::Variation::DataExtractor.new(font)
|
|
17
|
+
# data = extractor.extract
|
|
18
|
+
# if data
|
|
19
|
+
# puts "Axes: #{data[:axes].map(&:axis_tag).join(', ')}"
|
|
20
|
+
# puts "Instances: #{data[:instances].length}"
|
|
21
|
+
# end
|
|
22
|
+
class DataExtractor
|
|
23
|
+
# Initialize extractor with a font
|
|
24
|
+
#
|
|
25
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font to extract from
|
|
26
|
+
def initialize(font)
|
|
27
|
+
@font = font
|
|
28
|
+
@context = VariationContext.new(font)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Extract variation data from the font
|
|
32
|
+
#
|
|
33
|
+
# @return [Hash, nil] Variation data or nil if not a variable font
|
|
34
|
+
def extract
|
|
35
|
+
return nil unless @context.variable_font?
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
axes: extract_axes,
|
|
39
|
+
instances: extract_instances,
|
|
40
|
+
has_gvar: @font.has_table?("gvar"),
|
|
41
|
+
has_cff2: @font.has_table?("CFF2"),
|
|
42
|
+
variation_type: @context.variation_type,
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Check if font is a variable font
|
|
47
|
+
#
|
|
48
|
+
# @return [Boolean] True if font has fvar table
|
|
49
|
+
def variable_font?
|
|
50
|
+
@context.variable_font?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Extract variation axes from fvar table
|
|
56
|
+
#
|
|
57
|
+
# @return [Array<VariationAxisRecord>] Array of axis records
|
|
58
|
+
# @raise [VariationDataCorruptedError] If axes cannot be extracted
|
|
59
|
+
def extract_axes
|
|
60
|
+
return [] unless @context.fvar
|
|
61
|
+
|
|
62
|
+
@context.axes
|
|
63
|
+
rescue StandardError => e
|
|
64
|
+
raise VariationDataCorruptedError.new(
|
|
65
|
+
message: "Failed to extract variation axes: #{e.message}",
|
|
66
|
+
details: { error_class: e.class.name },
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Extract named instances from fvar table
|
|
71
|
+
#
|
|
72
|
+
# @return [Array<Hash>] Array of instance information
|
|
73
|
+
# @raise [VariationDataCorruptedError] If instances cannot be extracted
|
|
74
|
+
def extract_instances
|
|
75
|
+
return [] unless @context.fvar
|
|
76
|
+
|
|
77
|
+
@context.fvar.instances || []
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
raise VariationDataCorruptedError.new(
|
|
80
|
+
message: "Failed to extract instances: #{e.message}",
|
|
81
|
+
details: { error_class: e.class.name },
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "delta_parser"
|
|
4
|
+
require_relative "interpolator"
|
|
5
|
+
require_relative "region_matcher"
|
|
6
|
+
require_relative "table_accessor"
|
|
7
|
+
|
|
8
|
+
module Fontisan
|
|
9
|
+
module Variation
|
|
10
|
+
# Applies variation deltas to glyph outlines
|
|
11
|
+
#
|
|
12
|
+
# This class handles the complete delta application process for TrueType
|
|
13
|
+
# variable fonts using gvar table data:
|
|
14
|
+
# 1. Parse base glyph outline points
|
|
15
|
+
# 2. Match active tuple variations to coordinates
|
|
16
|
+
# 3. Parse and decompress deltas
|
|
17
|
+
# 4. Apply deltas: new_point = base + Σ(delta × scalar)
|
|
18
|
+
# 5. Expand IUP (Inferred Untouched Points)
|
|
19
|
+
#
|
|
20
|
+
# Reference: OpenType specification, gvar table
|
|
21
|
+
#
|
|
22
|
+
# @example Applying deltas to a glyph
|
|
23
|
+
# applier = Fontisan::Variation::DeltaApplier.new(font, interpolator, region_matcher)
|
|
24
|
+
# adjusted_points = applier.apply_deltas(glyph_id, coordinates)
|
|
25
|
+
class DeltaApplier
|
|
26
|
+
include TableAccessor
|
|
27
|
+
|
|
28
|
+
# @return [Font] Font object
|
|
29
|
+
attr_reader :font
|
|
30
|
+
|
|
31
|
+
# @return [Interpolator] Coordinate interpolator
|
|
32
|
+
attr_reader :interpolator
|
|
33
|
+
|
|
34
|
+
# @return [RegionMatcher] Region matcher
|
|
35
|
+
attr_reader :region_matcher
|
|
36
|
+
|
|
37
|
+
# @return [DeltaParser] Delta parser
|
|
38
|
+
attr_reader :delta_parser
|
|
39
|
+
|
|
40
|
+
# Initialize delta applier
|
|
41
|
+
#
|
|
42
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font with gvar table
|
|
43
|
+
# @param interpolator [Interpolator] Coordinate interpolator
|
|
44
|
+
# @param region_matcher [RegionMatcher] Region matcher
|
|
45
|
+
def initialize(font, interpolator, region_matcher)
|
|
46
|
+
@font = font
|
|
47
|
+
@interpolator = interpolator
|
|
48
|
+
@region_matcher = region_matcher
|
|
49
|
+
@delta_parser = DeltaParser.new
|
|
50
|
+
@variation_tables = {}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Apply deltas to a glyph at given coordinates
|
|
54
|
+
#
|
|
55
|
+
# @param glyph_id [Integer] Glyph ID
|
|
56
|
+
# @param coordinates [Hash<String, Float>] Design space coordinates
|
|
57
|
+
# @return [Array<Hash>, nil] Adjusted points or nil if not applicable
|
|
58
|
+
def apply_deltas(glyph_id, coordinates)
|
|
59
|
+
gvar = variation_table("gvar")
|
|
60
|
+
glyf = variation_table("glyf")
|
|
61
|
+
return nil unless gvar && glyf
|
|
62
|
+
|
|
63
|
+
# Get base glyph outline points
|
|
64
|
+
base_points = extract_glyph_points(glyph_id, glyf)
|
|
65
|
+
return nil if base_points.nil? || base_points.empty?
|
|
66
|
+
|
|
67
|
+
# Get tuple variations for this glyph
|
|
68
|
+
tuple_data = gvar.glyph_tuple_variations(glyph_id)
|
|
69
|
+
return base_points if tuple_data.nil? || tuple_data[:tuples].empty?
|
|
70
|
+
|
|
71
|
+
# Match active tuples to coordinates
|
|
72
|
+
matches = @region_matcher.match_tuples(
|
|
73
|
+
coordinates: coordinates,
|
|
74
|
+
tuples: tuple_data[:tuples],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return base_points if matches.empty?
|
|
78
|
+
|
|
79
|
+
# Apply each active tuple's deltas
|
|
80
|
+
adjusted_points = base_points.dup
|
|
81
|
+
matches.each do |match|
|
|
82
|
+
apply_tuple_deltas(adjusted_points, match, tuple_data, base_points.length)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
adjusted_points
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Extract outline points from glyph
|
|
89
|
+
#
|
|
90
|
+
# @param glyph_id [Integer] Glyph ID
|
|
91
|
+
# @param glyf [Glyf] Glyf table
|
|
92
|
+
# @return [Array<Hash>, nil] Array of points with :x, :y, :on_curve
|
|
93
|
+
def extract_glyph_points(glyph_id, glyf)
|
|
94
|
+
# This is a simplified version - full implementation would parse
|
|
95
|
+
# complete glyf table data including composite glyphs
|
|
96
|
+
glyph_data = glyf.glyph_data(glyph_id)
|
|
97
|
+
return nil if glyph_data.nil?
|
|
98
|
+
|
|
99
|
+
# Parse glyph outline (simplified)
|
|
100
|
+
# Real implementation would fully parse SimpleGlyph or CompositeGlyph
|
|
101
|
+
[]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
# Apply a single tuple's deltas to points
|
|
107
|
+
#
|
|
108
|
+
# @param points [Array<Hash>] Points to adjust (modified in place)
|
|
109
|
+
# @param match [Hash] Matched tuple with :tuple and :scalar
|
|
110
|
+
# @param tuple_data [Hash] Complete tuple data from gvar
|
|
111
|
+
# @param point_count [Integer] Number of points
|
|
112
|
+
def apply_tuple_deltas(points, match, tuple_data, point_count)
|
|
113
|
+
tuple = match[:tuple]
|
|
114
|
+
scalar = match[:scalar]
|
|
115
|
+
|
|
116
|
+
return if scalar.zero?
|
|
117
|
+
|
|
118
|
+
# Parse deltas for this tuple
|
|
119
|
+
# Note: In real implementation, we'd need to extract the actual
|
|
120
|
+
# delta data from the gvar table at the correct offset
|
|
121
|
+
deltas = parse_tuple_deltas(tuple, point_count, tuple_data)
|
|
122
|
+
return if deltas.nil?
|
|
123
|
+
|
|
124
|
+
# Check if points need IUP expansion
|
|
125
|
+
if tuple[:private_points]
|
|
126
|
+
# Expand IUP for untouched points
|
|
127
|
+
deltas = expand_iup(deltas, point_count)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Apply deltas with scalar
|
|
131
|
+
points.each_with_index do |point, i|
|
|
132
|
+
next if i >= deltas.length
|
|
133
|
+
|
|
134
|
+
delta = deltas[i]
|
|
135
|
+
point[:x] += delta[:x] * scalar
|
|
136
|
+
point[:y] += delta[:y] * scalar
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Parse deltas for a tuple variation
|
|
141
|
+
#
|
|
142
|
+
# @param tuple [Hash] Tuple variation info
|
|
143
|
+
# @param point_count [Integer] Number of points
|
|
144
|
+
# @param tuple_data [Hash] Complete tuple data
|
|
145
|
+
# @return [Array<Hash>, nil] Array of point deltas
|
|
146
|
+
def parse_tuple_deltas(tuple, point_count, tuple_data)
|
|
147
|
+
# In real implementation, this would:
|
|
148
|
+
# 1. Calculate offset to delta data
|
|
149
|
+
# 2. Extract raw delta bytes
|
|
150
|
+
# 3. Call delta_parser.parse with appropriate flags
|
|
151
|
+
|
|
152
|
+
# Placeholder - full implementation needs access to raw delta data
|
|
153
|
+
@delta_parser.parse(
|
|
154
|
+
"",
|
|
155
|
+
point_count,
|
|
156
|
+
private_points: tuple[:private_points],
|
|
157
|
+
shared_points: tuple_data[:has_shared_points] ? [] : nil,
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Expand IUP (Inferred Untouched Points)
|
|
162
|
+
#
|
|
163
|
+
# Points without explicit deltas have their deltas inferred through
|
|
164
|
+
# linear interpolation between surrounding touched points.
|
|
165
|
+
#
|
|
166
|
+
# @param deltas [Array<Hash>] Delta array (sparse)
|
|
167
|
+
# @param point_count [Integer] Total number of points
|
|
168
|
+
# @return [Array<Hash>] Expanded delta array
|
|
169
|
+
def expand_iup(deltas, point_count)
|
|
170
|
+
return deltas if deltas.length == point_count
|
|
171
|
+
|
|
172
|
+
expanded = Array.new(point_count) { { x: 0, y: 0 } }
|
|
173
|
+
|
|
174
|
+
# Copy explicit deltas
|
|
175
|
+
deltas.each_with_index do |delta, i|
|
|
176
|
+
next if i >= point_count
|
|
177
|
+
|
|
178
|
+
expanded[i] = delta if delta[:x] != 0 || delta[:y] != 0
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Find touched points
|
|
182
|
+
touched = []
|
|
183
|
+
deltas.each_with_index do |delta, i|
|
|
184
|
+
touched << i if delta[:x] != 0 || delta[:y] != 0
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
return expanded if touched.empty?
|
|
188
|
+
|
|
189
|
+
# Infer untouched points
|
|
190
|
+
point_count.times do |i|
|
|
191
|
+
next if touched.include?(i)
|
|
192
|
+
|
|
193
|
+
# Find previous and next touched points
|
|
194
|
+
prev_idx = find_previous_touched(touched, i)
|
|
195
|
+
next_idx = find_next_touched(touched, i, point_count)
|
|
196
|
+
|
|
197
|
+
# Interpolate delta
|
|
198
|
+
if prev_idx && next_idx
|
|
199
|
+
expanded[i] = interpolate_delta(
|
|
200
|
+
deltas[prev_idx],
|
|
201
|
+
deltas[next_idx],
|
|
202
|
+
i, prev_idx, next_idx
|
|
203
|
+
)
|
|
204
|
+
elsif prev_idx
|
|
205
|
+
# Use previous delta
|
|
206
|
+
expanded[i] = deltas[prev_idx].dup
|
|
207
|
+
elsif next_idx
|
|
208
|
+
# Use next delta
|
|
209
|
+
expanded[i] = deltas[next_idx].dup
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
expanded
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Find previous touched point
|
|
217
|
+
#
|
|
218
|
+
# @param touched [Array<Integer>] Touched point indices
|
|
219
|
+
# @param index [Integer] Current point index
|
|
220
|
+
# @return [Integer, nil] Previous touched index or nil
|
|
221
|
+
def find_previous_touched(touched, index)
|
|
222
|
+
touched.reverse_each do |t|
|
|
223
|
+
return t if t < index
|
|
224
|
+
end
|
|
225
|
+
nil
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Find next touched point
|
|
229
|
+
#
|
|
230
|
+
# @param touched [Array<Integer>] Touched point indices
|
|
231
|
+
# @param index [Integer] Current point index
|
|
232
|
+
# @param point_count [Integer] Total points (for wrapping)
|
|
233
|
+
# @return [Integer, nil] Next touched index or nil
|
|
234
|
+
def find_next_touched(touched, index, _point_count)
|
|
235
|
+
# Check forward
|
|
236
|
+
touched.each do |t|
|
|
237
|
+
return t if t > index
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Wrap around (contour is closed)
|
|
241
|
+
touched.first
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Interpolate delta between two touched points
|
|
245
|
+
#
|
|
246
|
+
# @param delta1 [Hash] First delta
|
|
247
|
+
# @param delta2 [Hash] Second delta
|
|
248
|
+
# @param current [Integer] Current point index
|
|
249
|
+
# @param idx1 [Integer] First point index
|
|
250
|
+
# @param idx2 [Integer] Second point index
|
|
251
|
+
# @return [Hash] Interpolated delta
|
|
252
|
+
def interpolate_delta(delta1, delta2, current, idx1, idx2)
|
|
253
|
+
# Linear interpolation
|
|
254
|
+
range = idx2 - idx1
|
|
255
|
+
return delta1.dup if range.zero?
|
|
256
|
+
|
|
257
|
+
ratio = (current - idx1).to_f / range
|
|
258
|
+
|
|
259
|
+
{
|
|
260
|
+
x: delta1[:x] + (delta2[:x] - delta1[:x]) * ratio,
|
|
261
|
+
y: delta1[:y] + (delta2[:y] - delta1[:y]) * ratio,
|
|
262
|
+
}
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|