fontisan 0.2.7 → 0.2.9

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +103 -0
  3. data/.rubocop_todo.yml +65 -361
  4. data/CHANGELOG.md +116 -0
  5. data/Gemfile +1 -1
  6. data/README.adoc +106 -27
  7. data/Rakefile +12 -7
  8. data/benchmark/variation_quick_bench.rb +1 -1
  9. data/docs/APPLE_LEGACY_FONTS.adoc +173 -0
  10. data/docs/COLLECTION_VALIDATION.adoc +143 -0
  11. data/docs/COLOR_FONTS.adoc +127 -0
  12. data/docs/DOCUMENTATION_SUMMARY.md +141 -0
  13. data/docs/FONT_HINTING.adoc +9 -1
  14. data/docs/VALIDATION.adoc +254 -0
  15. data/docs/WOFF_WOFF2_FORMATS.adoc +94 -0
  16. data/lib/fontisan/cli.rb +45 -13
  17. data/lib/fontisan/collection/dfont_builder.rb +2 -1
  18. data/lib/fontisan/commands/convert_command.rb +2 -4
  19. data/lib/fontisan/commands/info_command.rb +3 -3
  20. data/lib/fontisan/commands/pack_command.rb +2 -1
  21. data/lib/fontisan/commands/validate_command.rb +157 -6
  22. data/lib/fontisan/converters/collection_converter.rb +22 -13
  23. data/lib/fontisan/converters/svg_generator.rb +2 -1
  24. data/lib/fontisan/converters/woff2_encoder.rb +6 -6
  25. data/lib/fontisan/converters/woff_writer.rb +3 -1
  26. data/lib/fontisan/font_loader.rb +7 -6
  27. data/lib/fontisan/formatters/text_formatter.rb +18 -14
  28. data/lib/fontisan/hints/hint_converter.rb +1 -1
  29. data/lib/fontisan/hints/hint_validator.rb +13 -10
  30. data/lib/fontisan/hints/truetype_instruction_analyzer.rb +15 -8
  31. data/lib/fontisan/hints/truetype_instruction_generator.rb +1 -1
  32. data/lib/fontisan/models/collection_validation_report.rb +104 -0
  33. data/lib/fontisan/models/font_report.rb +24 -0
  34. data/lib/fontisan/models/validation_report.rb +7 -2
  35. data/lib/fontisan/open_type_font.rb +18 -425
  36. data/lib/fontisan/optimizers/charstring_rewriter.rb +1 -1
  37. data/lib/fontisan/optimizers/subroutine_optimizer.rb +6 -2
  38. data/lib/fontisan/sfnt_font.rb +699 -0
  39. data/lib/fontisan/sfnt_table.rb +264 -0
  40. data/lib/fontisan/subset/glyph_mapping.rb +2 -0
  41. data/lib/fontisan/subset/table_subsetter.rb +2 -2
  42. data/lib/fontisan/tables/cblc.rb +8 -4
  43. data/lib/fontisan/tables/cff/index.rb +2 -0
  44. data/lib/fontisan/tables/cff.rb +6 -3
  45. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +1 -1
  46. data/lib/fontisan/tables/cff2.rb +1 -1
  47. data/lib/fontisan/tables/cmap.rb +5 -5
  48. data/lib/fontisan/tables/cmap_table.rb +231 -0
  49. data/lib/fontisan/tables/glyf.rb +8 -10
  50. data/lib/fontisan/tables/glyf_table.rb +255 -0
  51. data/lib/fontisan/tables/head.rb +3 -3
  52. data/lib/fontisan/tables/head_table.rb +111 -0
  53. data/lib/fontisan/tables/hhea.rb +4 -4
  54. data/lib/fontisan/tables/hhea_table.rb +255 -0
  55. data/lib/fontisan/tables/hmtx_table.rb +191 -0
  56. data/lib/fontisan/tables/loca_table.rb +212 -0
  57. data/lib/fontisan/tables/maxp.rb +2 -2
  58. data/lib/fontisan/tables/maxp_table.rb +258 -0
  59. data/lib/fontisan/tables/name.rb +1 -1
  60. data/lib/fontisan/tables/name_table.rb +176 -0
  61. data/lib/fontisan/tables/os2.rb +8 -8
  62. data/lib/fontisan/tables/os2_table.rb +329 -0
  63. data/lib/fontisan/tables/post.rb +2 -2
  64. data/lib/fontisan/tables/post_table.rb +183 -0
  65. data/lib/fontisan/tables/sbix.rb +5 -4
  66. data/lib/fontisan/true_type_font.rb +12 -464
  67. data/lib/fontisan/utilities/checksum_calculator.rb +0 -44
  68. data/lib/fontisan/validation/collection_validator.rb +4 -2
  69. data/lib/fontisan/validators/basic_validator.rb +11 -21
  70. data/lib/fontisan/validators/font_book_validator.rb +29 -50
  71. data/lib/fontisan/validators/opentype_validator.rb +24 -28
  72. data/lib/fontisan/validators/validator.rb +87 -66
  73. data/lib/fontisan/validators/web_font_validator.rb +16 -21
  74. data/lib/fontisan/version.rb +1 -1
  75. data/lib/fontisan/woff2/glyf_transformer.rb +31 -8
  76. data/lib/fontisan/woff2/hmtx_transformer.rb +2 -1
  77. data/lib/fontisan/woff2/table_transformer.rb +4 -2
  78. data/lib/fontisan/woff2_font.rb +4 -2
  79. data/lib/fontisan/woff_font.rb +46 -30
  80. data/lib/fontisan.rb +2 -2
  81. data/scripts/compare_stack_aware.rb +1 -1
  82. data/scripts/measure_optimization.rb +1 -2
  83. metadata +23 -2
