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,418 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../optimizers/pattern_analyzer"
|
|
4
|
+
require_relative "../optimizers/subroutine_optimizer"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Variation
|
|
8
|
+
# Optimizes CFF subroutines for variable fonts
|
|
9
|
+
#
|
|
10
|
+
# This class analyzes CharStrings in CFF2 variable fonts and optimizes
|
|
11
|
+
# blend operations by extracting common blend sequences into subroutines,
|
|
12
|
+
# deduplicating variation regions, and minimizing ItemVariationStore data.
|
|
13
|
+
#
|
|
14
|
+
# Optimization strategies:
|
|
15
|
+
# 1. Blend pattern extraction - Find repeating blend sequences
|
|
16
|
+
# 2. Region deduplication - Merge identical variation regions
|
|
17
|
+
# 3. ItemVariationStore optimization - Compact delta storage
|
|
18
|
+
# 4. Subroutine reordering - Place frequent blends in low IDs
|
|
19
|
+
#
|
|
20
|
+
# @example Optimizing a variable font
|
|
21
|
+
# optimizer = Fontisan::Variation::Optimizer.new(cff2_table)
|
|
22
|
+
# optimized = optimizer.optimize
|
|
23
|
+
# # => Optimized CFF2 table with reduced file size
|
|
24
|
+
#
|
|
25
|
+
# @see docs/SUBROUTINE_ARCHITECTURE.md
|
|
26
|
+
# @see docs/CFF2_ARCHITECTURE.md
|
|
27
|
+
class Optimizer
|
|
28
|
+
# @return [CFF2] CFF2 table being optimized
|
|
29
|
+
attr_reader :cff2
|
|
30
|
+
|
|
31
|
+
# @return [Hash] Optimization statistics
|
|
32
|
+
attr_reader :stats
|
|
33
|
+
|
|
34
|
+
# Initialize optimizer
|
|
35
|
+
#
|
|
36
|
+
# @param cff2 [CFF2] CFF2 table with blend operators
|
|
37
|
+
# @param options [Hash] Optimization options
|
|
38
|
+
# @option options [Integer] :max_subrs Maximum subroutines (default: 65535)
|
|
39
|
+
# @option options [Float] :region_threshold Region similarity threshold (default: 0.001)
|
|
40
|
+
# @option options [Boolean] :deduplicate_regions Enable region deduplication (default: true)
|
|
41
|
+
def initialize(cff2, options = {})
|
|
42
|
+
@cff2 = cff2
|
|
43
|
+
@options = {
|
|
44
|
+
max_subrs: 65535,
|
|
45
|
+
region_threshold: 0.001,
|
|
46
|
+
deduplicate_regions: true,
|
|
47
|
+
}.merge(options)
|
|
48
|
+
|
|
49
|
+
@stats = {
|
|
50
|
+
original_size: 0,
|
|
51
|
+
optimized_size: 0,
|
|
52
|
+
blend_patterns_found: 0,
|
|
53
|
+
subroutines_created: 0,
|
|
54
|
+
regions_deduplicated: 0,
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Optimize CFF2 table
|
|
59
|
+
#
|
|
60
|
+
# Performs all optimization passes and returns optimized table.
|
|
61
|
+
#
|
|
62
|
+
# @return [CFF2] Optimized CFF2 table
|
|
63
|
+
def optimize
|
|
64
|
+
@stats[:original_size] = estimate_table_size(@cff2)
|
|
65
|
+
|
|
66
|
+
# Step 1: Analyze blend patterns across all CharStrings
|
|
67
|
+
blend_patterns = analyze_blend_patterns
|
|
68
|
+
|
|
69
|
+
# Step 2: Extract common blend sequences into subroutines
|
|
70
|
+
subroutines = extract_blend_subroutines(blend_patterns)
|
|
71
|
+
|
|
72
|
+
# Step 3: Deduplicate variation regions
|
|
73
|
+
deduplicate_regions if @options[:deduplicate_regions]
|
|
74
|
+
|
|
75
|
+
# Step 4: Optimize ItemVariationStore
|
|
76
|
+
optimize_item_variation_store
|
|
77
|
+
|
|
78
|
+
# Step 5: Rebuild CharStrings with subroutine calls
|
|
79
|
+
rebuild_charstrings(subroutines)
|
|
80
|
+
|
|
81
|
+
@stats[:optimized_size] = estimate_table_size(@cff2)
|
|
82
|
+
@stats[:savings_percent] = calculate_savings_percent
|
|
83
|
+
|
|
84
|
+
@cff2
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Analyze blend patterns in CharStrings
|
|
88
|
+
#
|
|
89
|
+
# Scans all CharStrings to find repeating blend operator sequences
|
|
90
|
+
# that can be extracted into subroutines.
|
|
91
|
+
#
|
|
92
|
+
# @return [Array<BlendPattern>] Identified blend patterns
|
|
93
|
+
def analyze_blend_patterns
|
|
94
|
+
patterns = []
|
|
95
|
+
glyph_count = @cff2.glyph_count
|
|
96
|
+
|
|
97
|
+
glyph_count.times do |glyph_id|
|
|
98
|
+
charstring = @cff2.charstring(glyph_id)
|
|
99
|
+
next unless charstring
|
|
100
|
+
|
|
101
|
+
# Extract blend operator sequences
|
|
102
|
+
blend_sequences = extract_blend_sequences(charstring)
|
|
103
|
+
patterns.concat(blend_sequences)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Group identical patterns
|
|
107
|
+
grouped = group_patterns(patterns)
|
|
108
|
+
@stats[:blend_patterns_found] = grouped.length
|
|
109
|
+
|
|
110
|
+
grouped
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Extract blend sequences from CharString
|
|
114
|
+
#
|
|
115
|
+
# @param charstring [String] Binary CharString data
|
|
116
|
+
# @return [Array<BlendPattern>] Blend patterns found
|
|
117
|
+
def extract_blend_sequences(charstring)
|
|
118
|
+
patterns = []
|
|
119
|
+
operators = parse_charstring_operators(charstring)
|
|
120
|
+
|
|
121
|
+
operators.each_with_index do |op, index|
|
|
122
|
+
next unless blend_operator?(op)
|
|
123
|
+
|
|
124
|
+
# Extract blend and surrounding context
|
|
125
|
+
pattern = extract_pattern_context(operators, index)
|
|
126
|
+
patterns << pattern if pattern
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
patterns
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Extract common blend sequences into subroutines
|
|
133
|
+
#
|
|
134
|
+
# @param patterns [Array<BlendPattern>] Blend patterns
|
|
135
|
+
# @return [Array<Subroutine>] Created subroutines
|
|
136
|
+
def extract_blend_subroutines(patterns)
|
|
137
|
+
# Filter patterns by frequency and savings
|
|
138
|
+
candidates = patterns.select do |pattern|
|
|
139
|
+
pattern[:frequency] >= 2 && pattern[:savings].positive?
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Convert patterns to format expected by SubroutineOptimizer
|
|
143
|
+
# The optimizer expects objects with methods, but we have hashes
|
|
144
|
+
# For now, just select and order them directly
|
|
145
|
+
selected = candidates.sort_by { |p| -p[:savings] }
|
|
146
|
+
.take(@options[:max_subrs])
|
|
147
|
+
|
|
148
|
+
# Order by frequency for efficient encoding
|
|
149
|
+
ordered = selected.sort_by { |p| -p[:frequency] }
|
|
150
|
+
|
|
151
|
+
@stats[:subroutines_created] = ordered.length
|
|
152
|
+
ordered
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Deduplicate variation regions
|
|
156
|
+
#
|
|
157
|
+
# Merges regions that are functionally identical (within threshold).
|
|
158
|
+
def deduplicate_regions
|
|
159
|
+
return unless @cff2.variation_store
|
|
160
|
+
|
|
161
|
+
regions = @cff2.variation_store.region_list
|
|
162
|
+
original_count = regions.length
|
|
163
|
+
|
|
164
|
+
# Find duplicate regions
|
|
165
|
+
unique_regions = []
|
|
166
|
+
region_mapping = {}
|
|
167
|
+
|
|
168
|
+
regions.each_with_index do |region, index|
|
|
169
|
+
# Check if region matches any existing unique region
|
|
170
|
+
match_index = find_matching_region(region, unique_regions)
|
|
171
|
+
|
|
172
|
+
if match_index
|
|
173
|
+
region_mapping[index] = match_index
|
|
174
|
+
else
|
|
175
|
+
region_mapping[index] = unique_regions.length
|
|
176
|
+
unique_regions << region
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Update references in ItemVariationStore
|
|
181
|
+
update_region_references(region_mapping) if regions.length > unique_regions.length
|
|
182
|
+
|
|
183
|
+
@cff2.variation_store.region_list = unique_regions
|
|
184
|
+
@stats[:regions_deduplicated] = original_count - unique_regions.length
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Find matching region within threshold
|
|
188
|
+
#
|
|
189
|
+
# @param region [RegionAxisCoordinates] Region to match
|
|
190
|
+
# @param unique_regions [Array<RegionAxisCoordinates>] Existing unique regions
|
|
191
|
+
# @return [Integer, nil] Index of matching region or nil
|
|
192
|
+
def find_matching_region(region, unique_regions)
|
|
193
|
+
unique_regions.each_with_index do |unique, index|
|
|
194
|
+
return index if regions_match?(region, unique)
|
|
195
|
+
end
|
|
196
|
+
nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Check if two regions match within threshold
|
|
200
|
+
#
|
|
201
|
+
# @param r1 [RegionAxisCoordinates] First region
|
|
202
|
+
# @param r2 [RegionAxisCoordinates] Second region
|
|
203
|
+
# @return [Boolean] True if regions match
|
|
204
|
+
def regions_match?(r1, r2)
|
|
205
|
+
return false unless r1.axis_count == r2.axis_count
|
|
206
|
+
|
|
207
|
+
r1.axis_count.times do |i|
|
|
208
|
+
coords1 = r1.region_axes[i]
|
|
209
|
+
coords2 = r2.region_axes[i]
|
|
210
|
+
|
|
211
|
+
# Compare start, peak, end coordinates
|
|
212
|
+
return false unless coords_similar?(coords1.start_coord, coords2.start_coord)
|
|
213
|
+
return false unless coords_similar?(coords1.peak_coord, coords2.peak_coord)
|
|
214
|
+
return false unless coords_similar?(coords1.end_coord, coords2.end_coord)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
true
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Check if coordinates are similar within threshold
|
|
221
|
+
#
|
|
222
|
+
# @param c1 [Float] First coordinate
|
|
223
|
+
# @param c2 [Float] Second coordinate
|
|
224
|
+
# @return [Boolean] True if similar
|
|
225
|
+
def coords_similar?(c1, c2)
|
|
226
|
+
(c1 - c2).abs <= @options[:region_threshold]
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Optimize ItemVariationStore
|
|
230
|
+
#
|
|
231
|
+
# Compacts delta storage by removing unused data and optimizing encoding.
|
|
232
|
+
def optimize_item_variation_store
|
|
233
|
+
return unless @cff2.variation_store
|
|
234
|
+
|
|
235
|
+
store = @cff2.variation_store
|
|
236
|
+
|
|
237
|
+
# Remove unused variation data
|
|
238
|
+
compact_variation_data(store)
|
|
239
|
+
|
|
240
|
+
# Optimize delta encoding (use shortest representation)
|
|
241
|
+
optimize_delta_encoding(store)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Compact variation data by removing unused entries
|
|
245
|
+
#
|
|
246
|
+
# @param store [ItemVariationStore] Variation store
|
|
247
|
+
def compact_variation_data(store)
|
|
248
|
+
# Identify used variation indices from CharStrings
|
|
249
|
+
used_indices = collect_used_variation_indices
|
|
250
|
+
|
|
251
|
+
# Remove unused data
|
|
252
|
+
store.item_variation_data.each do |data|
|
|
253
|
+
data.compact_unused(used_indices)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Optimize delta encoding for efficiency
|
|
258
|
+
#
|
|
259
|
+
# @param store [ItemVariationStore] Variation store
|
|
260
|
+
def optimize_delta_encoding(store)
|
|
261
|
+
store.item_variation_data.each(&:optimize_encoding)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Rebuild CharStrings with subroutine calls
|
|
265
|
+
#
|
|
266
|
+
# @param subroutines [Array<Subroutine>] Subroutines to use
|
|
267
|
+
def rebuild_charstrings(subroutines)
|
|
268
|
+
return if subroutines.empty?
|
|
269
|
+
|
|
270
|
+
glyph_count = @cff2.glyph_count
|
|
271
|
+
|
|
272
|
+
glyph_count.times do |glyph_id|
|
|
273
|
+
charstring = @cff2.charstring(glyph_id)
|
|
274
|
+
next unless charstring
|
|
275
|
+
|
|
276
|
+
# Rewrite CharString to use subroutines
|
|
277
|
+
optimized = rewrite_with_subroutines(charstring, subroutines)
|
|
278
|
+
@cff2.set_charstring(glyph_id, optimized)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Update subroutine index in CFF2
|
|
282
|
+
@cff2.local_subr_index = subroutines
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Get optimization statistics
|
|
286
|
+
#
|
|
287
|
+
# @return [Hash] Statistics about optimization
|
|
288
|
+
def statistics
|
|
289
|
+
@stats
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
private
|
|
293
|
+
|
|
294
|
+
# Parse CharString to operators
|
|
295
|
+
#
|
|
296
|
+
# @param charstring [String] Binary CharString data
|
|
297
|
+
# @return [Array<Hash>] Operators with operands
|
|
298
|
+
def parse_charstring_operators(_charstring)
|
|
299
|
+
# Placeholder - would parse binary CharString format
|
|
300
|
+
# Returns array of { operator:, operands:, position: }
|
|
301
|
+
[]
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Check if operator is a blend operator
|
|
305
|
+
#
|
|
306
|
+
# @param operator [Hash] Operator data
|
|
307
|
+
# @return [Boolean] True if blend operator
|
|
308
|
+
def blend_operator?(operator)
|
|
309
|
+
operator[:operator] == :blend
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Extract pattern with surrounding context
|
|
313
|
+
#
|
|
314
|
+
# @param operators [Array<Hash>] All operators
|
|
315
|
+
# @param blend_index [Integer] Index of blend operator
|
|
316
|
+
# @return [Hash, nil] Pattern data
|
|
317
|
+
def extract_pattern_context(_operators, _blend_index)
|
|
318
|
+
# Extract blend and preceding operands
|
|
319
|
+
# Returns { sequence:, frequency:, savings:, positions: }
|
|
320
|
+
nil
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Group identical patterns
|
|
324
|
+
#
|
|
325
|
+
# @param patterns [Array<BlendPattern>] Raw patterns
|
|
326
|
+
# @return [Array<BlendPattern>] Grouped patterns with frequency
|
|
327
|
+
def group_patterns(patterns)
|
|
328
|
+
grouped = {}
|
|
329
|
+
|
|
330
|
+
patterns.each do |pattern|
|
|
331
|
+
key = pattern_key(pattern)
|
|
332
|
+
grouped[key] ||= pattern.dup
|
|
333
|
+
grouped[key][:frequency] ||= 0
|
|
334
|
+
grouped[key][:frequency] += 1
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
grouped.values
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Generate key for pattern grouping
|
|
341
|
+
#
|
|
342
|
+
# @param pattern [Hash] Pattern data
|
|
343
|
+
# @return [String] Unique key
|
|
344
|
+
def pattern_key(pattern)
|
|
345
|
+
pattern[:sequence].join(",")
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Collect variation indices used in CharStrings
|
|
349
|
+
#
|
|
350
|
+
# @return [Set<Integer>] Set of used indices
|
|
351
|
+
def collect_used_variation_indices
|
|
352
|
+
require "set"
|
|
353
|
+
used = Set.new
|
|
354
|
+
|
|
355
|
+
glyph_count = @cff2.glyph_count
|
|
356
|
+
glyph_count.times do |glyph_id|
|
|
357
|
+
charstring = @cff2.charstring(glyph_id)
|
|
358
|
+
next unless charstring
|
|
359
|
+
|
|
360
|
+
# Extract variation indices from CharString
|
|
361
|
+
indices = extract_variation_indices(charstring)
|
|
362
|
+
used.merge(indices)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
used
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Extract variation indices from CharString
|
|
369
|
+
#
|
|
370
|
+
# @param charstring [String] Binary CharString
|
|
371
|
+
# @return [Array<Integer>] Variation indices
|
|
372
|
+
def extract_variation_indices(_charstring)
|
|
373
|
+
# Placeholder - would parse vsindex operators
|
|
374
|
+
[]
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Update region references after deduplication
|
|
378
|
+
#
|
|
379
|
+
# @param mapping [Hash<Integer, Integer>] Old index => new index
|
|
380
|
+
def update_region_references(mapping)
|
|
381
|
+
store = @cff2.variation_store
|
|
382
|
+
|
|
383
|
+
store.item_variation_data.each do |data|
|
|
384
|
+
data.update_region_indices(mapping)
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Rewrite CharString with subroutine calls
|
|
389
|
+
#
|
|
390
|
+
# @param charstring [String] Original CharString
|
|
391
|
+
# @param subroutines [Array<Subroutine>] Available subroutines
|
|
392
|
+
# @return [String] Optimized CharString
|
|
393
|
+
def rewrite_with_subroutines(charstring, _subroutines)
|
|
394
|
+
# Placeholder - would replace patterns with callsubr operators
|
|
395
|
+
charstring
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Estimate table size in bytes
|
|
399
|
+
#
|
|
400
|
+
# @param cff2 [CFF2] CFF2 table
|
|
401
|
+
# @return [Integer] Estimated size
|
|
402
|
+
def estimate_table_size(_cff2)
|
|
403
|
+
# Placeholder - would calculate actual binary size
|
|
404
|
+
0
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Calculate savings percentage
|
|
408
|
+
#
|
|
409
|
+
# @return [Float] Percentage saved
|
|
410
|
+
def calculate_savings_percent
|
|
411
|
+
return 0.0 if @stats[:original_size].zero?
|
|
412
|
+
|
|
413
|
+
saved = @stats[:original_size] - @stats[:optimized_size]
|
|
414
|
+
(saved.to_f / @stats[:original_size]) * 100.0
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "instance_generator"
|
|
4
|
+
require_relative "cache"
|
|
5
|
+
require_relative "../utils/thread_pool"
|
|
6
|
+
require "etc"
|
|
7
|
+
|
|
8
|
+
module Fontisan
|
|
9
|
+
module Variation
|
|
10
|
+
# Generates multiple font instances in parallel
|
|
11
|
+
#
|
|
12
|
+
# Uses thread pool for efficient batch processing with caching.
|
|
13
|
+
# Supports progress tracking and graceful error handling per instance.
|
|
14
|
+
#
|
|
15
|
+
# @example Basic batch generation
|
|
16
|
+
# generator = ParallelGenerator.new(font)
|
|
17
|
+
# coordinates_list = [
|
|
18
|
+
# { "wght" => 300 },
|
|
19
|
+
# { "wght" => 700 }
|
|
20
|
+
# ]
|
|
21
|
+
# instances = generator.generate_batch(coordinates_list)
|
|
22
|
+
#
|
|
23
|
+
# @example With progress callback
|
|
24
|
+
# generator.generate_batch(coordinates_list) do |index, total|
|
|
25
|
+
# puts "Generated #{index}/#{total}"
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# @example Custom thread count
|
|
29
|
+
# generator = ParallelGenerator.new(font, threads: 8)
|
|
30
|
+
class ParallelGenerator
|
|
31
|
+
# @return [TrueTypeFont, OpenTypeFont] Variable font
|
|
32
|
+
attr_reader :font
|
|
33
|
+
|
|
34
|
+
# @return [ThreadSafeCache] Thread-safe cache
|
|
35
|
+
attr_reader :cache
|
|
36
|
+
|
|
37
|
+
# @return [Integer] Number of threads
|
|
38
|
+
attr_reader :thread_count
|
|
39
|
+
|
|
40
|
+
# Initialize parallel generator
|
|
41
|
+
#
|
|
42
|
+
# @param font [TrueTypeFont, OpenTypeFont] Variable font
|
|
43
|
+
# @param options [Hash] Options
|
|
44
|
+
# @option options [ThreadSafeCache] :cache Cache instance (creates new if not provided)
|
|
45
|
+
# @option options [Integer] :threads Thread count (default: max(4, processor_count))
|
|
46
|
+
def initialize(font, options = {})
|
|
47
|
+
@font = font
|
|
48
|
+
@cache = options[:cache] || ThreadSafeCache.new
|
|
49
|
+
@thread_count = options[:threads] || [4, Etc.nprocessors].max
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Generate multiple instances in parallel
|
|
53
|
+
#
|
|
54
|
+
# Processes each coordinate set in parallel using thread pool.
|
|
55
|
+
# Returns results in same order as input coordinates.
|
|
56
|
+
#
|
|
57
|
+
# @param coordinates_list [Array<Hash>] List of coordinate sets
|
|
58
|
+
# @yield [index, total] Progress callback (optional)
|
|
59
|
+
# @yieldparam index [Integer] Current completed count
|
|
60
|
+
# @yieldparam total [Integer] Total count
|
|
61
|
+
# @return [Array<Hash>] Generated instances with metadata
|
|
62
|
+
def generate_batch(coordinates_list, &progress_callback)
|
|
63
|
+
return [] if coordinates_list.empty?
|
|
64
|
+
|
|
65
|
+
total = coordinates_list.length
|
|
66
|
+
results = Array.new(total)
|
|
67
|
+
completed = 0
|
|
68
|
+
mutex = Mutex.new
|
|
69
|
+
|
|
70
|
+
# Create thread pool
|
|
71
|
+
pool = Fontisan::Utils::ThreadPool.new(@thread_count)
|
|
72
|
+
|
|
73
|
+
# Schedule all jobs
|
|
74
|
+
futures = coordinates_list.map.with_index do |coordinates, index|
|
|
75
|
+
pool.schedule do
|
|
76
|
+
{
|
|
77
|
+
index: index,
|
|
78
|
+
result: generate_with_cache(coordinates),
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Collect results
|
|
84
|
+
futures.each do |future|
|
|
85
|
+
job_result = future.value
|
|
86
|
+
results[job_result[:index]] = job_result[:result]
|
|
87
|
+
|
|
88
|
+
# Update progress
|
|
89
|
+
if progress_callback
|
|
90
|
+
mutex.synchronize do
|
|
91
|
+
completed += 1
|
|
92
|
+
yield(completed, total)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Shutdown pool
|
|
98
|
+
pool.shutdown
|
|
99
|
+
|
|
100
|
+
results
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Generate instance with caching
|
|
104
|
+
#
|
|
105
|
+
# Uses cache to avoid regenerating identical instances.
|
|
106
|
+
#
|
|
107
|
+
# @param coordinates [Hash<String, Float>] Design space coordinates
|
|
108
|
+
# @return [Hash] Instance data with metadata
|
|
109
|
+
def generate_with_cache(coordinates)
|
|
110
|
+
font_checksum = calculate_font_checksum
|
|
111
|
+
|
|
112
|
+
begin
|
|
113
|
+
tables = @cache.fetch_instance(font_checksum, coordinates) do
|
|
114
|
+
generator = InstanceGenerator.new(@font, coordinates)
|
|
115
|
+
generator.generate
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
{
|
|
119
|
+
success: true,
|
|
120
|
+
coordinates: coordinates,
|
|
121
|
+
tables: tables,
|
|
122
|
+
error: nil,
|
|
123
|
+
}
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
{
|
|
126
|
+
success: false,
|
|
127
|
+
coordinates: coordinates,
|
|
128
|
+
tables: nil,
|
|
129
|
+
error: {
|
|
130
|
+
message: e.message,
|
|
131
|
+
class: e.class.name,
|
|
132
|
+
backtrace: e.backtrace&.first(5),
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
# Calculate font checksum for cache key
|
|
141
|
+
#
|
|
142
|
+
# @return [String] Font identifier
|
|
143
|
+
def calculate_font_checksum
|
|
144
|
+
# Use combination of table checksums for quick identification
|
|
145
|
+
# In production, might use actual checksum from head table
|
|
146
|
+
"font_#{@font.object_id}"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|