fontisan 0.2.1 → 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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +57 -385
  3. data/README.adoc +1483 -1435
  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/cli.rb +10 -3
  9. data/lib/fontisan/collection/builder.rb +2 -1
  10. data/lib/fontisan/collection/offset_calculator.rb +2 -0
  11. data/lib/fontisan/commands/base_command.rb +5 -2
  12. data/lib/fontisan/commands/convert_command.rb +6 -2
  13. data/lib/fontisan/commands/info_command.rb +111 -5
  14. data/lib/fontisan/commands/instance_command.rb +8 -7
  15. data/lib/fontisan/commands/validate_command.rb +4 -1
  16. data/lib/fontisan/constants.rb +24 -24
  17. data/lib/fontisan/converters/format_converter.rb +8 -4
  18. data/lib/fontisan/converters/outline_converter.rb +21 -16
  19. data/lib/fontisan/converters/woff_writer.rb +8 -3
  20. data/lib/fontisan/font_loader.rb +11 -4
  21. data/lib/fontisan/font_writer.rb +2 -0
  22. data/lib/fontisan/formatters/text_formatter.rb +45 -1
  23. data/lib/fontisan/hints/hint_converter.rb +43 -47
  24. data/lib/fontisan/hints/hint_validator.rb +284 -0
  25. data/lib/fontisan/hints/postscript_hint_applier.rb +1 -3
  26. data/lib/fontisan/hints/postscript_hint_extractor.rb +78 -43
  27. data/lib/fontisan/hints/truetype_hint_extractor.rb +22 -26
  28. data/lib/fontisan/hints/truetype_instruction_analyzer.rb +261 -0
  29. data/lib/fontisan/hints/truetype_instruction_generator.rb +266 -0
  30. data/lib/fontisan/loading_modes.rb +4 -4
  31. data/lib/fontisan/models/collection_brief_info.rb +31 -0
  32. data/lib/fontisan/models/font_export.rb +2 -2
  33. data/lib/fontisan/models/font_info.rb +3 -30
  34. data/lib/fontisan/models/hint.rb +22 -23
  35. data/lib/fontisan/models/outline.rb +4 -1
  36. data/lib/fontisan/models/validation_report.rb +1 -1
  37. data/lib/fontisan/open_type_font.rb +3 -1
  38. data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
  39. data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
  40. data/lib/fontisan/pipeline/output_writer.rb +8 -3
  41. data/lib/fontisan/pipeline/transformation_pipeline.rb +8 -3
  42. data/lib/fontisan/subset/table_subsetter.rb +5 -5
  43. data/lib/fontisan/tables/cff/charstring.rb +38 -12
  44. data/lib/fontisan/tables/cff/charstring_parser.rb +23 -11
  45. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +14 -14
  46. data/lib/fontisan/tables/cff/dict_builder.rb +4 -1
  47. data/lib/fontisan/tables/cff/hint_operation_injector.rb +6 -4
  48. data/lib/fontisan/tables/cff/offset_recalculator.rb +1 -1
  49. data/lib/fontisan/tables/cff/private_dict_writer.rb +10 -4
  50. data/lib/fontisan/tables/cff/table_builder.rb +1 -1
  51. data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
  52. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +7 -6
  53. data/lib/fontisan/tables/cff2/region_matcher.rb +2 -2
  54. data/lib/fontisan/tables/cff2/table_builder.rb +26 -20
  55. data/lib/fontisan/tables/cff2/table_reader.rb +35 -33
  56. data/lib/fontisan/tables/cff2/variation_data_extractor.rb +2 -2
  57. data/lib/fontisan/tables/cff2.rb +1 -1
  58. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +2 -1
  59. data/lib/fontisan/tables/glyf/curve_converter.rb +10 -4
  60. data/lib/fontisan/tables/glyf/glyph_builder.rb +27 -10
  61. data/lib/fontisan/tables/name.rb +4 -4
  62. data/lib/fontisan/true_type_font.rb +3 -1
  63. data/lib/fontisan/validation/checksum_validator.rb +2 -2
  64. data/lib/fontisan/variation/cache.rb +3 -1
  65. data/lib/fontisan/variation/converter.rb +2 -1
  66. data/lib/fontisan/variation/delta_applier.rb +2 -1
  67. data/lib/fontisan/variation/inspector.rb +2 -1
  68. data/lib/fontisan/variation/instance_generator.rb +2 -1
  69. data/lib/fontisan/variation/optimizer.rb +6 -3
  70. data/lib/fontisan/variation/subsetter.rb +32 -10
  71. data/lib/fontisan/variation/variation_preserver.rb +4 -1
  72. data/lib/fontisan/version.rb +1 -1
  73. data/lib/fontisan/woff2/glyf_transformer.rb +57 -30
  74. data/lib/fontisan/woff2_font.rb +31 -15
  75. data/lib/fontisan.rb +42 -2
  76. data/scripts/measure_optimization.rb +15 -7
  77. metadata +8 -2