@@ -36,94 +36,73 @@ module Fontisan
36
36
  super
37
37
 
38
38
  # Check 9: Name table Windows Unicode English encoding
39
- check_table :name_windows_encoding, 'name', severity: :error do |table|
39
+ check_table :name_windows_encoding, "name", severity: :error do |table|
40
40
  table.has_valid_platform_combos?([3, 1, 0x0409]) # Windows Unicode English
41
41
  end
42
42
 
43
43
  # Check 10: Name table Mac Roman English encoding
44
- check_table :name_mac_encoding, 'name', severity: :warning do |table|
44
+ check_table :name_mac_encoding, "name", severity: :warning do |table|
45
45
  table.has_valid_platform_combos?([1, 0, 0]) # Mac Roman English
46
46
  end
47
47
 
48
48
  # Check 11: OS/2 table version must be valid
49
- check_table :os2_version, 'OS/2', severity: :error do |table|
50
- table.valid_version?
51
- end
49
+ check_table :os2_version, "OS/2", severity: :error, &:valid_version?
52
50
 
53
51
  # Check 12: OS/2 weight class must be valid (1-1000)
54
- check_table :os2_weight_class, 'OS/2', severity: :error do |table|
55
- table.valid_weight_class?
56
- end
52
+ check_table :os2_weight_class, "OS/2", severity: :error,
53
+ &:valid_weight_class?
57
54
 
58
55
  # Check 13: OS/2 width class must be valid (1-9)
59
- check_table :os2_width_class, 'OS/2', severity: :error do |table|
60
- table.valid_width_class?
61
- end
56
+ check_table :os2_width_class, "OS/2", severity: :error,
57
+ &:valid_width_class?
62
58
 
63
59
  # Check 14: OS/2 vendor ID should be present
64
- check_table :os2_vendor_id, 'OS/2', severity: :warning do |table|
65
- table.has_vendor_id?
66
- end
60
+ check_table :os2_vendor_id, "OS/2", severity: :warning, &:has_vendor_id?
67
61
 
68
62
  # Check 15: OS/2 PANOSE classification should be present
69
- check_table :os2_panose, 'OS/2', severity: :info do |table|
70
- table.has_panose?
71
- end
63
+ check_table :os2_panose, "OS/2", severity: :info, &:has_panose?
72
64
 
