fontisan 0.2.1 → 0.2.3

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +58 -392
  3. data/README.adoc +1509 -1430
  4. data/Rakefile +3 -2
  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/base_collection.rb +296 -0
  9. data/lib/fontisan/cli.rb +10 -3
  10. data/lib/fontisan/collection/builder.rb +2 -1
  11. data/lib/fontisan/collection/offset_calculator.rb +2 -0
  12. data/lib/fontisan/commands/base_command.rb +5 -2
  13. data/lib/fontisan/commands/convert_command.rb +6 -2
  14. data/lib/fontisan/commands/info_command.rb +129 -5
  15. data/lib/fontisan/commands/instance_command.rb +8 -7
  16. data/lib/fontisan/commands/validate_command.rb +4 -1
  17. data/lib/fontisan/constants.rb +24 -24
  18. data/lib/fontisan/converters/format_converter.rb +8 -4
  19. data/lib/fontisan/converters/outline_converter.rb +21 -16
  20. data/lib/fontisan/converters/woff_writer.rb +8 -3
  21. data/lib/fontisan/font_loader.rb +120 -30
  22. data/lib/fontisan/font_writer.rb +2 -0
  23. data/lib/fontisan/formatters/text_formatter.rb +116 -19
  24. data/lib/fontisan/hints/hint_converter.rb +43 -47
  25. data/lib/fontisan/hints/hint_validator.rb +284 -0
  26. data/lib/fontisan/hints/postscript_hint_applier.rb +1 -3
  27. data/lib/fontisan/hints/postscript_hint_extractor.rb +78 -43
  28. data/lib/fontisan/hints/truetype_hint_extractor.rb +22 -26
  29. data/lib/fontisan/hints/truetype_instruction_analyzer.rb +261 -0
  30. data/lib/fontisan/hints/truetype_instruction_generator.rb +266 -0
  31. data/lib/fontisan/loading_modes.rb +4 -4
  32. data/lib/fontisan/models/collection_brief_info.rb +37 -0
  33. data/lib/fontisan/models/collection_info.rb +6 -1
  34. data/lib/fontisan/models/font_export.rb +2 -2
  35. data/lib/fontisan/models/font_info.rb +3 -30
  36. data/lib/fontisan/models/hint.rb +22 -23
  37. data/lib/fontisan/models/outline.rb +4 -1
  38. data/lib/fontisan/models/validation_report.rb +1 -1
  39. data/lib/fontisan/open_type_collection.rb +17 -220
  40. data/lib/fontisan/open_type_font.rb +3 -1
  41. data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
  42. data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
  43. data/lib/fontisan/pipeline/output_writer.rb +8 -3
  44. data/lib/fontisan/pipeline/transformation_pipeline.rb +8 -3
  45. data/lib/fontisan/subset/table_subsetter.rb +5 -5
  46. data/lib/fontisan/tables/cff/charstring.rb +38 -12
  47. data/lib/fontisan/tables/cff/charstring_parser.rb +23 -11
  48. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +14 -14
  49. data/lib/fontisan/tables/cff/dict_builder.rb +4 -1
  50. data/lib/fontisan/tables/cff/hint_operation_injector.rb +6 -4
  51. data/lib/fontisan/tables/cff/offset_recalculator.rb +1 -1
  52. data/lib/fontisan/tables/cff/private_dict_writer.rb +10 -4
  53. data/lib/fontisan/tables/cff/table_builder.rb +1 -1
  54. data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
  55. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +7 -6
  56. data/lib/fontisan/tables/cff2/region_matcher.rb +2 -2
  57. data/lib/fontisan/tables/cff2/table_builder.rb +26 -20
  58. data/lib/fontisan/tables/cff2/table_reader.rb +35 -33
  59. data/lib/fontisan/tables/cff2/variation_data_extractor.rb +2 -2
  60. data/lib/fontisan/tables/cff2.rb +1 -1
  61. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +2 -1
  62. data/lib/fontisan/tables/glyf/curve_converter.rb +10 -4
  63. data/lib/fontisan/tables/glyf/glyph_builder.rb +27 -10
  64. data/lib/fontisan/tables/name.rb +4 -4
  65. data/lib/fontisan/true_type_collection.rb +29 -113
  66. data/lib/fontisan/true_type_font.rb +3 -1
  67. data/lib/fontisan/validation/checksum_validator.rb +2 -2
  68. data/lib/fontisan/variation/cache.rb +3 -1
  69. data/lib/fontisan/variation/converter.rb +2 -1
  70. data/lib/fontisan/variation/delta_applier.rb +2 -1
  71. data/lib/fontisan/variation/inspector.rb +2 -1
  72. data/lib/fontisan/variation/instance_generator.rb +2 -1
  73. data/lib/fontisan/variation/optimizer.rb +6 -3
  74. data/lib/fontisan/variation/subsetter.rb +32 -10
  75. data/lib/fontisan/variation/variation_preserver.rb +4 -1
  76. data/lib/fontisan/version.rb +1 -1
  77. data/lib/fontisan/woff2/glyf_transformer.rb +57 -30
  78. data/lib/fontisan/woff2_font.rb +31 -15
  79. data/lib/fontisan.rb +42 -2
  80. data/scripts/measure_optimization.rb +15 -7
  81. metadata +9 -2
