fontisan 0.1.0 → 0.2.1
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 +672 -69
- data/Gemfile +1 -0
- data/LICENSE +5 -1
- data/README.adoc +1477 -297
- data/Rakefile +63 -41
- 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 +364 -4
- data/lib/fontisan/collection/builder.rb +341 -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 +317 -0
- data/lib/fontisan/collection/writer.rb +306 -0
- data/lib/fontisan/commands/base_command.rb +24 -1
- data/lib/fontisan/commands/convert_command.rb +218 -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 +286 -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 +203 -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 +79 -0
- data/lib/fontisan/converters/conversion_strategy.rb +96 -0
- data/lib/fontisan/converters/format_converter.rb +408 -0
- data/lib/fontisan/converters/outline_converter.rb +998 -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 +122 -15
- data/lib/fontisan/font_writer.rb +302 -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 +310 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +266 -0
- data/lib/fontisan/hints/postscript_hint_extractor.rb +354 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +117 -0
- data/lib/fontisan/hints/truetype_hint_extractor.rb +289 -0
- data/lib/fontisan/loading_modes.rb +115 -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 +405 -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 +321 -19
- data/lib/fontisan/open_type_font_extensions.rb +54 -0
- 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/pipeline/format_detector.rb +249 -0
- data/lib/fontisan/pipeline/output_writer.rb +154 -0
- data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
- data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
- data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
- data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +411 -0
- data/lib/fontisan/pipeline/variation_resolver.rb +165 -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 +934 -0
- data/lib/fontisan/tables/cff/charstring_builder.rb +356 -0
- data/lib/fontisan/tables/cff/charstring_parser.rb +237 -0
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -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 +257 -0
- data/lib/fontisan/tables/cff/encoding.rb +274 -0
- data/lib/fontisan/tables/cff/header.rb +102 -0
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +207 -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/offset_recalculator.rb +70 -0
- data/lib/fontisan/tables/cff/private_dict.rb +284 -0
- data/lib/fontisan/tables/cff/private_dict_writer.rb +125 -0
- data/lib/fontisan/tables/cff/table_builder.rb +221 -0
- data/lib/fontisan/tables/cff/top_dict.rb +236 -0
- data/lib/fontisan/tables/cff.rb +489 -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/private_dict_blend_handler.rb +246 -0
- data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
- data/lib/fontisan/tables/cff2/table_builder.rb +574 -0
- data/lib/fontisan/tables/cff2/table_reader.rb +419 -0
- data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
- data/lib/fontisan/tables/cff2.rb +346 -0
- data/lib/fontisan/tables/cvar.rb +203 -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 +231 -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 +321 -20
- data/lib/fontisan/true_type_font_extensions.rb +54 -0
- data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +60 -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/validation/variable_font_validator.rb +218 -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 +375 -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/instance_writer.rb +341 -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/tuple_variation_header.rb +51 -0
- data/lib/fontisan/variation/validator.rb +345 -0
- data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
- data/lib/fontisan/variation/variation_context.rb +211 -0
- data/lib/fontisan/variation/variation_preserver.rb +288 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/version.rb.orig +9 -0
- data/lib/fontisan/woff2/directory.rb +257 -0
- data/lib/fontisan/woff2/glyf_transformer.rb +666 -0
- data/lib/fontisan/woff2/header.rb +101 -0
- data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
- data/lib/fontisan/woff2/table_transformer.rb +163 -0
- data/lib/fontisan/woff2_font.rb +717 -0
- data/lib/fontisan/woff_font.rb +488 -0
- data/lib/fontisan.rb +132 -0
- data/scripts/compare_stack_aware.rb +187 -0
- data/scripts/measure_optimization.rb +141 -0
- metadata +234 -4
|
@@ -0,0 +1,375 @@
|
|
|
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 = Converter.new(font, axes)
|
|
28
|
+
# blend_data = converter.gvar_to_blend(glyph_id)
|
|
29
|
+
#
|
|
30
|
+
# @example Converting CFF2 blend to gvar
|
|
31
|
+
# converter = Converter.new(font, axes)
|
|
32
|
+
# tuple_data = converter.blend_to_gvar(glyph_id)
|
|
33
|
+
class Converter
|
|
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
|
+
charstring = cff2.charstring_for_glyph(glyph_id)
|
|
83
|
+
return nil unless charstring
|
|
84
|
+
|
|
85
|
+
# Parse CharString to extract blend data
|
|
86
|
+
charstring.parse unless charstring.instance_variable_get(:@parsed)
|
|
87
|
+
blend_data = charstring.blend_data
|
|
88
|
+
return nil if blend_data.nil? || blend_data.empty?
|
|
89
|
+
|
|
90
|
+
# Convert blend data to tuple format
|
|
91
|
+
convert_blend_to_tuples_for_glyph(blend_data)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Convert all glyphs from gvar to blend format
|
|
95
|
+
#
|
|
96
|
+
# @param glyph_count [Integer] Number of glyphs
|
|
97
|
+
# @return [Hash<Integer, Hash>] Map of glyph_id to blend data
|
|
98
|
+
def convert_all_gvar_to_blend(glyph_count)
|
|
99
|
+
return {} unless can_convert?
|
|
100
|
+
|
|
101
|
+
(0...glyph_count).each_with_object({}) do |glyph_id, result|
|
|
102
|
+
blend_data = gvar_to_blend(glyph_id)
|
|
103
|
+
result[glyph_id] = blend_data if blend_data
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Convert all glyphs from blend to gvar format
|
|
108
|
+
#
|
|
109
|
+
# @param glyph_count [Integer] Number of glyphs
|
|
110
|
+
# @return [Hash<Integer, Hash>] Map of glyph_id to tuple data
|
|
111
|
+
def convert_all_blend_to_gvar(glyph_count)
|
|
112
|
+
return {} unless can_convert?
|
|
113
|
+
|
|
114
|
+
(0...glyph_count).each_with_object({}) do |glyph_id, result|
|
|
115
|
+
tuple_data = blend_to_gvar(glyph_id)
|
|
116
|
+
result[glyph_id] = tuple_data if tuple_data
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Check if variation data can be converted
|
|
121
|
+
#
|
|
122
|
+
# @return [Boolean] True if conversion possible
|
|
123
|
+
def can_convert?
|
|
124
|
+
!@axes.empty? && (
|
|
125
|
+
has_variation_table?("gvar") ||
|
|
126
|
+
has_variation_table?("CFF2")
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
# Convert blend data from a glyph to tuple format
|
|
133
|
+
#
|
|
134
|
+
# @param blend_data [Array<Hash>] Array of blend operations
|
|
135
|
+
# @return [Hash] Tuple variation data
|
|
136
|
+
def convert_blend_to_tuples_for_glyph(blend_data)
|
|
137
|
+
# Each blend operation represents variation at different points
|
|
138
|
+
# We need to aggregate these into region-based tuples
|
|
139
|
+
|
|
140
|
+
# Extract all regions from blend operations
|
|
141
|
+
regions_map = {}
|
|
142
|
+
point_count = 0
|
|
143
|
+
|
|
144
|
+
blend_data.each_with_index do |blend_op, idx|
|
|
145
|
+
blend_op[:blends].each do |blend|
|
|
146
|
+
# Track the maximum point index we've seen
|
|
147
|
+
point_count = [point_count, idx + 1].max
|
|
148
|
+
|
|
149
|
+
# For each delta axis, we need to create or update a region
|
|
150
|
+
blend[:deltas].each_with_index do |delta, axis_index|
|
|
151
|
+
next if delta.zero? # Skip zero deltas
|
|
152
|
+
|
|
153
|
+
# Create region key based on unique delta pattern
|
|
154
|
+
region_key = "region_#{axis_index}"
|
|
155
|
+
|
|
156
|
+
regions_map[region_key] ||= {
|
|
157
|
+
axis_index: axis_index,
|
|
158
|
+
deltas_per_point: Array.new(point_count) { { x: 0, y: 0 } },
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Store this delta for this point
|
|
162
|
+
# Note: CFF2 blend deltas are per-coordinate, we need to map to x/y
|
|
163
|
+
# This is a simplified mapping - full implementation would track
|
|
164
|
+
# which coordinates are being varied
|
|
165
|
+
regions_map[region_key][:deltas_per_point][idx / 2] ||= { x: 0, y: 0 }
|
|
166
|
+
if idx.even?
|
|
167
|
+
regions_map[region_key][:deltas_per_point][idx / 2][:x] = delta
|
|
168
|
+
else
|
|
169
|
+
regions_map[region_key][:deltas_per_point][idx / 2][:y] = delta
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Convert regions to tuples
|
|
176
|
+
tuples = []
|
|
177
|
+
regions_map.each_value do |region_data|
|
|
178
|
+
axis_index = region_data[:axis_index]
|
|
179
|
+
|
|
180
|
+
# Build peak coordinates (one per axis)
|
|
181
|
+
peak = Array.new(@axes.length, 0.0)
|
|
182
|
+
peak[axis_index] = 1.0 if axis_index < @axes.length
|
|
183
|
+
|
|
184
|
+
# Build start/end (default full range)
|
|
185
|
+
start_vals = Array.new(@axes.length, -1.0)
|
|
186
|
+
end_vals = Array.new(@axes.length, 1.0)
|
|
187
|
+
|
|
188
|
+
tuples << {
|
|
189
|
+
peak: peak,
|
|
190
|
+
start: start_vals,
|
|
191
|
+
end: end_vals,
|
|
192
|
+
deltas: region_data[:deltas_per_point],
|
|
193
|
+
}
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
{
|
|
197
|
+
tuples: tuples,
|
|
198
|
+
point_count: point_count,
|
|
199
|
+
}
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Convert tuple variations to blend format
|
|
203
|
+
#
|
|
204
|
+
# @param tuple_data [Hash] Tuple variation data from gvar
|
|
205
|
+
# @return [Hash] Blend format data
|
|
206
|
+
def convert_tuples_to_blend(tuple_data)
|
|
207
|
+
tuples = tuple_data[:tuples] || []
|
|
208
|
+
point_count = tuple_data[:point_count] || 0
|
|
209
|
+
|
|
210
|
+
# Build blend regions from tuples
|
|
211
|
+
regions = tuples.map { |tuple| build_region_from_tuple(tuple) }
|
|
212
|
+
|
|
213
|
+
# Extract deltas for each point
|
|
214
|
+
point_deltas = extract_point_deltas(tuples, point_count)
|
|
215
|
+
|
|
216
|
+
{
|
|
217
|
+
regions: regions,
|
|
218
|
+
point_deltas: point_deltas,
|
|
219
|
+
num_regions: regions.length,
|
|
220
|
+
num_axes: @axes.length,
|
|
221
|
+
}
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Build region from tuple peak/start/end coordinates
|
|
225
|
+
#
|
|
226
|
+
# @param tuple [Hash] Tuple data with :peak, :start, :end
|
|
227
|
+
# @return [Hash] Region definition
|
|
228
|
+
def build_region_from_tuple(tuple)
|
|
229
|
+
region = {}
|
|
230
|
+
|
|
231
|
+
@axes.each_with_index do |axis, axis_index|
|
|
232
|
+
# Extract coordinates for this axis
|
|
233
|
+
peak = tuple[:peak] ? tuple[:peak][axis_index] : 0.0
|
|
234
|
+
start_val = tuple[:start] ? tuple[:start][axis_index] : -1.0
|
|
235
|
+
end_val = tuple[:end] ? tuple[:end][axis_index] : 1.0
|
|
236
|
+
|
|
237
|
+
region[axis.axis_tag] = {
|
|
238
|
+
start: start_val,
|
|
239
|
+
peak: peak,
|
|
240
|
+
end: end_val,
|
|
241
|
+
}
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
region
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Extract point deltas from all tuples
|
|
248
|
+
#
|
|
249
|
+
# @param tuples [Array<Hash>] Tuple variations
|
|
250
|
+
# @param point_count [Integer] Number of points
|
|
251
|
+
# @return [Array<Array<Hash>>] Deltas per point per tuple
|
|
252
|
+
def extract_point_deltas(tuples, point_count)
|
|
253
|
+
return [] if point_count.zero?
|
|
254
|
+
|
|
255
|
+
# Initialize deltas array
|
|
256
|
+
point_deltas = Array.new(point_count) { [] }
|
|
257
|
+
|
|
258
|
+
# For each tuple, extract deltas for all points
|
|
259
|
+
tuples.each do |tuple|
|
|
260
|
+
deltas = parse_tuple_deltas(tuple, point_count)
|
|
261
|
+
|
|
262
|
+
deltas.each_with_index do |delta, point_index|
|
|
263
|
+
point_deltas[point_index] << delta
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
point_deltas
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Parse deltas from a tuple
|
|
271
|
+
#
|
|
272
|
+
# @param tuple [Hash] Tuple data
|
|
273
|
+
# @param point_count [Integer] Number of points
|
|
274
|
+
# @return [Array<Hash>] Deltas with :x and :y
|
|
275
|
+
def parse_tuple_deltas(tuple, point_count)
|
|
276
|
+
# If tuple has deltas array, use it
|
|
277
|
+
if tuple[:deltas].is_a?(Array)
|
|
278
|
+
return tuple[:deltas].map do |delta|
|
|
279
|
+
{ x: delta[:x] || 0, y: delta[:y] || 0 }
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Otherwise return zeros (placeholder for parsing raw delta data)
|
|
284
|
+
# Full implementation would:
|
|
285
|
+
# 1. Parse delta data from tuple[:data]
|
|
286
|
+
# 2. Decompress if needed
|
|
287
|
+
# 3. Return array of { x: dx, y: dy } for each point
|
|
288
|
+
Array.new(point_count) { { x: 0, y: 0 } }
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Convert blend data to tuple format
|
|
292
|
+
#
|
|
293
|
+
# @param blend_data [Hash] Blend format data
|
|
294
|
+
# @return [Hash] Tuple variation data
|
|
295
|
+
def convert_blend_to_tuples(blend_data)
|
|
296
|
+
regions = blend_data[:regions] || []
|
|
297
|
+
point_deltas = blend_data[:point_deltas] || []
|
|
298
|
+
|
|
299
|
+
# Build tuples from regions
|
|
300
|
+
tuples = regions.map.with_index do |region, region_index|
|
|
301
|
+
build_tuple_from_region(region, point_deltas, region_index)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
{
|
|
305
|
+
tuples: tuples,
|
|
306
|
+
point_count: point_deltas.length,
|
|
307
|
+
}
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Build tuple from region and deltas
|
|
311
|
+
#
|
|
312
|
+
# @param region [Hash] Region definition
|
|
313
|
+
# @param point_deltas [Array<Array<Hash>>] Deltas per point
|
|
314
|
+
# @param region_index [Integer] Region index
|
|
315
|
+
# @return [Hash] Tuple data
|
|
316
|
+
def build_tuple_from_region(region, point_deltas, region_index)
|
|
317
|
+
# Extract peak, start, end for all axes
|
|
318
|
+
peak = Array.new(@axes.length, 0.0)
|
|
319
|
+
start_vals = Array.new(@axes.length, -1.0)
|
|
320
|
+
end_vals = Array.new(@axes.length, 1.0)
|
|
321
|
+
|
|
322
|
+
@axes.each_with_index do |axis, axis_index|
|
|
323
|
+
axis_region = region[axis.axis_tag]
|
|
324
|
+
next unless axis_region
|
|
325
|
+
|
|
326
|
+
peak[axis_index] = axis_region[:peak]
|
|
327
|
+
start_vals[axis_index] = axis_region[:start]
|
|
328
|
+
end_vals[axis_index] = axis_region[:end]
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Extract deltas for this region
|
|
332
|
+
deltas = point_deltas.map do |point_delta_set|
|
|
333
|
+
point_delta_set[region_index] || { x: 0, y: 0 }
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
{
|
|
337
|
+
peak: peak,
|
|
338
|
+
start: start_vals,
|
|
339
|
+
end: end_vals,
|
|
340
|
+
deltas: deltas,
|
|
341
|
+
}
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Encode deltas in CharString blend format
|
|
345
|
+
#
|
|
346
|
+
# @param base_value [Numeric] Base value
|
|
347
|
+
# @param deltas [Array<Numeric>] Delta values
|
|
348
|
+
# @return [Array<Numeric>] Blend operator arguments
|
|
349
|
+
def encode_blend_operator(base_value, deltas)
|
|
350
|
+
# CFF2 blend format: base_value delta1 delta2 ... K N blend
|
|
351
|
+
# Where K = number of deltas, N = number of blend operations
|
|
352
|
+
[base_value] + deltas + [deltas.length, 1]
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Decode blend operator arguments to base and deltas
|
|
356
|
+
#
|
|
357
|
+
# @param args [Array<Numeric>] Blend operator arguments
|
|
358
|
+
# @return [Hash] Base value and deltas
|
|
359
|
+
def decode_blend_operator(args)
|
|
360
|
+
return { base: 0, deltas: [] } if args.length < 3
|
|
361
|
+
|
|
362
|
+
# Last two values are K and N
|
|
363
|
+
k = args[-2]
|
|
364
|
+
_n = args[-1]
|
|
365
|
+
|
|
366
|
+
# Before K and N: base + deltas
|
|
367
|
+
values = args[0...-2]
|
|
368
|
+
base = values[0] || 0
|
|
369
|
+
deltas = values[1, k] || []
|
|
370
|
+
|
|
371
|
+
{ base: base, deltas: deltas }
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
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
|