fontisan 0.2.0 → 0.2.2

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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +119 -308
  3. data/README.adoc +1525 -1323
  4. data/Rakefile +45 -47
  5. data/benchmark/variation_quick_bench.rb +4 -4
  6. data/docs/FONT_HINTING.adoc +562 -0
  7. data/docs/VARIABLE_FONT_OPERATIONS.adoc +599 -0
  8. data/lib/fontisan/cli.rb +92 -34
  9. data/lib/fontisan/collection/builder.rb +82 -0
  10. data/lib/fontisan/collection/offset_calculator.rb +2 -0
  11. data/lib/fontisan/collection/table_deduplicator.rb +76 -0
  12. data/lib/fontisan/commands/base_command.rb +21 -2
  13. data/lib/fontisan/commands/convert_command.rb +96 -165
  14. data/lib/fontisan/commands/info_command.rb +111 -5
  15. data/lib/fontisan/commands/instance_command.rb +77 -85
  16. data/lib/fontisan/commands/validate_command.rb +28 -0
  17. data/lib/fontisan/config/validation_rules.yml +1 -1
  18. data/lib/fontisan/constants.rb +34 -24
  19. data/lib/fontisan/converters/format_converter.rb +154 -1
  20. data/lib/fontisan/converters/outline_converter.rb +101 -34
  21. data/lib/fontisan/converters/woff_writer.rb +9 -4
  22. data/lib/fontisan/font_loader.rb +14 -9
  23. data/lib/fontisan/font_writer.rb +9 -6
  24. data/lib/fontisan/formatters/text_formatter.rb +45 -1
  25. data/lib/fontisan/hints/hint_converter.rb +131 -2
  26. data/lib/fontisan/hints/hint_validator.rb +284 -0
  27. data/lib/fontisan/hints/postscript_hint_applier.rb +219 -140
  28. data/lib/fontisan/hints/postscript_hint_extractor.rb +151 -16
  29. data/lib/fontisan/hints/truetype_hint_applier.rb +90 -44
  30. data/lib/fontisan/hints/truetype_hint_extractor.rb +134 -11
  31. data/lib/fontisan/hints/truetype_instruction_analyzer.rb +261 -0
  32. data/lib/fontisan/hints/truetype_instruction_generator.rb +266 -0
  33. data/lib/fontisan/loading_modes.rb +6 -4
  34. data/lib/fontisan/models/collection_brief_info.rb +31 -0
  35. data/lib/fontisan/models/font_info.rb +3 -30
  36. data/lib/fontisan/models/hint.rb +183 -12
  37. data/lib/fontisan/models/outline.rb +4 -1
  38. data/lib/fontisan/open_type_font.rb +28 -10
  39. data/lib/fontisan/open_type_font_extensions.rb +54 -0
  40. data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
  41. data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
  42. data/lib/fontisan/pipeline/format_detector.rb +249 -0
  43. data/lib/fontisan/pipeline/output_writer.rb +159 -0
  44. data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
  45. data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
  46. data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
  47. data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
  48. data/lib/fontisan/pipeline/transformation_pipeline.rb +416 -0
  49. data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
  50. data/lib/fontisan/subset/table_subsetter.rb +5 -5
  51. data/lib/fontisan/tables/cff/charstring.rb +58 -3
  52. data/lib/fontisan/tables/cff/charstring_builder.rb +34 -0
  53. data/lib/fontisan/tables/cff/charstring_parser.rb +249 -0
  54. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
  55. data/lib/fontisan/tables/cff/dict_builder.rb +19 -1
  56. data/lib/fontisan/tables/cff/hint_operation_injector.rb +209 -0
  57. data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
  58. data/lib/fontisan/tables/cff/private_dict_writer.rb +131 -0
  59. data/lib/fontisan/tables/cff/table_builder.rb +221 -0
  60. data/lib/fontisan/tables/cff.rb +2 -0
  61. data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
  62. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +247 -0
  63. data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
  64. data/lib/fontisan/tables/cff2/table_builder.rb +580 -0
  65. data/lib/fontisan/tables/cff2/table_reader.rb +421 -0
  66. data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
  67. data/lib/fontisan/tables/cff2.rb +10 -5
  68. data/lib/fontisan/tables/cvar.rb +2 -41
  69. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +2 -1
  70. data/lib/fontisan/tables/glyf/curve_converter.rb +10 -4
  71. data/lib/fontisan/tables/glyf/glyph_builder.rb +27 -10
  72. data/lib/fontisan/tables/gvar.rb +2 -41
  73. data/lib/fontisan/tables/name.rb +4 -4
  74. data/lib/fontisan/true_type_font.rb +27 -10
  75. data/lib/fontisan/true_type_font_extensions.rb +54 -0
  76. data/lib/fontisan/utilities/checksum_calculator.rb +42 -0
  77. data/lib/fontisan/validation/checksum_validator.rb +2 -2
  78. data/lib/fontisan/validation/table_validator.rb +1 -1
  79. data/lib/fontisan/validation/variable_font_validator.rb +218 -0
  80. data/lib/fontisan/variation/cache.rb +3 -1
  81. data/lib/fontisan/variation/converter.rb +121 -13
  82. data/lib/fontisan/variation/delta_applier.rb +2 -1
  83. data/lib/fontisan/variation/inspector.rb +2 -1
  84. data/lib/fontisan/variation/instance_generator.rb +2 -1
  85. data/lib/fontisan/variation/instance_writer.rb +341 -0
  86. data/lib/fontisan/variation/optimizer.rb +6 -3
  87. data/lib/fontisan/variation/subsetter.rb +32 -10
  88. data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
  89. data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
  90. data/lib/fontisan/variation/variation_preserver.rb +291 -0
  91. data/lib/fontisan/version.rb +1 -1
  92. data/lib/fontisan/version.rb.orig +9 -0
  93. data/lib/fontisan/woff2/glyf_transformer.rb +693 -0
  94. data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
  95. data/lib/fontisan/woff2_font.rb +489 -468
  96. data/lib/fontisan/woff_font.rb +16 -11
  97. data/lib/fontisan.rb +54 -2
  98. data/scripts/measure_optimization.rb +15 -7
  99. metadata +37 -2