@@ -82,11 +82,11 @@ module Fontisan
82
82
  when :postscript
83
83
  # Convert font-level TT → PS
84
84
  if hint_set.font_program || hint_set.control_value_program ||
85
- hint_set.control_values&.any?
85
+ hint_set.control_values&.any?
86
86
  ps_dict = convert_tt_programs_to_ps_dict(
87
87
  hint_set.font_program,
88
88
  hint_set.control_value_program,
89
- hint_set.control_values
89
+ hint_set.control_values,
90
90
  )
91
91
  result.private_dict_hints = ps_dict.to_json
92
92
  end
@@ -102,7 +102,7 @@ module Fontisan
102
102
  # Convert font-level PS → TT
103
103
  if hint_set.private_dict_hints && hint_set.private_dict_hints != "{}"
104
104
  tt_programs = convert_ps_dict_to_tt_programs(
105
- JSON.parse(hint_set.private_dict_hints)
105
+ JSON.parse(hint_set.private_dict_hints),
106
106
  )
107
107
  result.font_program = tt_programs[:fpgm]
108
108
  result.control_value_program = tt_programs[:prep]
@@ -137,7 +137,7 @@ module Fontisan
137
137
  Models::Hint.new(
138
138
  type: hint.type,
139
139
  data: ps_data,
140
- source_format: :postscript
140
+ source_format: :postscript,
141
141
  )
142
142
  rescue StandardError => e
143
143
  warn "Failed to convert hint to PostScript: #{e.message}"
@@ -158,7 +158,7 @@ module Fontisan
158
158
  Models::Hint.new(
159
159
  type: hint.type,
160
160
  data: { instructions: tt_instructions },
161
- source_format: :truetype
161
+ source_format: :truetype,
162
162
  )
163
163
  rescue StandardError => e
164
164
  warn "Failed to convert hint to TrueType: #{e.message}"
@@ -230,7 +230,8 @@ module Fontisan
230
230
  # Convert TrueType font programs to PostScript Private dict
231
231
  #
232
232
  # Analyzes TrueType fpgm, prep, and cvt to extract semantic intent
233
- # and generate corresponding PostScript hint parameters.
233
+ # and generate corresponding PostScript hint parameters using the
234
+ # TrueTypeInstructionAnalyzer.
234
235
  #
235
236
  # @param fpgm [String] Font program bytecode
236
237
  # @param prep [String] Control value program bytecode
@@ -239,8 +240,8 @@ module Fontisan
239
240
  def convert_tt_programs_to_ps_dict(fpgm, prep, cvt)
240
241
  hints = {}
241
242
 
242
- # Extract stem widths from cvt if present
243
- # CVT values typically contain standard widths
243
+ # Extract stem widths from CVT if present
244
+ # CVT values typically contain standard widths at the beginning
244
245
  if cvt && !cvt.empty?
245
246
  # First CVT value often represents standard horizontal stem
246
247
  hints[:std_hw] = cvt[0].abs if cvt.length > 0
@@ -248,14 +249,36 @@ module Fontisan
248
249
  hints[:std_vw] = cvt[1].abs if cvt.length > 1
249
250
  end
250
251
 
251
- # Analyze control value program for alignment zones
252
- # TrueType doesn't have exact Blue zones, so we use defaults
252
+ # Use the instruction analyzer to extract additional hint parameters
253
+ analyzer = TrueTypeInstructionAnalyzer.new
254
+
255
+ # Analyze prep program if present
256
+ prep_hints = if prep && !prep.empty?
257
+ analyzer.analyze_prep(prep, cvt)
258
+ else
259
+ {}
260
+ end
261
+
262
+ # Analyze fpgm program complexity
263
+ fpgm_hints = if fpgm && !fpgm.empty?
264
+ analyzer.analyze_fpgm(fpgm)
265
+ else
266
+ {}
267
+ end
268
+
269
+ # Extract blue zones from CVT if present
270
+ blue_zones = if cvt && !cvt.empty?
271
+ analyzer.extract_blue_zones_from_cvt(cvt)
272
+ else
273
+ {}
274
+ end
275
+
276
+ # Merge all extracted hints (prep_hints and fpgm_hints override stem widths if present)
277
+ hints.merge!(prep_hints).merge!(fpgm_hints).merge!(blue_zones)
278
+
279
+ # Provide default blue_values if none were detected
253
280
  # These are standard values that work for most Latin fonts
