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,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Optimizers
|
|
5
|
+
# Main orchestrator for CFF subroutine generation pipeline.
|
|
6
|
+
# Coordinates PatternAnalyzer, SubroutineOptimizer, SubroutineBuilder,
|
|
7
|
+
# and CharstringRewriter to generate optimized subroutines for fonts.
|
|
8
|
+
#
|
|
9
|
+
# The generator processes CharStrings from a CFF font table by:
|
|
10
|
+
# 1. Analyzing patterns across all glyphs
|
|
11
|
+
# 2. Selecting optimal patterns (avoiding conflicts, within limits)
|
|
12
|
+
# 3. Ordering patterns by frequency for efficient encoding
|
|
13
|
+
# 4. Building actual subroutine CharStrings
|
|
14
|
+
# 5. Rewriting original CharStrings with subroutine calls
|
|
15
|
+
#
|
|
16
|
+
# @example Basic usage
|
|
17
|
+
# generator = SubroutineGenerator.new(min_pattern_length: 10)
|
|
18
|
+
# result = generator.generate(font)
|
|
19
|
+
# puts "Generated #{result[:selected_count]} subroutines"
|
|
20
|
+
# puts "Total savings: #{result[:savings]} bytes"
|
|
21
|
+
#
|
|
22
|
+
# @see docs/SUBROUTINE_ARCHITECTURE.md
|
|
23
|
+
class SubroutineGenerator
|
|
24
|
+
# Default minimum pattern length in bytes
|
|
25
|
+
DEFAULT_MIN_PATTERN_LENGTH = 10
|
|
26
|
+
|
|
27
|
+
# Default maximum number of subroutines (CFF limit)
|
|
28
|
+
DEFAULT_MAX_SUBROUTINES = 65_535
|
|
29
|
+
|
|
30
|
+
# Initialize generator with options
|
|
31
|
+
# @param options [Hash] configuration options
|
|
32
|
+
# @option options [Integer] :min_pattern_length (10) minimum pattern size
|
|
33
|
+
# @option options [Integer] :max_subroutines (65535) max subroutine count
|
|
34
|
+
# @option options [Boolean] :optimize_ordering (true) enable frequency
|
|
35
|
+
# ordering
|
|
36
|
+
def initialize(options = {})
|
|
37
|
+
@min_pattern_length = options[:min_pattern_length] ||
|
|
38
|
+
DEFAULT_MIN_PATTERN_LENGTH
|
|
39
|
+
@max_subroutines = options[:max_subroutines] || DEFAULT_MAX_SUBROUTINES
|
|
40
|
+
@optimize_ordering = options[:optimize_ordering] != false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Generate subroutines for a font
|
|
44
|
+
#
|
|
45
|
+
# Main entry point for the subroutine generation pipeline. Processes
|
|
46
|
+
# a font's CFF table to create optimized subroutines and rewrite
|
|
47
|
+
# CharStrings.
|
|
48
|
+
#
|
|
49
|
+
# @param font [Fontisan::OpenTypeFont] font to optimize
|
|
50
|
+
# @return [Hash] result containing:
|
|
51
|
+
# - :local_subrs [Array<String>] subroutine CharStrings
|
|
52
|
+
# - :charstrings [Hash<Integer, String>] rewritten CharStrings
|
|
53
|
+
# - :bias [Integer] CFF bias value for subroutines
|
|
54
|
+
# - :savings [Integer] total bytes saved
|
|
55
|
+
# - :pattern_count [Integer] total patterns found
|
|
56
|
+
# - :selected_count [Integer] patterns selected as subroutines
|
|
57
|
+
# @raise [ArgumentError] if font has no CFF table
|
|
58
|
+
def generate(font)
|
|
59
|
+
# 1. Extract CharStrings from CFF table
|
|
60
|
+
charstrings = extract_charstrings(font)
|
|
61
|
+
|
|
62
|
+
# Handle empty font gracefully
|
|
63
|
+
if charstrings.empty?
|
|
64
|
+
return {
|
|
65
|
+
local_subrs: [],
|
|
66
|
+
charstrings: {},
|
|
67
|
+
bias: 0,
|
|
68
|
+
savings: 0,
|
|
69
|
+
pattern_count: 0,
|
|
70
|
+
selected_count: 0,
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# 2. Analyze patterns
|
|
75
|
+
analyzer = PatternAnalyzer.new(
|
|
76
|
+
min_length: @min_pattern_length,
|
|
77
|
+
stack_aware: true
|
|
78
|
+
)
|
|
79
|
+
patterns = analyzer.analyze(charstrings)
|
|
80
|
+
|
|
81
|
+
# 3. Optimize selection
|
|
82
|
+
optimizer = SubroutineOptimizer.new(patterns,
|
|
83
|
+
max_subrs: @max_subroutines)
|
|
84
|
+
selected_patterns = optimizer.optimize_selection
|
|
85
|
+
|
|
86
|
+
# 4. Optimize ordering (if enabled)
|
|
87
|
+
if @optimize_ordering
|
|
88
|
+
selected_patterns = optimizer.optimize_ordering(selected_patterns)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# 5. Build subroutines
|
|
92
|
+
builder = SubroutineBuilder.new(selected_patterns, type: :local)
|
|
93
|
+
subroutines = builder.build
|
|
94
|
+
|
|
95
|
+
# 6. Build subroutine map
|
|
96
|
+
subroutine_map = build_subroutine_map(selected_patterns)
|
|
97
|
+
|
|
98
|
+
# 7. Rewrite CharStrings
|
|
99
|
+
rewriter = CharstringRewriter.new(subroutine_map, builder)
|
|
100
|
+
rewritten_charstrings = rewrite_charstrings(
|
|
101
|
+
charstrings,
|
|
102
|
+
selected_patterns,
|
|
103
|
+
rewriter,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# 8. Return complete result
|
|
107
|
+
{
|
|
108
|
+
local_subrs: subroutines,
|
|
109
|
+
charstrings: rewritten_charstrings,
|
|
110
|
+
bias: builder.bias,
|
|
111
|
+
savings: calculate_total_savings(selected_patterns),
|
|
112
|
+
pattern_count: patterns.length,
|
|
113
|
+
selected_count: selected_patterns.length,
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
# Extract CharStrings from CFF table
|
|
120
|
+
#
|
|
121
|
+
# Retrieves raw CharString byte sequences for each glyph from the
|
|
122
|
+
# font's CFF table. The CharStrings INDEX is accessed through the
|
|
123
|
+
# CFF table structure.
|
|
124
|
+
#
|
|
125
|
+
# @param font [Fontisan::OpenTypeFont] font to extract from
|
|
126
|
+
# @return [Hash<Integer, String>] glyph_id => charstring_bytes
|
|
127
|
+
# @raise [ArgumentError] if font has no CFF table
|
|
128
|
+
def extract_charstrings(font)
|
|
129
|
+
cff = font.table("CFF ")
|
|
130
|
+
raise ArgumentError, "Font must have CFF table" unless cff
|
|
131
|
+
|
|
132
|
+
charstrings = {}
|
|
133
|
+
|
|
134
|
+
# Get CharStrings INDEX for first font (index 0)
|
|
135
|
+
charstrings_index = cff.charstrings_index(0)
|
|
136
|
+
unless charstrings_index
|
|
137
|
+
raise ArgumentError, "Font CFF table has no CharStrings"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Extract raw CharString bytes for each glyph
|
|
141
|
+
# Index.each yields raw bytes, we add index manually
|
|
142
|
+
index = 0
|
|
143
|
+
charstrings_index.each do |cs_data|
|
|
144
|
+
charstrings[index] = cs_data
|
|
145
|
+
index += 1
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
charstrings
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Build map from pattern bytes to subroutine ID
|
|
152
|
+
#
|
|
153
|
+
# Creates a lookup table for the rewriter to quickly find which
|
|
154
|
+
# subroutine ID corresponds to each pattern's byte sequence.
|
|
155
|
+
#
|
|
156
|
+
# @param patterns [Array<Pattern>] selected patterns
|
|
157
|
+
# @return [Hash<String, Integer>] pattern_bytes => subroutine_id
|
|
158
|
+
def build_subroutine_map(patterns)
|
|
159
|
+
map = {}
|
|
160
|
+
patterns.each_with_index do |pattern, index|
|
|
161
|
+
map[pattern.bytes] = index
|
|
162
|
+
end
|
|
163
|
+
map
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Rewrite all CharStrings with subroutine calls
|
|
167
|
+
#
|
|
168
|
+
# Processes each glyph's CharString, replacing pattern occurrences
|
|
169
|
+
# with calls to their corresponding subroutines. Glyphs without
|
|
170
|
+
# applicable patterns are kept unchanged.
|
|
171
|
+
#
|
|
172
|
+
# @param charstrings [Hash<Integer, String>] original CharStrings
|
|
173
|
+
# @param patterns [Array<Pattern>] patterns to use
|
|
174
|
+
# @param rewriter [CharstringRewriter] rewriter instance
|
|
175
|
+
# @return [Hash<Integer, String>] rewritten CharStrings
|
|
176
|
+
def rewrite_charstrings(charstrings, patterns, rewriter)
|
|
177
|
+
rewritten = {}
|
|
178
|
+
|
|
179
|
+
charstrings.each do |glyph_id, charstring|
|
|
180
|
+
# Find patterns for this glyph
|
|
181
|
+
glyph_patterns = patterns.select { |p| p.glyphs.include?(glyph_id) }
|
|
182
|
+
|
|
183
|
+
rewritten[glyph_id] = if glyph_patterns.empty?
|
|
184
|
+
# No patterns, keep original
|
|
185
|
+
charstring
|
|
186
|
+
else
|
|
187
|
+
# Rewrite with subroutine calls
|
|
188
|
+
rewriter.rewrite(charstring, glyph_patterns)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
rewritten
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Calculate total byte savings
|
|
196
|
+
#
|
|
197
|
+
# Sums up the savings from all selected patterns to determine
|
|
198
|
+
# total file size reduction achieved by subroutinization.
|
|
199
|
+
#
|
|
200
|
+
# @param patterns [Array<Pattern>] selected patterns
|
|
201
|
+
# @return [Integer] total bytes saved
|
|
202
|
+
def calculate_total_savings(patterns)
|
|
203
|
+
patterns.sum(&:savings)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Optimizers
|
|
5
|
+
# Optimizes subroutine selection and ordering for maximum file size reduction.
|
|
6
|
+
# Uses a greedy algorithm to select the most beneficial patterns while avoiding
|
|
7
|
+
# conflicts, then orders them by frequency for efficient encoding.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage
|
|
10
|
+
# analyzer = PatternAnalyzer.new
|
|
11
|
+
# patterns = analyzer.analyze(charstrings)
|
|
12
|
+
# optimizer = SubroutineOptimizer.new(patterns, max_subrs: 65535)
|
|
13
|
+
# selected_patterns = optimizer.optimize_selection
|
|
14
|
+
# ordered_patterns = optimizer.optimize_ordering(selected_patterns)
|
|
15
|
+
#
|
|
16
|
+
# @see docs/SUBROUTINE_ARCHITECTURE.md
|
|
17
|
+
class SubroutineOptimizer
|
|
18
|
+
# Initialize optimizer with patterns
|
|
19
|
+
# @param patterns [Array<Pattern>] patterns from analyzer
|
|
20
|
+
# @param max_subrs [Integer] maximum number of subroutines (default: 65535)
|
|
21
|
+
def initialize(patterns, max_subrs: 65535)
|
|
22
|
+
@patterns = patterns
|
|
23
|
+
@max_subrs = max_subrs
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Select optimal subset of patterns to subroutinize
|
|
27
|
+
# Uses greedy algorithm: select by highest savings first, checking for
|
|
28
|
+
# conflicts with already selected patterns.
|
|
29
|
+
#
|
|
30
|
+
# @return [Array<Pattern>] selected patterns
|
|
31
|
+
def optimize_selection
|
|
32
|
+
selected = []
|
|
33
|
+
remaining = @patterns.sort_by { |p| -p.savings }
|
|
34
|
+
|
|
35
|
+
remaining.each do |pattern|
|
|
36
|
+
break if selected.length >= @max_subrs
|
|
37
|
+
next if conflicts_with_selected?(pattern, selected)
|
|
38
|
+
|
|
39
|
+
selected << pattern
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
selected
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Optimize subroutine ordering by frequency
|
|
46
|
+
# Higher frequency patterns get lower IDs for more efficient encoding
|
|
47
|
+
# in CFF format.
|
|
48
|
+
#
|
|
49
|
+
# @param subroutines [Array<Pattern>] subroutines to order
|
|
50
|
+
# @return [Array<Pattern>] ordered subroutines
|
|
51
|
+
def optimize_ordering(subroutines)
|
|
52
|
+
# Higher frequency = lower ID (shorter encoding)
|
|
53
|
+
subroutines.sort_by { |subr| -subr.frequency }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Check if nesting would be beneficial
|
|
57
|
+
# TODO: Phase 2.1 - check if subroutines contain common patterns
|
|
58
|
+
#
|
|
59
|
+
# @param subroutines [Array<Pattern>] subroutines to analyze
|
|
60
|
+
# @return [Array<Pattern>] subroutines (unchanged for now)
|
|
61
|
+
def optimize_nesting(subroutines)
|
|
62
|
+
subroutines
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Check if pattern conflicts with any already selected patterns
|
|
68
|
+
# A conflict occurs when patterns overlap in the same glyph at
|
|
69
|
+
# overlapping positions.
|
|
70
|
+
#
|
|
71
|
+
# @param pattern [Pattern] pattern to check
|
|
72
|
+
# @param selected [Array<Pattern>] already selected patterns
|
|
73
|
+
# @return [Boolean] true if conflicts, false otherwise
|
|
74
|
+
def conflicts_with_selected?(pattern, selected)
|
|
75
|
+
selected.any? do |sel|
|
|
76
|
+
# Check if they share any glyphs
|
|
77
|
+
common_glyphs = pattern.glyphs & sel.glyphs
|
|
78
|
+
next false if common_glyphs.empty?
|
|
79
|
+
|
|
80
|
+
# Check if positions overlap in any common glyph
|
|
81
|
+
common_glyphs.any? { |gid| positions_overlap?(pattern, sel, gid) }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Check if two patterns overlap at positions in a specific glyph
|
|
86
|
+
# Ranges overlap if they intersect at any point.
|
|
87
|
+
#
|
|
88
|
+
# @param p1 [Pattern] first pattern
|
|
89
|
+
# @param p2 [Pattern] second pattern
|
|
90
|
+
# @param glyph_id [Integer] glyph to check
|
|
91
|
+
# @return [Boolean] true if positions overlap, false otherwise
|
|
92
|
+
def positions_overlap?(p1, p2, glyph_id)
|
|
93
|
+
pos1 = p1.positions[glyph_id] || []
|
|
94
|
+
pos2 = p2.positions[glyph_id] || []
|
|
95
|
+
|
|
96
|
+
pos1.any? do |start1|
|
|
97
|
+
end1 = start1 + p1.length
|
|
98
|
+
pos2.any? do |start2|
|
|
99
|
+
end2 = start2 + p2.length
|
|
100
|
+
# Check if ranges overlap: start1 < end2 && start2 < end1
|
|
101
|
+
start1 < end2 && start2 < end1
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|