73
65
  # Check 16: OS/2 typographic metrics must be valid
74
- check_table :os2_typo_metrics, 'OS/2', severity: :error do |table|
75
- table.valid_typo_metrics?
76
- end
66
+ check_table :os2_typo_metrics, "OS/2", severity: :error,
67
+ &:valid_typo_metrics?
77
68
 
78
69
  # Check 17: OS/2 Windows metrics must be valid
79
- check_table :os2_win_metrics, 'OS/2', severity: :error do |table|
80
- table.valid_win_metrics?
81
- end
70
+ check_table :os2_win_metrics, "OS/2", severity: :error,
71
+ &:valid_win_metrics?
82
72
 
83
73
  # Check 18: OS/2 Unicode ranges should be present
84
- check_table :os2_unicode_ranges, 'OS/2', severity: :warning do |table|
85
- table.has_unicode_ranges?
86
- end
74
+ check_table :os2_unicode_ranges, "OS/2", severity: :warning,
75
+ &:has_unicode_ranges?
87
76
 
88
77
  # Check 19: Head table bounding box must be valid
89
- check_table :head_bounding_box, 'head', severity: :error do |table|
90
- table.valid_bounding_box?
91
- end
78
+ check_table :head_bounding_box, "head", severity: :error,
79
+ &:valid_bounding_box?
92
80
 
93
81
  # Check 20: Hhea ascent/descent must be valid
94
- check_table :hhea_ascent_descent, 'hhea', severity: :error do |table|
95
- table.valid_ascent_descent?
96
- end
82
+ check_table :hhea_ascent_descent, "hhea", severity: :error,
83
+ &:valid_ascent_descent?
97
84
 
98
85
  # Check 21: Hhea line gap should be valid
99
- check_table :hhea_line_gap, 'hhea', severity: :warning do |table|
100
- table.valid_line_gap?
101
- end
86
+ check_table :hhea_line_gap, "hhea", severity: :warning,
87
+ &:valid_line_gap?
102
88
 
103
89
  # Check 22: Hhea horizontal metrics count must be valid
104
- check_table :hhea_metrics_count, 'hhea', severity: :error do |table|
105
- table.valid_number_of_h_metrics?
106
- end
90
+ check_table :hhea_metrics_count, "hhea", severity: :error,
91
+ &:valid_number_of_h_metrics?
107
92
 
108
93
  # Check 23: Post table version must be valid
109
- check_table :post_version, 'post', severity: :error do |table|
110
- table.valid_version?
111
- end
94
+ check_table :post_version, "post", severity: :error, &:valid_version?
112
95
 
113
96
  # Check 24: Post table italic angle should be valid
114
- check_table :post_italic_angle, 'post', severity: :warning do |table|
115
- table.valid_italic_angle?
116
- end
97
+ check_table :post_italic_angle, "post", severity: :warning,
98
+ &:valid_italic_angle?
117
99
 
118
100
  # Check 25: Post table underline metrics should be present
119
- check_table :post_underline, 'post', severity: :info do |table|
120
- table.has_underline_metrics?
121
- end
101
+ check_table :post_underline, "post", severity: :info,
102
+ &:has_underline_metrics?
122
103
 
123
104
  # Check 26: Cmap table must have subtables
124
- check_table :cmap_subtables, 'cmap', severity: :error do |table|
125
- table.has_subtables?
126
- end
105
+ check_table :cmap_subtables, "cmap", severity: :error, &:has_subtables?
127
106
  end
128
107
  end
129
108
  end
@@ -33,45 +33,44 @@ module Fontisan
33
33
  super
34
34
 
35
35
  # Check 27: Maxp TrueType metrics (only for version 1.0)
36
- check_table :maxp_truetype_metrics, 'maxp', severity: :warning do |table|
36
+ check_table :maxp_truetype_metrics, "maxp",
37
+ severity: :warning do |table|
37
38
  !table.version_1_0? || table.has_truetype_metrics?
38
39
  end
39
40
 