254
- hints[:blue_values] = [-20, 0, 706, 726]
255
-
256
- # Optional: Add other_blues for descenders if we detect them
257
- # This would require analyzing prep program, which is complex
258
- # For now, use conservative defaults
281
+ hints[:blue_values] ||= [-20, 0, 706, 726]
259
282
 
260
283
  hints
261
284
  rescue StandardError => e
@@ -266,44 +289,17 @@ module Fontisan
266
289
  # Convert PostScript Private dict to TrueType font programs
267
290
  #
268
291
  # Generates TrueType control values and programs from PostScript
269
- # hint parameters.
292
+ # hint parameters using the TrueTypeInstructionGenerator.
270
293
  #
271
294
  # @param ps_dict [Hash] PostScript Private dict parameters
272
295
  # @return [Hash] TrueType programs ({ fpgm:, prep:, cvt: })
273
296
  def convert_ps_dict_to_tt_programs(ps_dict)
274
- # Handle both string and symbol keys from JSON
275
- ps_dict = ps_dict.transform_keys(&:to_sym) if ps_dict.keys.first.is_a?(String)
276
-
277
- # Generate control values from PS parameters
278
- cvt = []
279
-
280
- # Add standard stem widths as CVT values
281
- cvt << ps_dict[:std_hw] if ps_dict[:std_hw]
282
- cvt << ps_dict[:std_vw] if ps_dict[:std_vw]
283
-
284
- # Add stem snap values if present
285
- if ps_dict[:stem_snap_h]&.is_a?(Array)
286
- cvt.concat(ps_dict[:stem_snap_h])
287
- end
288
- if ps_dict[:stem_snap_v]&.is_a?(Array)
289
- cvt.concat(ps_dict[:stem_snap_v])
290
- end
291
-
292
- # Remove duplicates and sort
293
- cvt = cvt.uniq.sort
294
-
295
- # Generate basic prep program (empty for converted fonts)
296
- # A real implementation would generate instructions to set up CVT
297
- prep = ""
298
-
299
- # fpgm typically empty for converted fonts
300
- # Functions would need to be synthesized from scratch
301
- fpgm = ""
302
-
303
- { fpgm: fpgm, prep: prep, cvt: cvt }
297
+ # Use the instruction generator to create real TrueType programs
298
+ generator = TrueTypeInstructionGenerator.new
299
+ generator.generate(ps_dict)
304
300
  rescue StandardError => e
305
301
  warn "Error converting PS dict to TT programs: #{e.message}"
306
- { fpgm: "", prep: "", cvt: [] }
302
+ { fpgm: "".b, prep: "".b, cvt: [] }
307
303
  end
308
304
  end
309
305
  end