@@ -1,70 +1,116 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../models/hint"
4
-
5
3
  module Fontisan
6
4
  module Hints
7
- # Applies rendering hints to TrueType glyph data
5
+ # Applies rendering hints to TrueType font tables
6
+ #
7
+ # This applier writes TrueType hint data into font-level tables:
8
+ # - fpgm (Font Program) - bytecode executed once at font initialization
9
+ # - prep (Control Value Program) - bytecode for glyph preparation
10
+ # - cvt (Control Values) - array of 16-bit values for hinting metrics
8
11
  #
9
- # This applier converts universal Hint objects into TrueType bytecode
10
- # instructions and integrates them into glyph data. It ensures proper
11
- # instruction sequencing and maintains compatibility with TrueType
12
- # instruction execution model.
12
+ # The applier ensures proper table structure with correct checksums
13
+ # and does not corrupt the font if hint application fails.
13
14
  #
14
- # @example Apply hints to a glyph
15
+ # @example Apply hints from a HintSet
15
16
  # applier = TrueTypeHintApplier.new
16
- # glyph_with_hints = applier.apply(glyph, hints)
17
+ # tables = {}
18
+ # updated_tables = applier.apply(hint_set, tables)
17
19
  class TrueTypeHintApplier
18
- # Apply hints to TrueType glyph
20
+ # Apply TrueType hints to font tables
19
21
  #
20
- # @param glyph [Glyph] Target glyph
21
- # @param hints [Array<Hint>] Hints to apply
22
- # @return [Glyph] Glyph with applied hints
23
- def apply(glyph, hints)
24
- return glyph if hints.nil? || hints.empty?
25
- return glyph if glyph.nil?
22
+ # @param hint_set [HintSet] Hint data to apply
23
+ # @param tables [Hash] Font tables to update
24
+ # @return [Hash] Updated font tables
25
+ def apply(hint_set, tables)
26
+ return tables if hint_set.nil? || hint_set.empty?
27
+ return tables unless hint_set.format == "truetype"
26
28
 
27
- # Convert hints to TrueType instructions
28
- instructions = build_instructions(hints)
29
+ # Write fpgm table if present
30
+ if hint_set.font_program && !hint_set.font_program.empty?
31
+ tables["fpgm"] = build_fpgm_table(hint_set.font_program)
32
+ end
29
33
 