40
41
  # Check 28: Maxp max zones must be valid
41
- check_table :maxp_zones, 'maxp', severity: :error do |table|
42
- table.valid_max_zones?
43
- end
42
+ check_table :maxp_zones, "maxp", severity: :error, &:valid_max_zones?
44
43
 
45
44
  # Check 29: Glyf glyphs must be accessible (TrueType fonts only)
46
45
  check_glyphs :glyf_accessible, severity: :error do |font|
47
- glyf = font.table('glyf')
46
+ glyf = font.table("glyf")
48
47
  next true unless glyf # Skip if CFF font
49
48
 
50
- loca = font.table('loca')
51
- head = font.table('head')
52
- maxp = font.table('maxp')
49
+ loca = font.table("loca")
50
+ head = font.table("head")
51
+ maxp = font.table("maxp")
53
52
  glyf.all_glyphs_accessible?(loca, head, maxp.num_glyphs)
54
53
  end
55
54
 
56
55
  # Check 30: Glyf glyphs should not be clipped
57
56
  check_glyphs :glyf_no_clipping, severity: :warning do |font|
58
- glyf = font.table('glyf')
57
+ glyf = font.table("glyf")
59
58
  next true unless glyf
60
59
 
61
- loca = font.table('loca')
62
- head = font.table('head')
63
- maxp = font.table('maxp')
60
+ loca = font.table("loca")
61
+ head = font.table("head")
62
+ maxp = font.table("maxp")
64
63
  glyf.no_clipped_glyphs?(loca, head, maxp.num_glyphs)
65
64
  end
66
65
 
67
66
  # Check 31: Glyf contour counts must be valid
68
67
  check_glyphs :glyf_valid_contours, severity: :error do |font|
69
- glyf = font.table('glyf')
68
+ glyf = font.table("glyf")
70
69
  next true unless glyf
71
70
 
72
- loca = font.table('loca')
73
- head = font.table('head')
74
- maxp = font.table('maxp')
71
+ loca = font.table("loca")
72
+ head = font.table("head")
73
+ maxp = font.table("maxp")
75
74
 
76
75
  (0...maxp.num_glyphs).all? do |glyph_id|
77
76
  glyf.valid_contour_count?(glyph_id, loca, head)
@@ -79,29 +78,26 @@ module Fontisan
79
78
  end
80
79
 
81
80
  # Check 32: Cmap must have Unicode mapping
82
- check_table :cmap_unicode_mapping, 'cmap', severity: :error do |table|
83
- table.has_unicode_mapping?
84
- end
81
+ check_table :cmap_unicode_mapping, "cmap", severity: :error,
82
+ &:has_unicode_mapping?
85
83
 
86
84
  # Check 33: Cmap should have BMP coverage
87
- check_table :cmap_bmp_coverage, 'cmap', severity: :warning do |table|
88
- table.has_bmp_coverage?
89
- end
85
+ check_table :cmap_bmp_coverage, "cmap", severity: :warning,
86
+ &:has_bmp_coverage?
90
87
 
91
88
  # Check 34: Cmap must have format 4 subtable
92
- check_table :cmap_format4, 'cmap', severity: :error do |table|
93
- table.has_format_4_subtable?
94
- end
89
+ check_table :cmap_format4, "cmap", severity: :error,
90
+ &:has_format_4_subtable?
95
91
 
96
92
  # Check 35: Cmap glyph indices must be valid
97
93
  check_structure :cmap_glyph_indices, severity: :error do |font|
98
- cmap = font.table('cmap')
99
- maxp = font.table('maxp')
94
+ cmap = font.table("cmap")
95
+ maxp = font.table("maxp")
100
96
  cmap.valid_glyph_indices?(maxp.num_glyphs)
101
97
  end
102
98
 
103
99
  # Check 36: Table checksums (info level - many fonts have mismatches)
104
- check_structure :checksum_valid, severity: :info do |font|
100
+ check_structure :checksum_valid, severity: :info do |_font|
105
101
  # Table checksum validation (info level - for reference)