@@ -0,0 +1,284 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Hints
5
+ # Validates hint data for correctness and compatibility
6
+ #
7
+ # This validator ensures that hints are well-formed and compatible with
8
+ # their target format. It performs multiple levels of validation:
9
+ # - TrueType instruction bytecode validation
10
+ # - PostScript hint parameter validation
11
+ # - Stack neutrality verification
12
+ # - Hint-outline compatibility checking
13
+ #
14
+ # @example Validate TrueType instructions
15
+ # validator = HintValidator.new
16
+ # result = validator.validate_truetype_instructions(prep_bytes)
17
+ # if result[:valid]
18
+ # puts "Valid TrueType instructions"
19
+ # else
20
+ # puts "Errors: #{result[:errors]}"
21
+ # end
22
+ #
23
+ # @example Validate PostScript hints
24
+ # validator = HintValidator.new
25
+ # result = validator.validate_postscript_hints(ps_dict)
26
+ # puts result[:warnings] if result[:warnings].any?
27
+ class HintValidator
28
+ # Maximum allowed values for PostScript hint parameters
29
+ MAX_BLUE_VALUES = 14 # 7 pairs
30
+ MAX_OTHER_BLUES = 10 # 5 pairs
31
+ MAX_STEM_SNAP = 12 # Maximum stem snap entries
32
+
33
+ # Validate TrueType instruction bytecode
34
+ #
35
+ # Checks for:
36
+ # - Valid instruction opcodes
37
+ # - Correct parameter counts
38
+ # - Stack neutrality
39
+ #
40
+ # @param instructions [String] Binary instruction bytes
41
+ # @return [Hash] Validation result with :valid, :errors, :warnings keys
42
+ def validate_truetype_instructions(instructions)
43
+ return { valid: true, errors: [], warnings: [] } if instructions.nil? || instructions.empty?
44
+
45
+ errors = []
46
+ warnings = []
47
+
48
+ begin
49
+ bytes = instructions.bytes
50
+ stack_depth = 0
51
+ index = 0
52
+
53
+ while index < bytes.length
54
+ opcode = bytes[index]
55
+ index += 1
56
+
57
+ case opcode
58
+ when 0x40 # NPUSHB
59
+ count = bytes[index]
60
+ index += 1
61
+ if index + count > bytes.length
62
+ errors << "NPUSHB: Not enough bytes (need #{count}, have #{bytes.length - index})"
63
+ break
64
+ end
65
+ stack_depth += count
66
+ index += count
67
+
68
+ when 0x41 # NPUSHW
69
+ count = bytes[index]
70
+ index += 1
71
+ if index + (count * 2) > bytes.length
72
+ errors << "NPUSHW: Not enough bytes (need #{count * 2}, have #{bytes.length - index})"
73
+ break
74
+ end
75
+ stack_depth += count
76
+ index += count * 2
77
+
78
+ when 0xB0..0xB7 # PUSHB[0-7]
79
+ count = opcode - 0xB0 + 1
80
+ if index + count > bytes.length
81
+ errors << "PUSHB[#{count - 1}]: Not enough bytes"
82
+ break
83
+ end
84
+ stack_depth += count
85
+ index += count
86
+
87
+ when 0xB8..0xBF # PUSHW[0-7]
88
+ count = opcode - 0xB8 + 1
89
+ if index + (count * 2) > bytes.length
90
+ errors << "PUSHW[#{count - 1}]: Not enough bytes"
91
+ break
92
+ end
93
+ stack_depth += count
94
+ index += count * 2
95
+
96
+ when 0x1D, 0x1E, 0x1F # SCVTCI, SSWCI, SSW
97
+ if stack_depth < 1
98
+ errors << "#{opcode_name(opcode)}: Stack underflow"
99
+ end
100
+ stack_depth -= 1
101
+
102
+ when 0x44, 0x70 # WCVTP, WCVTF
103
+ if stack_depth < 2
104
+ errors << "#{opcode_name(opcode)}: Stack underflow (need 2 values)"
105
+ end
106
+ stack_depth -= 2
107
+
108
+ else
109
+ warnings << "Unknown opcode: 0x#{opcode.to_s(16).upcase} at offset #{index - 1}"
110
+ end
111
+ end
112
+
113
+ # Check stack neutrality
114
+ if stack_depth != 0
115
+ warnings << "Stack not neutral: #{stack_depth} value(s) remaining"
116
+ end
117
+
118
+ rescue StandardError => e
119
+ errors << "Exception during validation: #{e.message}"
120
+ end
121
+
122
+ {
123
+ valid: errors.empty?,
124
+ errors: errors,
125
+ warnings: warnings,
126
+ }
127
+ end
128
+
129
+ # Validate PostScript hint parameters
130
+ #
131
+ # Checks for:
132
+ # - Valid parameter ranges
133
+ # - Proper pair counts for blue zones
134
+ # - Sensible stem width values
135
+ #
136
+ # @param hints [Hash] PostScript hint parameters
137
+ # @return [Hash] Validation result with :valid, :errors, :warnings keys
138
+ def validate_postscript_hints(hints)
139
+ errors = []
140
+ warnings = []
141
+
142
+ # Validate blue_values
143
+ if hints[:blue_values]
144
+ blue_values = hints[:blue_values]
145
+ if blue_values.length > MAX_BLUE_VALUES
146
+ errors << "blue_values exceeds maximum (#{MAX_BLUE_VALUES}): #{blue_values.length}"
147
+ end
148
+ if blue_values.length.odd?
149
+ errors << "blue_values must be pairs (even count): #{blue_values.length}"
150
+ end
151
+ end
152
+
153
+ # Validate other_blues
154
+ if hints[:other_blues]
155
+ other_blues = hints[:other_blues]
156
+ if other_blues.length > MAX_OTHER_BLUES
157
+ errors << "other_blues exceeds maximum (#{MAX_OTHER_BLUES}): #{other_blues.length}"
158
+ end
159
+ if other_blues.length.odd?
160
+ errors << "other_blues must be pairs (even count): #{other_blues.length}"
161
+ end
162
+ end
163
+
164
+ # Validate stem widths
165
+ [:std_hw, :std_vw].each do |key|
166
+ if hints[key] && hints[key] <= 0
167
+ errors << "#{key} must be positive: #{hints[key]}"
168
+ end
169
+ end
170
+
171
+ # Validate stem snaps
172
+ [:stem_snap_h, :stem_snap_v].each do |key|
173
+ if hints[key]
174
+ if hints[key].length > MAX_STEM_SNAP
175
+ errors << "#{key} exceeds maximum (#{MAX_STEM_SNAP}): #{hints[key].length}"
176
+ end
177
+ if hints[key].any? { |v| v <= 0 }
178
+ warnings << "#{key} contains non-positive values"
179
+ end
180
+ end
181
+ end
182
+
183
+ # Validate blue_scale
184
+ if hints[:blue_scale]
185
+ if hints[:blue_scale] <= 0
186
+ errors << "blue_scale must be positive: #{hints[:blue_scale]}"
187
+ end
188
+ if hints[:blue_scale] > 1.0
189
+ warnings << "blue_scale unusually large (>1.0): #{hints[:blue_scale]}"
190
+ end
191
+ end
192
+
193
+ # Validate language_group
194
+ if hints[:language_group]
195
+ unless [0, 1].include?(hints[:language_group])
196
+ errors << "language_group must be 0 (Latin) or 1 (CJK): #{hints[:language_group]}"
197
+ end
198
+ end
199
+
200
+ {
201
+ valid: errors.empty?,
202
+ errors: errors,
203
+ warnings: warnings,
204
+ }
205
+ end
206
+
207
+ # Validate stack neutrality of instruction sequence
208
+ #
209
+ # Ensures the instruction sequence leaves the stack in the same state
210
+ # as it started (net stack change of zero).
211
+ #
212
+ # @param instructions [String] Binary instruction bytes
213
+ # @return [Hash] Result with :neutral, :stack_depth, :errors keys
214
+ def validate_stack_neutrality(instructions)
215
+ return { neutral: true, stack_depth: 0, errors: [] } if instructions.nil? || instructions.empty?
216
+
217
+ errors = []
218
+ stack_depth = 0
219
+ bytes = instructions.bytes
220
+ index = 0
221
+
222
+ begin
223
+ while index < bytes.length
224
+ opcode = bytes[index]
225
+ index += 1
226
+
227
+ case opcode
228
+ when 0x40 # NPUSHB
229
+ count = bytes[index]
230
+ index += 1 + count
231
+ stack_depth += count
232
+
233
+ when 0x41 # NPUSHW
234
+ count = bytes[index]
235
+ index += 1 + (count * 2)
236
+ stack_depth += count
237
+
238
+ when 0xB0..0xB7 # PUSHB[0-7]
239
+ count = opcode - 0xB0 + 1
240
+ index += count
241
+ stack_depth += count
242
+
243
+ when 0xB8..0xBF # PUSHW[0-7]
244
+ count = opcode - 0xB8 + 1
245
+ index += count * 2
246
+ stack_depth += count
247
+
248
+ when 0x1D, 0x1E, 0x1F # SCVTCI, SSWCI, SSW
249
+ stack_depth -= 1
250
+
251
+ when 0x44, 0x70 # WCVTP, WCVTF
252
+ stack_depth -= 2
253
+ end
254
+ end
255
+ rescue StandardError => e
256
+ errors << "Error analyzing stack: #{e.message}"
257
+ end
258
+
259
+ {
260
+ neutral: stack_depth == 0,
261
+ stack_depth: stack_depth,
262
+ errors: errors,
263
+ }
264
+ end
265
+
266
+ private
267
+
268
+ # Get human-readable name for opcode
269
+ #
270
+ # @param opcode [Integer] Instruction opcode
271
+ # @return [String] Opcode name
272
+ def opcode_name(opcode)
273
+ case opcode
274
+ when 0x1D then "SCVTCI"
275
+ when 0x1E then "SSWCI"
276
+ when 0x1F then "SSW"
277
+ when 0x44 then "WCVTP"
278
+ when 0x70 then "WCVTF"
279
+ else "0x#{opcode.to_s(16).upcase}"
280
+ end
281
+ end
282
+ end
283
+ end
284
+ end
@@ -147,14 +147,12 @@ module Fontisan
147
147
  # Prepare per-glyph hint data if present