30
- # Apply to glyph (this is a simplified version)
31
- # In a real implementation, we would need to:
32
- # 1. Analyze existing glyph structure
33
- # 2. Insert instructions at appropriate points
34
- # 3. Update glyph instruction data
34
+ # Write prep table if present
35
+ if hint_set.control_value_program && !hint_set.control_value_program.empty?
36
+ tables["prep"] = build_prep_table(hint_set.control_value_program)
37
+ end
35
38
 
36
- # For now, we just return the glyph as-is since
37
- # this is a complex operation requiring deep integration
38
- # with the glyph structure
39
- glyph
39
+ # Write cvt table if present
40
+ if hint_set.control_values && !hint_set.control_values.empty?
41
+ tables["cvt "] = build_cvt_table(hint_set.control_values)
42
+ end
43
+
44
+ # Future enhancement: Apply per-glyph hints to glyf table
45
+ # For now, font-level tables only
46
+
47
+ tables
40
48
  end
41
49
 
42
50
  private
43
51
 
44
- # Build TrueType instruction sequence from hints
52
+ # Build fpgm (Font Program) table
45
53
  #
46
- # @param hints [Array<Hint>] Hints to convert
47
- # @return [Array<Integer>] Instruction bytes
48
- def build_instructions(hints)
49
- instructions = []
54
+ # @param program_data [String] Raw bytecode
55
+ # @return [Hash] Table structure with tag, data, and checksum
56
+ def build_fpgm_table(program_data)
57
+ {
58
+ tag: "fpgm",
59
+ data: program_data,
60
+ checksum: calculate_checksum(program_data),
61
+ }
62
+ end
50
63
 
51
- hints.each do |hint|
52
- hint_instructions = hint.to_truetype
53
- instructions.concat(hint_instructions) if hint_instructions
54
- end
64
+ # Build prep (Control Value Program) table
65
+ #
66
+ # @param program_data [String] Raw bytecode
67
+ # @return [Hash] Table structure with tag, data, and checksum
68
+ def build_prep_table(program_data)
69
+ {
70
+ tag: "prep",
71
+ data: program_data,
72
+ checksum: calculate_checksum(program_data),
73
+ }
74
+ end
75
+
76
+ # Build cvt (Control Values) table
77
+ #
78
+ # CVT values are 16-bit signed integers (FWORD) in big-endian format.
79
+ # Each value represents a design-space coordinate used for hinting.
80
+ #
81
+ # @param control_values [Array<Integer>] Array of 16-bit signed values
82
+ # @return [Hash] Table structure with tag, data, and checksum
83
+ def build_cvt_table(control_values)
84
+ # Pack as 16-bit big-endian signed integers (s> = signed big-endian)
85
+ data = control_values.pack("s>*")
55
86
 
56
- instructions
87
+ {
88
+ tag: "cvt ",
89
+ data: data,
90
+ checksum: calculate_checksum(data),
91
+ }
57
92
  end
58
93
 
59
- # Validate instruction sequence
94
+ # Calculate OpenType table checksum
95
+ #
96
+ # OpenType spec requires tables to be checksummed as 32-bit unsigned
97
+ # integers in big-endian format. The table is padded to a multiple of
98
+ # 4 bytes with zeros before checksum calculation.
60
99
  #
61
- # @param instructions [Array<Integer>] Instructions to validate
62
- # @return [Boolean] True if valid
63
- def valid_instructions?(instructions)
64
- return true if instructions.empty?
100
+ # @param data [String] Table data
101
+ # @return [Integer] 32-bit checksum
102
+ def calculate_checksum(data)
103
+ # Pad to 4-byte boundary with zeros
104
+ padding_needed = (4 - data.length % 4) % 4
105
+ padded = data + ("\x00" * padding_needed)
106
+
107
+ # Sum as 32-bit unsigned integers in big-endian
108
+ checksum = 0
109
+ (0...padded.length).step(4) do |i|
110
+ checksum = (checksum + padded[i, 4].unpack1("N")) & 0xFFFFFFFF
111
+ end
65
112
 
66
- # Basic validation - check for valid opcodes
67
- instructions.all? { |byte| byte >= 0 && byte <= 255 }
113
+ checksum
68
114
  end
69
115
  end
70
116
  end
@@ -29,12 +29,36 @@ module Fontisan
29
29
  MDRP_MIN_RND_BLACK = 0xC0
30
30
  IUP_Y = 0x30