@@ -29,7 +29,7 @@ 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
@@ -104,7 +104,7 @@ module Fontisan
104
104
  hints << Models::Hint.new(
105
105
  type: :interpolate,
106
106
  data: { axis: opcode == IUP_Y ? :y : :x },
107
- source_format: :truetype
107
+ source_format: :truetype,
108
108
  )
109
109
  i += 1
110
110
 
@@ -113,7 +113,7 @@ module Fontisan
113
113
  hints << Models::Hint.new(
114
114
  type: :shift,
115
115
  data: { instructions: [opcode] },
116
- source_format: :truetype
116
+ source_format: :truetype,
117
117
  )
118
118
  i += 1
119
119
 
@@ -122,7 +122,7 @@ module Fontisan
122
122
  hints << Models::Hint.new(
123
123
  type: :align,
124
124
  data: {},
125
- source_format: :truetype
125
+ source_format: :truetype,
126
126
  )
127
127
  i += 1
128
128
 
@@ -137,9 +137,9 @@ module Fontisan
137
137
  type: :delta,
138
138
  data: {
139
139
  instructions: [opcode] + [count] + delta_data,
140
- count: count
140
+ count: count,
141
141
  },
142
- source_format: :truetype
142
+ source_format: :truetype,
143
143
  )
144
144
  i += count * 2 + 1
145
145
  end
@@ -165,7 +165,7 @@ module Fontisan
165
165
 
166
166
  # Check if next instruction is MDRP (stem width)
167
167
  has_width = index + 1 < bytes.length &&
168
- bytes[index + 1] == MDRP_MIN_RND_BLACK
168
+ bytes[index + 1] == MDRP_MIN_RND_BLACK
169
169
 
170
170
  if has_width
171
171
  Models::Hint.new(
@@ -173,12 +173,10 @@ module Fontisan
173
173
  data: {
174
174
  position: 0, # Would be extracted from graphics state
175
175
  width: 0, # Would be calculated from MDRP
176
- orientation: :vertical # Inferred from instruction context
176
+ orientation: :vertical, # Inferred from instruction context
177
177
  },
178
- source_format: :truetype
178
+ source_format: :truetype,
179
179
  )
180
- else
181
- nil
182
180
  end
183
181
  end
184
182
 
@@ -265,21 +263,19 @@ module Fontisan
265
263
 
266
264
  # Iterate through all glyphs
267
265
  (0...num_glyphs).each do |glyph_id|
268
- begin
269
- glyph = glyf_table.glyph_for(glyph_id)
270
- next unless glyph
271
- next if glyph.number_of_contours <= 0 # Skip compound glyphs and empty glyphs
272
-
273
- # Extract hints from simple glyph instructions
274
- hints = extract(glyph)
275
- next if hints.empty?
276
-
277
- # Store glyph hints
278
- hint_set.add_glyph_hints(glyph_id, hints)
279
- rescue StandardError => e
280
- # Skip glyphs that fail to parse
281
- next
282
- end
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
283
279
  end
284
280
  rescue StandardError => e
285
281
  warn "Failed to extract glyph hints: #{e.message}"