148
148
  per_glyph_hints = if has_per_glyph_hints
149
149
  extract_per_glyph_hints(hint_set)
150
- else
151
- nil
152
150
  end
153
151
 
154
152
  new_cff_data = Tables::Cff::TableBuilder.rebuild(
155
153
  cff_table,
156
154
  private_dict_hints: hint_params,
157
- per_glyph_hints: per_glyph_hints
155
+ per_glyph_hints: per_glyph_hints,
158
156
  )
159
157
 
160
158
  tables["CFF "] = new_cff_data
@@ -91,12 +91,12 @@ module Fontisan
91
91
  if operator?(byte)
92
92
  # Process operator
93
93
  operator = if byte == 12
94
- # Two-byte operator
95
- i += 1
96
- (12 << 8) | bytes[i]
97
- else
98
- byte
99
- end
94
+ # Two-byte operator
95
+ i += 1
96
+ (12 << 8) | bytes[i]
97
+ else
98
+ byte
99
+ end
100
100
 
101
101
  hint = process_operator(operator, stack)
102
102
  hints << hint if hint
@@ -165,7 +165,7 @@ module Fontisan
165
165
  # 5-byte signed integer
166
166
  if index + 4 < bytes.length
167
167
  num = (bytes[index + 1] << 24) | (bytes[index + 2] << 16) |