31
31
  IUP_X = 0x31
32
- SHP = [0x32, 0x33]
32
+ SHP = [0x32, 0x33].freeze
33
33
  ALIGNRP = 0x3C
34
34
  DELTAP1 = 0x5D
35
35
  DELTAP2 = 0x71
36
36
  DELTAP3 = 0x72
37
37
 
38
+ # Extract complete hint data from TrueType font
39
+ #
40
+ # This extracts both font-level hints (fpgm, prep, cvt tables) and
41
+ # per-glyph hints from glyph instructions.
42
+ #
43
+ # @param font [TrueTypeFont] TrueType font object
44
+ # @return [Models::HintSet] Complete hint set
45
+ def extract_from_font(font)
46
+ hint_set = Models::HintSet.new(format: "truetype")
47
+
48
+ # Extract font-level programs
49
+ hint_set.font_program = extract_font_program(font)
50
+ hint_set.control_value_program = extract_control_value_program(font)
51
+ hint_set.control_values = extract_control_values(font)
52
+
53
+ # Extract per-glyph hints
54
+ extract_glyph_hints(font, hint_set)
55
+
56
+ # Update metadata
57
+ hint_set.has_hints = !hint_set.empty?
58
+
59
+ hint_set
60
+ end
61
+
38
62
  # Extract hints from TrueType glyph
39
63
  #
40
64
  # @param glyph [Glyph] TrueType glyph with instructions
@@ -80,7 +104,7 @@ module Fontisan
80
104
  hints << Models::Hint.new(
81
105
  type: :interpolate,
82
106
  data: { axis: opcode == IUP_Y ? :y : :x },
83
- source_format: :truetype
107
+ source_format: :truetype,
84
108
  )
85
109
  i += 1
86
110
 
@@ -89,7 +113,7 @@ module Fontisan
89
113
  hints << Models::Hint.new(
90
114
  type: :shift,
91
115
  data: { instructions: [opcode] },
92
- source_format: :truetype
116
+ source_format: :truetype,
93
117
  )
94
118
  i += 1
95
119
 
@@ -98,7 +122,7 @@ module Fontisan
98
122
  hints << Models::Hint.new(
99
123
  type: :align,
100
124
  data: {},
101
- source_format: :truetype
125
+ source_format: :truetype,
102
126
  )
103
127
  i += 1
104
128
 
@@ -113,9 +137,9 @@ module Fontisan
113
137
  type: :delta,
114
138
  data: {
115
139
  instructions: [opcode] + [count] + delta_data,
116
- count: count
140
+ count: count,
117
141
  },
118
- source_format: :truetype
142
+ source_format: :truetype,
119
143
  )
120
144
  i += count * 2 + 1
121
145
  end
@@ -141,7 +165,7 @@ module Fontisan
141
165
 
142
166
  # Check if next instruction is MDRP (stem width)
143
167
  has_width = index + 1 < bytes.length &&
144
- bytes[index + 1] == MDRP_MIN_RND_BLACK
168
+ bytes[index + 1] == MDRP_MIN_RND_BLACK
145
169
 
146
170
  if has_width
147
171
  Models::Hint.new(
@@ -149,14 +173,113 @@ module Fontisan
149
173
  data: {
150
174
  position: 0, # Would be extracted from graphics state
151
175
  width: 0, # Would be calculated from MDRP
152
- orientation: :vertical # Inferred from instruction context
176
+ orientation: :vertical, # Inferred from instruction context
153
177
  },
154
- source_format: :truetype
178
+ source_format: :truetype,
155
179
  )
156
- else
157
- nil
158
180
  end
159
181
  end