106
102
  # Most fonts have checksum mismatches, so we make it info not error
107
103
  true # Placeholder - actual checksum validation if desired
@@ -80,7 +80,8 @@ module Fontisan
80
80
  # @param block [Proc] Check logic that receives (table, value) as parameters
81
81
  def check_field(check_id, field_key, severity: :error, &block)
82
82
  unless @current_table_context
83
- raise ArgumentError, "check_field must be called within check_table block"
83
+ raise ArgumentError,
84
+ "check_field must be called within check_table block"
84
85
  end
85
86
 
86
87
  @checks << {
@@ -194,7 +195,7 @@ module Fontisan
194
195
  issues: [],
195
196
  }
196
197
  end
197
- rescue => e
198
+ rescue StandardError => e
198
199
  {
199
200
  check_id: check_def[:id],
200
201
  passed: false,
@@ -239,7 +240,7 @@ module Fontisan
239
240
 
240
241
  begin
241
242
  result = check_def[:block].call(table)
242
- passed = result != false && result != nil
243
+ passed = result != false && !result.nil?
243
244
 
244
245
  {
245
246
  check_id: check_def[:id],
@@ -247,12 +248,16 @@ module Fontisan
247
248
  severity: check_def[:severity].to_s,
248
249
  messages: passed ? [] : ["Table '#{table_tag}' validation failed"],
249
250
  table: table_tag,
250
- issues: passed ? [] : [{
251
- severity: check_def[:severity].to_s,
252
- category: "table_validation",
253
- table: table_tag,
254
- message: "Table '#{table_tag}' failed validation",
255
- }],
251
+ issues: if passed
252
+ []
253
+ else
254
+ [{
255
+ severity: check_def[:severity].to_s,
256
+ category: "table_validation",
257
+ table: table_tag,
258
+ message: "Table '#{table_tag}' failed validation",
259
+ }]
260
+ end,
256
261
  }
257
262
  ensure
258
263
  @current_table_context = old_context
@@ -290,12 +295,10 @@ module Fontisan
290
295
  # Get field value
291
296
  value = if table.respond_to?(field_key)
292
297
  table.public_send(field_key)
293
- else
294
- nil
295
298
  end
296
299
 
297
300
  result = check_def[:block].call(table, value)
298
- passed = result != false && result != nil
301
+ passed = result != false && !result.nil?
299
302
 
300
303
  {
301
304
  check_id: check_def[:id],
@@ -304,13 +307,17 @@ module Fontisan
304
307
  messages: passed ? [] : ["Field '#{field_key}' validation failed"],
305
308
  table: table_tag,
306
309
  field: field_key.to_s,
307
- issues: passed ? [] : [{
308
- severity: check_def[:severity].to_s,
309
- category: "field_validation",
310
- table: table_tag,
311
- field: field_key.to_s,
312
- message: "Field '#{field_key}' in table '#{table_tag}' failed validation",
313
- }],
310
+ issues: if passed
311
+ []
312
+ else
313
+ [{
314
+ severity: check_def[:severity].to_s,
315
+ category: "field_validation",
316
+ table: table_tag,
317
+ field: field_key.to_s,
318
+ message: "Field '#{field_key}' in table '#{table_tag}' failed validation",
319
+ }]
320
+ end,
314
321
  }
315
322
  end
316
323
 
@@ -321,18 +328,22 @@ module Fontisan
321
328
  # @return [Hash] Check result
322
329
  def execute_structure_check(font, check_def)
323
330
  result = check_def[:block].call(font)
324
- passed = result != false && result != nil
331
+ passed = result != false && !result.nil?
325
332
 
326
333
  {
327
334
  check_id: check_def[:id],
328
335
  passed: passed,
329
336
  severity: check_def[:severity].to_s,
330
337
  messages: passed ? [] : ["Structure validation failed"],
331
- issues: passed ? [] : [{
332
- severity: check_def[:severity].to_s,
333
- category: "structure",
334
- message: "Font structure validation failed for check '#{check_def[:id]}'",
335
- }],
338
+ issues: if passed
339
+ []
340
+ else
341
+ [{
342
+ severity: check_def[:severity].to_s,
343
+ category: "structure",
344
+ message: "Font structure validation failed for check '#{check_def[:id]}'",
345
+ }]
346
+ end,
336
347
  }
337
348
  end
338
349
 
@@ -343,18 +354,22 @@ module Fontisan
343
354
  # @return [Hash] Check result
344
355
  def execute_usability_check(font, check_def)
345
356
  result = check_def[:block].call(font)
346
- passed = result != false && result != nil
357
+ passed = result != false && !result.nil?
347
358
 
348
359
  {
349
360
  check_id: check_def[:id],
350
361
  passed: passed,
351
362
  severity: check_def[:severity].to_s,
352
363
  messages: passed ? [] : ["Usability check failed"],
353
- issues: passed ? [] : [{
354
- severity: check_def[:severity].to_s,
355
- category: "usability",
356
- message: "Font usability check failed for '#{check_def[:id]}'",
357
- }],
364
+ issues: if passed
365
+ []
366
+ else
367
+ [{
368
+ severity: check_def[:severity].to_s,
369
+ category: "usability",
370
+ message: "Font usability check failed for '#{check_def[:id]}'",
371
+ }]
372
+ end,
358
373
  }
359
374
  end
360
375
 
@@ -365,18 +380,22 @@ module Fontisan
365
380
  # @return [Hash] Check result
366
381
  def execute_instruction_check(font, check_def)
367
382
  result = check_def[:block].call(font)
368
- passed = result != false && result != nil
383
+ passed = result != false && !result.nil?
369
384
 
370
385
  {
371
386
  check_id: check_def[:id],
372
387
  passed: passed,
373
388
  severity: check_def[:severity].to_s,
374
389
  messages: passed ? [] : ["Instruction validation failed"],
375
- issues: passed ? [] : [{
376
- severity: check_def[:severity].to_s,
377
- category: "instructions",
378
- message: "TrueType instruction check failed for '#{check_def[:id]}'",
379
- }],
390
+ issues: if passed
391
+ []
392
+ else
393
+ [{
394
+ severity: check_def[:severity].to_s,
395
+ category: "instructions",
396
+ message: "TrueType instruction check failed for '#{check_def[:id]}'",
397
+ }]
398
+ end,
380
399
  }
381
400
  end
382
401
 
@@ -387,18 +406,22 @@ module Fontisan
387
406
  # @return [Hash] Check result
388
407
  def execute_glyph_check(font, check_def)
389
408
  result = check_def[:block].call(font)
390
- passed = result != false && result != nil
409
+ passed = result != false && !result.nil?
391
410
 
392
411
  {
393
412
  check_id: check_def[:id],
394
413
  passed: passed,
395
414
  severity: check_def[:severity].to_s,
396
415
  messages: passed ? [] : ["Glyph validation failed"],
397
- issues: passed ? [] : [{
398
- severity: check_def[:severity].to_s,
399
- category: "glyphs",
400
- message: "Glyph validation failed for check '#{check_def[:id]}'",
401
- }],
416
+ issues: if passed
417
+ []
418
+ else
419
+ [{
420
+ severity: check_def[:severity].to_s,
421
+ category: "glyphs",
422
+ message: "Glyph validation failed for check '#{check_def[:id]}'",
423
+ }]
424
+ end,
402
425
  }
403
426
  end
404
427
 
@@ -408,7 +431,7 @@ module Fontisan
408
431
  # @param all_results [Array<Hash>] All check results
409
432
  # @param elapsed [Float] Elapsed time in seconds
410
433
  # @return [ValidationReport] Complete report
411
- def build_report(font, all_results, elapsed)
434
+ def build_report(font, all_results, _elapsed)
412
435
  # Extract font path from font object
413
436
  font_path = if font.respond_to?(:path)
414
437
  font.path
@@ -438,28 +461,26 @@ module Fontisan
438
461
  report.check_results << check_result
439
462
 
440
463
  # Add issues to main report
441
- if result[:issues]
442
- result[:issues].each do |issue_data|
443
- case issue_data[:severity]
444
- when "error", "fatal"
445
- report.add_error(
446
- issue_data[:category] || "validation",
447
- issue_data[:message],
448
- issue_data[:table] || issue_data[:field],
449
- )
450
- when "warning"
451
- report.add_warning(
452
- issue_data[:category] || "validation",
453
- issue_data[:message],
454
- issue_data[:table] || issue_data[:field],
455
- )
456
- when "info"
457
- report.add_info(
458
- issue_data[:category] || "validation",
459
- issue_data[:message],
460
- issue_data[:table] || issue_data[:field],
461
- )
462
- end
464
+ result[:issues]&.each do |issue_data|
465
+ case issue_data[:severity]
466
+ when "error", "fatal"
467
+ report.add_error(
468
+ issue_data[:category] || "validation",
469
+ issue_data[:message],
470
+ issue_data[:table] || issue_data[:field],
471
+ )
472
+ when "warning"
473
+ report.add_warning(
474
+ issue_data[:category] || "validation",
475
+ issue_data[:message],
476
+ issue_data[:table] || issue_data[:field],
477
+ )
478
+ when "info"
479
+ report.add_info(
480
+ issue_data[:category] || "validation",
481
+ issue_data[:message],
482
+ issue_data[:table] || issue_data[:field],
483
+ )
463
484
  end
464
485
  end
465
486
  end
@@ -33,18 +33,16 @@ module Fontisan
33
33
  super
34
34
 
35
35
  # Check 9: OS/2 embedding permissions must allow web use
36
- check_table :embedding_permissions, 'OS/2', severity: :error do |table|
37
- table.has_embedding_permissions?
38
- end
36
+ check_table :embedding_permissions, "OS/2", severity: :error,
37
+ &:has_embedding_permissions?
39
38
 
40
39
  # Check 10: OS/2 version should be present
41
- check_table :os2_version_web, 'OS/2', severity: :warning do |table|
42
- table.valid_version?
43
- end
40
+ check_table :os2_version_web, "OS/2", severity: :warning,
41
+ &:valid_version?
44
42
 
45
43
  # Check 11: Glyph complexity should be reasonable for web
46
44
  check_glyphs :no_complex_glyphs, severity: :warning do |font|
47
- maxp = font.table('maxp')
45
+ maxp = font.table("maxp")
48
46
  next true unless maxp.version_1_0?
49
47
 
50
48
  # Check max points and contours are reasonable for web rendering
@@ -53,33 +51,30 @@ module Fontisan
53
51
  end
54
52
 
55
53
  # Check 12: Cmap must have Unicode mapping for web
56
- check_table :character_coverage, 'cmap', severity: :error do |table|
57
- table.has_unicode_mapping?
58
- end
54
+ check_table :character_coverage, "cmap", severity: :error,
55
+ &:has_unicode_mapping?
59
56
 
60
57
  # Check 13: Cmap should have BMP coverage
61
- check_table :cmap_bmp_web, 'cmap', severity: :warning do |table|
62
- table.has_bmp_coverage?
63
- end
58
+ check_table :cmap_bmp_web, "cmap", severity: :warning,
59
+ &:has_bmp_coverage?
64
60
 
65
61
  # Check 14: Glyf glyphs must be accessible (web browsers need this)
66
62
  check_glyphs :glyph_accessible_web, severity: :error do |font|
67
- glyf = font.table('glyf')
63
+ glyf = font.table("glyf")
68
64
  next true unless glyf
69
65
 
70
- loca = font.table('loca')
71
- head = font.table('head')
72
- maxp = font.table('maxp')
66
+ loca = font.table("loca")
67
+ head = font.table("head")
68
+ maxp = font.table("maxp")
73
69
  glyf.all_glyphs_accessible?(loca, head, maxp.num_glyphs)
74
70
  end
75
71
 
76
72
  # Check 15: Head table must have valid bounding box
77
- check_table :head_bbox_web, 'head', severity: :error do |table|
78
- table.valid_bounding_box?
79
- end
73
+ check_table :head_bbox_web, "head", severity: :error,
74
+ &:valid_bounding_box?
80
75
 
81
76
  # Check 16: Hhea metrics must be valid for web rendering
82
- check_table :hhea_metrics_web, 'hhea', severity: :error do |table|
77
+ check_table :hhea_metrics_web, "hhea", severity: :error do |table|
83
78
  table.valid_ascent_descent? && table.valid_number_of_h_metrics?
84
79
  end
85
80
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.2.7"
4
+ VERSION = "0.2.9"
5
5
  end
@@ -265,16 +265,33 @@ module Fontisan
265
265
  glyph_io, bbox_io, instruction_io)