168
- (bytes[index + 3] << 8) | bytes[index + 4]
168
+ (bytes[index + 3] << 8) | bytes[index + 4]
169
169
  num = num - 4294967296 if num > 2147483647
170
170
  [num, 5]
171
171
  else
@@ -204,7 +204,7 @@ module Fontisan
204
204
  Models::Hint.new(
205
205
  type: :hint_replacement,
206
206
  data: { mask: stack.dup },
207
- source_format: :postscript
207
+ source_format: :postscript,
208
208
  )
209
209
 
210
210
  when CNTRMASK
@@ -212,11 +212,9 @@ module Fontisan
212
212
  Models::Hint.new(
213
213
  type: :counter,
214
214
  data: { zones: stack.dup },
215
- source_format: :postscript
215
+ source_format: :postscript,
216
216
  )
217
217
 
218
- else
219
- nil
220
218
  end
221
219
  end
222
220
 
@@ -238,9 +236,9 @@ module Fontisan
238
236
  data: {
239
237
  position: position,
240
238
  width: width,
241
- orientation: orientation
239
+ orientation: orientation,
242
240
  },
243
- source_format: :postscript
241
+ source_format: :postscript,
244
242
  )
245
243
  end
246
244
 
@@ -258,7 +256,7 @@ module Fontisan
258
256
  pos_idx = i * 2
259
257
  stems << {
260
258
  position: stack[pos_idx],
261
- width: stack[pos_idx + 1]
259
+ width: stack[pos_idx + 1],
262
260
  }
263
261
  end
264
262
 
@@ -266,9 +264,9 @@ module Fontisan
266
264
  type: :stem3,
267
265
  data: {
268
266
  stems: stems,
269
- orientation: orientation
267
+ orientation: orientation,
270
268
  },
271
- source_format: :postscript
269
+ source_format: :postscript,
272
270
  )
273
271
  end
274
272
 
@@ -293,19 +291,58 @@ module Fontisan
293
291
 
294
292
  # Extract hint-related parameters from Private DICT
295
293
  # These are the key hinting parameters in CFF