182
+
183
+ # Extract font program (fpgm table)
184
+ #
185
+ # @param font [TrueTypeFont] TrueType font
186
+ # @return [String] Font program bytecode (binary string)
187
+ def extract_font_program(font)
188
+ return "" unless font.has_table?("fpgm")
189
+
190
+ font_program_data = font.instance_variable_get(:@table_data)["fpgm"]
191
+ return "" unless font_program_data
192
+
193
+ # Return as binary string
194
+ font_program_data.force_encoding("ASCII-8BIT")
195
+ rescue StandardError => e
196
+ warn "Failed to extract font program: #{e.message}"
197
+ ""
198
+ end
199
+
200
+ # Extract control value program (prep table)
201
+ #
202
+ # @param font [TrueTypeFont] TrueType font
203
+ # @return [String] Control value program bytecode (binary string)
204
+ def extract_control_value_program(font)
205
+ return "" unless font.has_table?("prep")
206
+
207
+ prep_data = font.instance_variable_get(:@table_data)["prep"]
208
+ return "" unless prep_data
209
+
210
+ # Return as binary string
211
+ prep_data.force_encoding("ASCII-8BIT")
212
+ rescue StandardError => e
213
+ warn "Failed to extract control value program: #{e.message}"
214
+ ""
215
+ end
216
+
217
+ # Extract control values (cvt table)
218
+ #
219
+ # @param font [TrueTypeFont] TrueType font
220
+ # @return [Array<Integer>] Control values
221
+ def extract_control_values(font)
222
+ return [] unless font.has_table?("cvt ")
223
+
224
+ cvt_data = font.instance_variable_get(:@table_data)["cvt "]
225
+ return [] unless cvt_data
226
+
227
+ # CVT table is an array of 16-bit signed integers (FWord values)
228
+ values = []
229
+ io = StringIO.new(cvt_data)
230
+ while !io.eof?
231
+ # Read 16-bit big-endian signed integer
232
+ bytes = io.read(2)
233
+ break unless bytes&.length == 2
234
+
235
+ value = bytes.unpack1("n") # Unsigned short
236
+ # Convert to signed
237
+ value = value - 65536 if value > 32767
238
+ values << value
239
+ end
240
+
241
+ values
242
+ rescue StandardError => e
243
+ warn "Failed to extract control values: #{e.message}"
244
+ []
245
+ end
246
+
247
+ # Extract per-glyph hints from glyf table
248
+ #
249
+ # @param font [TrueTypeFont] TrueType font
250
+ # @param hint_set [Models::HintSet] Hint set to populate
251
+ # @return [void]
252
+ def extract_glyph_hints(font, hint_set)
253
+ return unless font.has_table?("glyf")
254
+
255
+ glyf_table = font.table("glyf")
256
+ return unless glyf_table
257
+
258
+ # Get number of glyphs from maxp table
259
+ maxp_table = font.table("maxp")
260
+ return unless maxp_table
261
+
262
+ num_glyphs = maxp_table.num_glyphs
263
+
264
+ # Iterate through all glyphs
265
+ (0...num_glyphs).each do |glyph_id|
266
+ glyph = glyf_table.glyph_for(glyph_id)
267
+ next unless glyph
268
+ next if glyph.number_of_contours <= 0 # Skip compound glyphs and empty glyphs
269
+
270
+ # Extract hints from simple glyph instructions
271
+ hints = extract(glyph)
272
+ next if hints.empty?
273
+
274
+ # Store glyph hints
275
+ hint_set.add_glyph_hints(glyph_id, hints)
276
+ rescue StandardError
277
+ # Skip glyphs that fail to parse
278
+ next
279
+ end
280
+ rescue StandardError => e
281
+ warn "Failed to extract glyph hints: #{e.message}"
282
+ end
160
283
  end
161
284
  end