266
266
  # Read end points of contours
267
267
  end_pts_of_contours = []
268
+ max_points_per_glyph = 100000 # Sanity limit
269
+
268
270
  num_contours.times do
269
271
  if end_pts_of_contours.empty?
270
- end_pts_of_contours << read_255_uint16(n_points_io)
272
+ val = read_255_uint16(n_points_io)
273
+ end_pts_of_contours << val
271
274
  else
272
275
  delta = read_255_uint16(n_points_io)
273
- end_pts_of_contours << end_pts_of_contours.last + delta + 1
276
+ next_end_pt = end_pts_of_contours.last + delta + 1
277
+
278
+ # Sanity check to prevent explosion from corrupted data
279
+ if delta > 10000 || next_end_pt > max_points_per_glyph
280
+ # Data appears corrupted, stop reading
281
+ break
282
+ end
283
+
284
+ end_pts_of_contours << next_end_pt
274
285
  end
275
286
  end
276
287
 
277
- total_points = end_pts_of_contours.last + 1
288
+ # Handle case where stream was corrupted
289
+ if end_pts_of_contours.empty?
290
+ # Return minimal empty glyph
291
+ return ["\x00\x00"].pack("n") * 5 # Empty glyph: num_contours=0, bbox=0
292
+ end
293
+
294
+ total_points = [end_pts_of_contours.last + 1, max_points_per_glyph].min
278
295
 