@@ -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
@@ -0,0 +1,266 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Hints
5
+ # Generates TrueType instruction bytecode from PostScript hint parameters
6
+ #
7
+ # This class is the inverse of TrueTypeInstructionAnalyzer - it takes
8
+ # PostScript hint parameters and generates equivalent TrueType prep/fpgm
9
+ # programs and CVT values.
10
+ #
11
+ # TrueType Instruction Opcodes:
12
+ # - NPUSHB (0x40): Push n bytes
13
+ # - NPUSHW (0x41): Push n words (16-bit)
14
+ # - PUSHB[n] (0xB0-0xB7): Push 1-8 bytes
15
+ # - PUSHW[n] (0xB8-0xBF): Push 1-8 words
16
+ # - SSW (0x1F): Set Single Width
17
+ # - SSWCI (0x1E): Set Single Width Cut-In
18
+ # - SCVTCI (0x1D): Set CVT Cut-In
19
+ # - WCVTP (0x44): Write CVT in Pixels
20
+ # - WCVTF (0x70): Write CVT in FUnits
21
+ #
22
+ # @example Generate TrueType programs
23
+ # generator = TrueTypeInstructionGenerator.new
24
+ # programs = generator.generate({
25
+ # blue_scale: 0.039625,
26
+ # std_hw: 80,
27
+ # std_vw: 90
28
+ # })
29
+ # programs[:prep] # => Binary prep program
30
+ # programs[:fpgm] # => Binary fpgm program (usually empty)
31
+ # programs[:cvt] # => Array of CVT values
32
+ class TrueTypeInstructionGenerator
33
+ # TrueType instruction opcodes
34
+ NPUSHB = 0x40 # Push n bytes
35
+ NPUSHW = 0x41 # Push n words (16-bit)
36
+ PUSHB_BASE = 0xB0 # PUSHB[0] through PUSHB[7]
37
+ PUSHW_BASE = 0xB8 # PUSHW[0] through PUSHW[7]
38
+ SSW = 0x1F # Set Single Width
39
+ SSWCI = 0x1E # Set Single Width Cut-In
40
+ SCVTCI = 0x1D # Set CVT Cut-In
41
+ WCVTP = 0x44 # Write CVT in Pixels
42
+ WCVTF = 0x70 # Write CVT in FUnits
43
+
44
+ # Size thresholds for instruction selection
45
+ MAX_PUSHB_INLINE = 8 # Maximum bytes for PUSHB[n]
46
+ MAX_PUSHW_INLINE = 8 # Maximum words for PUSHW[n]
47
+ BYTE_MAX = 255 # Maximum value for byte
48
+ WORD_MAX = 65535 # Maximum value for word
49
+
50
+ # Generate TrueType programs and CVT from PostScript parameters
51
+ #
52
+ # @param ps_params [Hash] PostScript hint parameters
53
+ # @option ps_params [Float] :blue_scale Blue scale value (0.0-1.0)
54
+ # @option ps_params [Integer] :std_hw Standard horizontal width
55
+ # @option ps_params [Integer] :std_vw Standard vertical width
56
+ # @option ps_params [Array<Integer>] :stem_snap_h Horizontal stem snap values
57
+ # @option ps_params [Array<Integer>] :stem_snap_v Vertical stem snap values
58
+ # @option ps_params [Array<Integer>] :blue_values Blue zone values
59
+ # @option ps_params [Array<Integer>] :other_blues Other blue zone values
60
+ # @return [Hash] Hash with :prep, :fpgm, and :cvt keys
61
+ def generate(ps_params)
62
+ # Normalize keys to symbols
63
+ ps_params = normalize_keys(ps_params)
64
+
65
+ {
66
+ fpgm: generate_fpgm(ps_params),
67
+ prep: generate_prep(ps_params),
68
+ cvt: generate_cvt(ps_params)
69
+ }
70
+ end
71
+
72
+ # Generate prep (Control Value Program) from PostScript parameters
73
+ #
74
+ # The prep program sets up global hint parameters:
75
+ # - CVT Cut-In (from blue_scale)
76
+ # - Single Width Cut-In (from std_hw/std_vw)
77
+ # - Single Width (from std_hw or std_vw)
78
+ #
79
+ # @param ps_params [Hash] PostScript parameters
80
+ # @return [String] Binary instruction bytes
81
+ def generate_prep(ps_params)
82
+ instructions = []
83
+
84
+ # Set CVT Cut-In from blue_scale if present
85
+ if ps_params[:blue_scale]
86
+ cvt_cut_in = calculate_cvt_cut_in(ps_params[:blue_scale])
87
+ instructions.concat(push_value(cvt_cut_in))
88
+ instructions << SCVTCI
89
+ end
90
+
91
+ # Set Single Width Cut-In if we have stem widths
92
+ if ps_params[:std_hw] || ps_params[:std_vw]
93
+ sw_cut_in = calculate_sw_cut_in(ps_params)
94
+ instructions.concat(push_value(sw_cut_in))
95
+ instructions << SSWCI
96
+ end
97
+
98
+ # Set Single Width (prefer horizontal, fall back to vertical)
99
+ single_width = ps_params[:std_hw] || ps_params[:std_vw]
100
+ if single_width
101
+ instructions.concat(push_value(single_width))
102
+ instructions << SSW
103
+ end
104
+
105
+ instructions.pack("C*")
106
+ end
107
+
108
+ # Generate fpgm (Font Program) from PostScript parameters
109
+ #
110
+ # For converted fonts, fpgm is typically empty as font-level
111
+ # functions are not needed for basic hint conversion.
112
+ #
113
+ # @param _ps_params [Hash] PostScript parameters (unused)
114
+ # @return [String] Binary instruction bytes (empty for converted fonts)
115
+ def generate_fpgm(_ps_params)
116
+ # For converted fonts, fpgm is typically empty
117
+ # Advanced implementations might generate function definitions here
118
+ "".b
119
+ end
120
+
121
+ # Generate CVT (Control Value Table) from PostScript parameters
122
+ #
123
+ # CVT entries are derived from:
124
+ # - stem_snap_h/stem_snap_v: Stem widths
125
+ # - blue_values/other_blues: Alignment zones
126
+ # - std_hw/std_vw: Standard widths
127
+ #
128
+ # Duplicates are removed and values sorted for optimal CVT organization.
129
+ #
130
+ # @param ps_params [Hash] PostScript parameters
131
+ # @return [Array<Integer>] Array of 16-bit signed integers
132
+ def generate_cvt(ps_params)
133
+ cvt = []
134
+
135
+ # Add standard widths to CVT
136
+ cvt << ps_params[:std_hw] if ps_params[:std_hw]
137
+ cvt << ps_params[:std_vw] if ps_params[:std_vw]
138
+
139
+ # Add stem snap values
140
+ if ps_params[:stem_snap_h]
141
+ cvt.concat(ps_params[:stem_snap_h])
142
+ end
143
+
144
+ if ps_params[:stem_snap_v]
145
+ cvt.concat(ps_params[:stem_snap_v])
146
+ end
147
+
148
+ # Add blue zone values (as pairs: bottom, top)
149
+ if ps_params[:blue_values]
150
+ cvt.concat(ps_params[:blue_values])
151
+ end
152
+
153
+ if ps_params[:other_blues]
154
+ cvt.concat(ps_params[:other_blues])
155
+ end
156
+
157
+ # Remove duplicates and sort for optimal CVT organization
158
+ cvt.uniq.sort
159
+ end
160
+
161
+ private
162
+
163
+ # Normalize hash keys to symbols
164
+ #
165
+ # @param hash [Hash] Input hash with string or symbol keys
166
+ # @return [Hash] Hash with symbol keys
167
+ def normalize_keys(hash)
168
+ return hash unless hash.is_a?(Hash)
169
+ return hash if hash.empty? || hash.keys.first.is_a?(Symbol)
170
+
171
+ hash.transform_keys(&:to_sym)
172
+ end
173
+
174
+ # Calculate CVT Cut-In from PostScript blue_scale
175
+ #
176
+ # Blue scale controls the threshold at which alignment zones apply.
177
+ # We convert this to TrueType's CVT Cut-In value.
178
+ #
179
+ # @param blue_scale [Float] PostScript blue scale (0.0-1.0)
180
+ # @return [Integer] CVT Cut-In value in pixels
181
+ def calculate_cvt_cut_in(blue_scale)
182
+ # blue_scale of 0.039625 (common default) maps to ~17px cut-in
183
+ # Linear scaling: 0.039625 -> 17, 0.0 -> 0, 1.0 -> 428
184
+ (blue_scale * 428).round.clamp(0, 255)
185
+ end
186
+
187
+ # Calculate Single Width Cut-In from stem widths
188
+ #
189
+ # The cut-in determines when to apply single-width rounding.
190
+ # We use 9 pixels as a sensible default.
191
+ #
192
+ # @param _ps_params [Hash] PostScript parameters (for future use)
193
+ # @return [Integer] Single Width Cut-In in pixels
194
+ def calculate_sw_cut_in(_ps_params)
195
+ 9 # Standard value: 9 pixels
196
+ end
197
+
198
+ # Push a single value onto the TrueType stack
199
+ #
200
+ # Selects the most efficient instruction based on value size.
201
+ #
202
+ # @param value [Integer] Value to push
203
+ # @return [Array<Integer>] Instruction bytes
204
+ def push_value(value)
205
+ if value <= BYTE_MAX
206
+ push_bytes([value])
207
+ else
208
+ push_words([value])
209
+ end
210
+ end
211
+
212
+ # Push byte values using most efficient instruction
213
+ #
214
+ # Uses PUSHB[n] for 1-8 values, NPUSHB for more.
215
+ #
216
+ # @param values [Array<Integer>] Byte values (0-255)
217
+ # @return [Array<Integer>] Instruction bytes
218
+ def push_bytes(values)
219
+ return [] if values.empty?
220
+
221
+ # Validate all values fit in bytes
222
+ unless values.all? { |v| v >= 0 && v <= BYTE_MAX }
223
+ raise ArgumentError, "Values must be in range 0-255 for PUSHB"
224
+ end
225
+
226
+ count = values.size
227
+
228
+ if count <= MAX_PUSHB_INLINE
229
+ # Use PUSHB[n-1] for 1-8 values
230
+ [PUSHB_BASE + count - 1] + values
231
+ else
232
+ # Use NPUSHB for more than 8 values
233
+ [NPUSHB, count] + values
234
+ end
235
+ end
236
+
237
+ # Push word values using most efficient instruction
238
+ #
239
+ # Uses PUSHW[n] for 1-8 values, NPUSHW for more.
240
+ # Words are encoded big-endian (high byte first).
241
+ #
242
+ # @param values [Array<Integer>] Word values (0-65535)
243
+ # @return [Array<Integer>] Instruction bytes
244
+ def push_words(values)
245
+ return [] if values.empty?
246
+
247
+ # Validate all values fit in words
248
+ unless values.all? { |v| v >= 0 && v <= WORD_MAX }
249
+ raise ArgumentError, "Values must be in range 0-65535 for PUSHW"
250
+ end
251
+
252
+ count = values.size
253
+ # Convert words to big-endian byte pairs
254
+ word_bytes = values.flat_map { |v| [(v >> 8) & 0xFF, v & 0xFF] }
255
+
256
+ if count <= MAX_PUSHW_INLINE
257
+ # Use PUSHW[n-1] for 1-8 values
258
+ [PUSHW_BASE + count - 1] + word_bytes
259
+ else
260
+ # Use NPUSHW for more than 8 values
261
+ [NPUSHW, count] + word_bytes
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end
@@ -28,12 +28,12 @@ module Fontisan
28
28
  MODES = {
29
29
  METADATA => {
30
30
  tables: %w[name head hhea maxp OS/2 post].freeze,
31
- description: "Metadata mode - loads only identification and metrics tables (otfinfo-equivalent)"
31
+ description: "Metadata mode - loads only identification and metrics tables (otfinfo-equivalent)",
32
32
  }.freeze,
33
33
  FULL => {
34
34
  tables: :all,
35
- description: "Full mode - loads all tables in the font"
36
- }.freeze
35
+ description: "Full mode - loads all tables in the font",
36
+ }.freeze,
37
37
  }.freeze