162
285
  end
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Hints
5
+ # Analyzes TrueType bytecode instructions to extract hint parameters
6
+ #
7
+ # This analyzer parses fpgm (Font Program) and prep (Control Value Program)
8
+ # bytecode to extract semantic hint information that can be converted to
9
+ # PostScript Private dict parameters.
10
+ #
11
+ # **Key Extracted Parameters:**
12
+ #
13
+ # - Blue zones (alignment zones for baseline, x-height, cap-height)
14
+ # - Stem widths (from CVT setup in prep)
15
+ # - Delta base and shift values
16
+ # - Twilight zone setup
17
+ #
18
+ # @example Analyze prep program
19
+ # analyzer = TrueTypeInstructionAnalyzer.new
20
+ # params = analyzer.analyze_prep(prep_bytecode, cvt_values)
21
+ class TrueTypeInstructionAnalyzer
22
+ # TrueType instruction opcodes relevant for hint extraction
23
+ NPUSHB = 0x40 # Push N bytes
24
+ NPUSHW = 0x41 # Push N words
25
+ PUSHB = (0xB0..0xB7).to_a # Push 1-8 bytes
26
+ PUSHW = (0xB8..0xBF).to_a # Push 1-8 words
27
+ SVTCA_Y = 0x00 # Set freedom and projection vectors to Y-axis
28
+ SVTCA_X = 0x01 # Set freedom and projection vectors to X-axis
29
+ RCVT = 0x45 # Read CVT
30
+ WCVTP = 0x44 # Write CVT (in Pixels)
31
+ WCVTF = 0x70 # Write CVT (in FUnits)
32
+ MDAP = [0x2E, 0x2F].freeze # Move Direct Absolute Point
33
+ SCVTCI = 0x1D # Set Control Value Table Cut In
34
+ SSWCI = 0x1E # Set Single Width Cut In
35
+ SSW = 0x1F # Set Single Width
36
+
37
+ # Analyze prep program to extract hint parameters
38
+ #
39
+ # @param prep [String] Control value program bytecode
40
+ # @param cvt [Array<Integer>] Control values
41
+ # @return [Hash] Extracted hint parameters
42
+ def analyze_prep(prep, cvt = [])
43
+ return {} if prep.nil? && (cvt.nil? || cvt.empty?)
44
+
45
+ params = {}
46
+
47
+ # Parse prep bytecode if present
48
+ if prep && !prep.empty?
49
+ bytes = prep.bytes
50
+ stack = []
51
+ i = 0
52
+
53
+ while i < bytes.length
54
+ opcode = bytes[i]
55
+
56
+ case opcode
57
+ when NPUSHB
58
+ # Push N bytes
59
+ i += 1
60
+ count = bytes[i]
61
+ i += 1
62
+ count.times do
63
+ stack.push(bytes[i])
64
+ i += 1
65
+ end
66
+ next
67
+
68
+ when NPUSHW
69
+ # Push N words (16-bit values)
70
+ i += 1
71
+ count = bytes[i]
72
+ i += 1
73
+ count.times do
74
+ value = (bytes[i] << 8) | bytes[i + 1]
75
+ # Convert to signed
76
+ value = value - 65536 if value > 32767
77
+ stack.push(value)
78
+ i += 2
79
+ end
80
+ next
81
+
82
+ when *PUSHB
83
+ # Push 1-8 bytes
84
+ count = opcode - 0xB0 + 1
85
+ i += 1
86
+ count.times do
87
+ stack.push(bytes[i])
88
+ i += 1
89
+ end
90
+ next
91
+
92
+ when *PUSHW
93
+ # Push 1-8 words
94
+ count = opcode - 0xB8 + 1
95
+ i += 1
96
+ count.times do
97
+ value = (bytes[i] << 8) | bytes[i + 1]
98
+ value = value - 65536 if value > 32767
99
+ stack.push(value)
100
+ i += 2
101
+ end
102
+ next
103
+
104
+ when WCVTP, WCVTF
105
+ # Write to CVT - this shows which CVT indices are being set up
106
+ # Pattern: value cvt_index WCVTP (stack top to bottom)
107
+ if stack.length >= 2
108
+ value = stack.pop
109
+ cvt_index = stack.pop
110
+ # Track CVT modifications (useful for understanding setup)
111
+ end
112
+
113
+ when SSW
114
+ # Set Single Width - used for stem width control
115
+ if stack.length >= 1
116
+ width = stack.pop
117
+ params[:single_width] = width unless params[:single_width]
118
+ end
119
+
120
+ when SSWCI
121
+ # Set Single Width Cut In
122
+ if stack.length >= 1
123
+ params[:single_width_cut_in] = stack.pop
124
+ end
125
+
126
+ when SCVTCI
127
+ # Set CVT Cut In
128
+ if stack.length >= 1
129
+ params[:cvt_cut_in] = stack.pop
130
+ end
131
+ end
132
+
133
+ i += 1
134
+ end
135
+ end
136
+
137
+ # Extract blue zones from CVT analysis (always do this if CVT is present)
138
+ if cvt && !cvt.empty?
139
+ params.merge!(extract_blue_zones_from_cvt(cvt))
140
+ end
141
+
142
+ params
143
+ rescue StandardError => e
144
+ warn "Error analyzing prep program: #{e.message}"
145
+ {}
146
+ end
147
+
148
+ # Analyze Font Program (fpgm) for complexity indicators
149
+ #
150
+ # The fpgm contains font-level function definitions. While we don't
151
+ # fully decompile it, we can extract useful metadata about hint complexity.
152
+ #
153
+ # @param fpgm [String] Binary fpgm data
154
+ # @return [Hash] Analysis results with complexity indicators
155
+ def analyze_fpgm(fpgm)
156
+ return {} if fpgm.nil? || fpgm.empty?
157
+
158
+ size = fpgm.bytesize
159
+
160
+ # Estimate complexity based on size
161
+ complexity = if size < 100
162
+ :simple
163
+ elsif size < 200
164
+ :moderate
165
+ else
166
+ :complex
167
+ end
168
+
169
+ {
170
+ fpgm_size: size,
171
+ has_functions: size > 0,
172
+ complexity: complexity,
173
+ }
174
+ rescue StandardError
175
+ # Return empty hash on any error
176
+ {}
177
+ end
178
+
179
+ # Extract blue zones from CVT values using heuristics
180
+ #
181
+ # Blue zones in PostScript define alignment constraints for
182
+ # baseline, x-height, cap-height, ascender, and descender.
183
+ # TrueType doesn't have explicit blue zones, but we can derive
184
+ # them from CVT values using common patterns.
185
+ #
186
+ # Heuristics:
187
+ # - Negative values near -250 to -200: Descender zones
188
+ # - Values near 0: Baseline zones
189
+ # - Values near 500-550: X-height zones
190
+ # - Values near 700-750: Cap-height zones
191
+ # - For large UPM (>2000): Scale thresholds proportionally
192
+ #
193
+ # @param cvt [Array<Integer>] Control Value Table entries
194
+ # @return [Hash] Extracted blue zone parameters
195
+ def extract_blue_zones_from_cvt(cvt)
196
+ return {} if cvt.nil? || cvt.empty?
197
+
198
+ zones = {}
199
+
200
+ # Detect scale from maximum absolute value
201
+ max_value = cvt.map(&:abs).max
202
+ scale_factor = max_value > 1000 ? (max_value / 1000.0) : 1.0
203
+
204
+ # Scaled thresholds
205
+ descender_min = (-300 * scale_factor).to_i
206
+ descender_max = (-150 * scale_factor).to_i
207
+ baseline_range = (50 * scale_factor).to_i
208
+ xheight_min = (450 * scale_factor).to_i
209
+ xheight_max = (600 * scale_factor).to_i
210
+ capheight_min = (650 * scale_factor).to_i
211
+ capheight_max = (1500 * scale_factor).to_i # Wider range for large UPM
212
+
213
+ # Group CVT values by typical alignment zones
214
+ descender_values = cvt.select { |v| v < descender_max && v > descender_min }
215
+ baseline_values = cvt.select { |v| v >= -baseline_range && v <= baseline_range }
216
+ xheight_values = cvt.select { |v| v >= xheight_min && v <= xheight_max }
217
+ capheight_values = cvt.select { |v| v >= capheight_min && v <= capheight_max }
218
+
219
+ # Build blue_values (baseline and top zones)
220
+ blue_values = []
221
+
222
+ # Add baseline zone if detected
223
+ if baseline_values.any?
224
+ min_baseline = baseline_values.min
225
+ max_baseline = baseline_values.max
226
+ blue_values << min_baseline << max_baseline
227
+ end
228
+
229
+ # Add cap-height zone if detected (or any top zone for large UPM)
230
+ if capheight_values.any?
231
+ min_cap = capheight_values.min
232
+ max_cap = capheight_values.max
233
+ blue_values << min_cap << max_cap
234
+ end
235
+
236
+ zones[:blue_values] = blue_values unless blue_values.empty?
237
+
238
+ # Build other_blues (descender zones)
239
+ if descender_values.any?
240
+ min_desc = descender_values.min
241
+ max_desc = descender_values.max
242
+ zones[:other_blues] = [min_desc, max_desc]
243
+ end
244
+
245
+ zones
246
+ end
247
+
248
+ private
249
+
250
+ # Estimate complexity of bytecode program
251
+ #
252
+ # @param bytes [Array<Integer>] Bytecode
253
+ # @return [Symbol] Complexity level (:simple, :moderate, :complex)
254
+ def estimate_complexity(bytes)
255
+ return :simple if bytes.length < 50
256
+ return :moderate if bytes.length < 200
257
+ :complex
258
+ end
259
+ end
260
+ end
261
+ end