279
296
  # Read flags
280
297
  flags = read_flags(flag_io, total_points)
@@ -517,6 +534,15 @@ instruction_io, variable_font: false)
517
534
  flags = []
518
535
 
519
536
  while flags.size < count
537
+ # Safety check to prevent infinite loops with corrupted streams
538
+ if flags.size > 200000
539
+ # Stream appears corrupted, pad with zeros
540
+ while flags.size < count
541
+ flags << 0
542
+ end
543
+ break
544
+ end
545
+
520
546
  # EOF protection for variable fonts
521
547
  break if io.eof? || (io.size - io.pos) < 1
522
548
 
@@ -527,15 +553,12 @@ instruction_io, variable_font: false)
527
553
  break if io.eof? || (io.size - io.pos) < 1
528
554
 
529
555
  repeat_count = read_uint8(io)
556
+ # Safety check on repeat count
557
+ repeat_count = [repeat_count, 100].min
530
558
  repeat_count.times { flags << flag }
531
559
  end
532
560
  end
533
561
 
534
- # Pad with zero flags if needed
535
- while flags.size < count
536
- flags << 0
537
- end
538
-
539
562
  flags
540
563
  end
541
564
 
@@ -33,7 +33,8 @@ module Fontisan
33
33
  # @param glyf_lsbs [Array<Integer>, nil] LSB values from glyf bboxes (optional)
34
34
  # @return [String] Standard hmtx table data
35
35
  # @raise [InvalidFontError] If data is corrupted or invalid
36
- def self.reconstruct(transformed_data, num_glyphs, num_h_metrics, glyf_lsbs = nil)
36
+ def self.reconstruct(transformed_data, num_glyphs, num_h_metrics,
37
+ glyf_lsbs = nil)
37
38
  io = StringIO.new(transformed_data)
38
39
 
39
40
  # Read transformation flags