38
38
 
39
39
  # Pre-computed Set for O(1) lookup of metadata tables
@@ -80,7 +80,7 @@ module Fontisan
80
80
  # @raise [ArgumentError] if mode is invalid
81
81
  def self.default_lazy?(mode)
82
82
  validate_mode!(mode)
83
- true # Lazy loading is recommended for all modes
83
+ true # Lazy loading is recommended for all modes
84
84
  end
85
85
 
86
86
  # Get mode description
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require_relative "font_info"
5
+
6
+ module Fontisan
7
+ module Models
8
+ # Model for collection brief information
9
+ #
10
+ # Represents collection metadata plus brief info for each font.
11
+ # Used by InfoCommand in brief mode for collections.
12
+ #
13
+ # @example Creating collection brief info
14
+ # info = CollectionBriefInfo.new(
15
+ # collection_path: "fonts.ttc",
16
+ # collection_type: "TTC",
17
+ # collection_version: "1.0",
18
+ # num_fonts: 3,
19
+ # fonts: [font_info1, font_info2, font_info3]
20
+ # )
21
+ class CollectionBriefInfo < Lutaml::Model::Serializable
22
+ attribute :collection_path, :string
23
+ attribute :collection_type, :string
24
+ attribute :collection_version, :string
25
+ attribute :num_fonts, :integer
26
+ attribute :fonts, FontInfo, collection: true
27
+
28
+ key_value do
29
+ map "collection_path", to: :collection_path
30
+ map "collection_type", to: :collection_type
31
+ map "collection_version", to: :collection_version
32
+ map "num_fonts", to: :num_fonts
33
+ map "fonts", to: :fonts
34
+ end
35
+ end
36
+ end
37
+ end