Class: Fontisan::Optimizers::PatternAnalyzer
- Inherits:
-
Object
- Object
- Fontisan::Optimizers::PatternAnalyzer
- Defined in:
- lib/fontisan/optimizers/pattern_analyzer.rb
Overview
Analyzes CharString patterns across glyphs to identify repeated sequences suitable for subroutinization. Implements suffix tree-based pattern matching for efficient detection of repeated byte sequences.
Can optionally use stack-aware detection to ensure patterns are stack-neutral, making them safe for subroutinization without causing stack underflow/overflow.
Defined Under Namespace
Classes: Pattern
Instance Method Summary collapse
-
#analyze(charstrings) ⇒ Array<Pattern>
Analyze CharStrings to find repeated patterns.
-
#build_stack_trackers(charstrings) ⇒ Object
Build stack trackers for all CharStrings (if stack-aware).
-
#calculate_savings ⇒ Object
Calculate byte savings for each pattern.
-
#extract_patterns(charstrings) ⇒ Object
Extract patterns from all CharStrings Uses operator boundaries to ensure patterns are syntactically valid OPTIMIZED: Samples glyphs and uses discrete lengths to avoid O(n³) complexity.
-
#filter_patterns ⇒ Object
Filter patterns by criteria.
-
#find_maximal_patterns ⇒ Object
Find maximal patterns (not contained in larger patterns) TODO: Implement in optimization phase.
-
#find_operator_boundaries(charstring) ⇒ Array<Integer>
Find operator boundaries in CharString Returns positions where operators end, which are valid pattern boundaries.
-
#initialize(min_length: 10, stack_aware: false) ⇒ PatternAnalyzer
constructor
Initialize pattern analyzer.
-
#skip_number(io) ⇒ Object
Skip over a number without decoding Handles all CFF integer encoding formats.
Constructor Details
#initialize(min_length: 10, stack_aware: false) ⇒ PatternAnalyzer
Initialize pattern analyzer
56 57 58 59 60 61 |
# File 'lib/fontisan/optimizers/pattern_analyzer.rb', line 56 def initialize(min_length: 10, stack_aware: false) @min_length = min_length @stack_aware = stack_aware @patterns = {} @stack_trackers = {} # Cache StackTracker instances per glyph end |
Instance Method Details
#analyze(charstrings) ⇒ Array<Pattern>
Analyze CharStrings to find repeated patterns
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/fontisan/optimizers/pattern_analyzer.rb', line 67 def analyze(charstrings) raise ArgumentError, "No CharStrings provided" if charstrings.empty? # Build stack trackers if stack-aware mode enabled build_stack_trackers(charstrings) if @stack_aware # Extract all byte sequences and build pattern candidates extract_patterns(charstrings) # Calculate savings for each pattern calculate_savings # Filter patterns by minimum length and positive savings filter_patterns # Sort by savings (descending) and return @patterns.values.sort_by { |p| -p.savings } end |
#build_stack_trackers(charstrings) ⇒ Object
Build stack trackers for all CharStrings (if stack-aware)
140 141 142 143 144 145 146 |
# File 'lib/fontisan/optimizers/pattern_analyzer.rb', line 140 def build_stack_trackers(charstrings) charstrings.each do |glyph_id, charstring| tracker = StackTracker.new(charstring) tracker.track @stack_trackers[glyph_id] = tracker end end |
#calculate_savings ⇒ Object
Calculate byte savings for each pattern
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 |
# File 'lib/fontisan/optimizers/pattern_analyzer.rb', line 269 def calculate_savings @patterns.each_value do |pattern| # Savings = (pattern_length - overhead) * (frequency - 1) # -1 because we keep one occurrence in a subroutine overhead = pattern.call_overhead savings_per_use = pattern.length - overhead # Total savings across all uses (minus the subroutine definition) pattern.savings = if savings_per_use.positive? savings_per_use * (pattern.frequency - 1) else 0 end end end |
#extract_patterns(charstrings) ⇒ Object
Extract patterns from all CharStrings Uses operator boundaries to ensure patterns are syntactically valid OPTIMIZED: Samples glyphs and uses discrete lengths to avoid O(n³) complexity
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 |
# File 'lib/fontisan/optimizers/pattern_analyzer.rb', line 151 def extract_patterns(charstrings) pattern_occurrences = Hash.new { |h, k| h[k] = [] } # OPTIMIZATION 1: Sample glyphs if there are too many # For large fonts (1000+ glyphs), sample 30% of glyphs sample_size = if charstrings.length > 1000 (charstrings.length * 0.3).to_i else charstrings.length end # Use deterministic selection instead of random sampling # Sort keys first to ensure consistent ordering across platforms sampled_glyphs = charstrings.keys.sort.take(sample_size) # NEW: Pre-compute boundaries for sampled glyphs # Check if boundaries are useful (more than just start position) glyph_boundaries = {} use_boundaries = false sampled_glyphs.each do |glyph_id| boundaries = find_operator_boundaries(charstrings[glyph_id]) glyph_boundaries[glyph_id] = boundaries # If any glyph has meaningful boundaries (more than just [0]), use boundary mode use_boundaries = true if boundaries.length > 2 end # OPTIMIZATION 2: Use discrete pattern lengths instead of continuous range # This reduces iterations from 40 to ~5 pattern_lengths = [@min_length, @min_length + 5, @min_length + 10, @min_length + 15, @min_length + 20] # For each sampled glyph, extract patterns sampled_glyphs.each do |glyph_id| charstring = charstrings[glyph_id] next if charstring.length < @min_length if use_boundaries # Use boundary-based extraction for valid CFF CharStrings boundaries = glyph_boundaries[glyph_id] # Try each boundary as a potential start position boundaries.each do |start_pos| # Find boundaries that could be end positions pattern_lengths.each do |target_length| # Find next boundary that gives us approximately target_length end_pos = boundaries.find { |b| b >= start_pos + target_length } next unless end_pos actual_length = end_pos - start_pos next if actual_length < @min_length next if actual_length > @min_length + 25 # Max pattern size # Check if pattern is stack-neutral (if stack-aware mode) if @stack_aware tracker = @stack_trackers[glyph_id] next unless tracker next unless tracker.stack_neutral?(start_pos, end_pos) end pattern_bytes = charstring[start_pos, actual_length] # Record occurrence: pattern => [[glyph_id, position], ...] pattern_occurrences[pattern_bytes] << [glyph_id, start_pos] end end else # Fall back to sliding window for non-CFF data (e.g., test data) pattern_lengths.each do |length| break if length > charstring.length (0..charstring.length - length).each do |start_pos| # Check if pattern is stack-neutral (if stack-aware mode) if @stack_aware tracker = @stack_trackers[glyph_id] next unless tracker next unless tracker.stack_neutral?(start_pos, start_pos + length) end pattern_bytes = charstring[start_pos, length] # Record occurrence: pattern => [[glyph_id, position], ...] pattern_occurrences[pattern_bytes] << [glyph_id, start_pos] end end end end # Convert occurrences to Pattern objects pattern_occurrences.each do |bytes, occurrences| # Only keep patterns that appear in at least 2 glyphs or 2+ times next if occurrences.length < 2 # Group by glyph_id by_glyph = occurrences.group_by(&:first) # Only keep if appears in multiple glyphs next if by_glyph.keys.length < 2 # Build positions hash positions = {} by_glyph.each do |glyph_id, glyph_occurrences| positions[glyph_id] = glyph_occurrences.map(&:last).uniq end @patterns[bytes] = Pattern.new( bytes, bytes.length, by_glyph.keys, occurrences.length, 0, # Will be calculated later positions, @stack_aware, # Mark if validated as stack-neutral ) end end |
#filter_patterns ⇒ Object
Filter patterns by criteria
286 287 288 289 290 291 292 293 294 295 296 297 298 299 |
# File 'lib/fontisan/optimizers/pattern_analyzer.rb', line 286 def filter_patterns @patterns.select! do |_bytes, pattern| # Must meet minimum length next false if pattern.length < @min_length # Must have positive savings next false if pattern.savings <= 0 # Must appear in at least 2 glyphs next false if pattern.glyphs.length < 2 true end end |
#find_maximal_patterns ⇒ Object
Find maximal patterns (not contained in larger patterns) TODO: Implement in optimization phase
303 304 305 306 307 |
# File 'lib/fontisan/optimizers/pattern_analyzer.rb', line 303 def find_maximal_patterns # For now, keep all patterns # Future: remove patterns that are substrings of larger patterns # with same or higher frequency end |
#find_operator_boundaries(charstring) ⇒ Array<Integer>
Find operator boundaries in CharString Returns positions where operators end, which are valid pattern boundaries
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/fontisan/optimizers/pattern_analyzer.rb', line 92 def find_operator_boundaries(charstring) io = StringIO.new(charstring) boundaries = [0] # Start is always a boundary until io.eof? byte = io.getbyte if byte <= 31 && byte != 28 # Operator byte (28 is a number encoding prefix) if byte == 12 # Two-byte operator io.getbyte end # Mark position after operator as boundary boundaries << io.pos else # Number - skip it io.pos -= 1 skip_number(io) end end boundaries end |
#skip_number(io) ⇒ Object
Skip over a number without decoding Handles all CFF integer encoding formats
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/fontisan/optimizers/pattern_analyzer.rb', line 120 def skip_number(io) byte = io.getbyte return if byte.nil? case byte when 28 # 3-byte signed integer io.read(2) when 32..246 # Single byte integer - already consumed when 247..254 # 2-byte integer io.getbyte when 255 # 5-byte integer io.read(4) end end |