296
- hints[:blue_values] = private_dict.blue_values if private_dict.respond_to?(:blue_values)
297
- hints[:other_blues] = private_dict.other_blues if private_dict.respond_to?(:other_blues)
298
- hints[:family_blues] = private_dict.family_blues if private_dict.respond_to?(:family_blues)
299
- hints[:family_other_blues] = private_dict.family_other_blues if private_dict.respond_to?(:family_other_blues)
300
- hints[:blue_scale] = private_dict.blue_scale if private_dict.respond_to?(:blue_scale)
301
- hints[:blue_shift] = private_dict.blue_shift if private_dict.respond_to?(:blue_shift)
302
- hints[:blue_fuzz] = private_dict.blue_fuzz if private_dict.respond_to?(:blue_fuzz)
303
- hints[:std_hw] = private_dict.std_hw if private_dict.respond_to?(:std_hw)
304
- hints[:std_vw] = private_dict.std_vw if private_dict.respond_to?(:std_vw)
305
- hints[:stem_snap_h] = private_dict.stem_snap_h if private_dict.respond_to?(:stem_snap_h)
306
- hints[:stem_snap_v] = private_dict.stem_snap_v if private_dict.respond_to?(:stem_snap_v)
307
- hints[:force_bold] = private_dict.force_bold if private_dict.respond_to?(:force_bold)
308
- hints[:language_group] = private_dict.language_group if private_dict.respond_to?(:language_group)
294
+ if private_dict.respond_to?(:blue_values)
295
+ hints[:blue_values] =
296
+ private_dict.blue_values
297
+ end
298
+ if private_dict.respond_to?(:other_blues)
299
+ hints[:other_blues] =
300
+ private_dict.other_blues
301
+ end
302
+ if private_dict.respond_to?(:family_blues)
303
+ hints[:family_blues] =
304
+ private_dict.family_blues
305
+ end
306
+ if private_dict.respond_to?(:family_other_blues)
307
+ hints[:family_other_blues] =
308
+ private_dict.family_other_blues
309
+ end
310
+ if private_dict.respond_to?(:blue_scale)
311
+ hints[:blue_scale] =
312
+ private_dict.blue_scale
313
+ end
314
+ if private_dict.respond_to?(:blue_shift)
315
+ hints[:blue_shift] =
316
+ private_dict.blue_shift
317
+ end
318
+ if private_dict.respond_to?(:blue_fuzz)
319
+ hints[:blue_fuzz] =
320
+ private_dict.blue_fuzz
321
+ end
322
+ if private_dict.respond_to?(:std_hw)
323
+ hints[:std_hw] =
324
+ private_dict.std_hw
325
+ end
326
+ if private_dict.respond_to?(:std_vw)
327
+ hints[:std_vw] =
328
+ private_dict.std_vw
329
+ end
330
+ if private_dict.respond_to?(:stem_snap_h)
331
+ hints[:stem_snap_h] =
332
+ private_dict.stem_snap_h
333
+ end
334
+ if private_dict.respond_to?(:stem_snap_v)
335
+ hints[:stem_snap_v] =
336
+ private_dict.stem_snap_v
337
+ end
338
+ if private_dict.respond_to?(:force_bold)
339
+ hints[:force_bold] =
340
+ private_dict.force_bold
341
+ end
342
+ if private_dict.respond_to?(:language_group)
343
+ hints[:language_group] =
344
+ private_dict.language_group
345
+ end
309
346
 
310
347
  hints.compact
311
348
  rescue StandardError => e
@@ -331,20 +368,18 @@ module Fontisan
331
368
  # Iterate through all glyphs
332
369
  glyph_count = cff_table.glyph_count(0)
333
370
  (0...glyph_count).each do |glyph_id|
334
- begin
335
- # Get CharString for this glyph
336
- charstring = cff_table.charstring_for_glyph(glyph_id, 0)
337
- next unless charstring
338
-
339
- # Extract hints from CharString
340
- hints = extract(charstring)
341
- next if hints.empty?
342
-
343
- # Store glyph hints
344
- hint_set.add_glyph_hints(glyph_id, hints)
345
- rescue StandardError => e
346
- warn "Failed to extract hints for glyph #{glyph_id}: #{e.message}"
347
- end
371
+ # Get CharString for this glyph
372
+ charstring = cff_table.charstring_for_glyph(glyph_id, 0)
373
+ next unless charstring
374
+
375
+ # Extract hints from CharString
376
+ hints = extract(charstring)
377
+ next if hints.empty?
378
+
379
+ # Store glyph hints
380
+ hint_set.add_glyph_hints(glyph_id, hints)
381
+ rescue StandardError => e
382
+ warn "Failed to extract hints for glyph #{glyph_id}: #{e.message}"
348
383
  end
349
384
  rescue StandardError => e
350
385
  warn "Failed to extract CharString hints: #{e.message}"