fontisan 0.2.4 → 0.2.5

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +150 -30
  3. data/README.adoc +497 -242
  4. data/lib/fontisan/cli.rb +67 -6
  5. data/lib/fontisan/commands/validate_command.rb +107 -151
  6. data/lib/fontisan/converters/woff2_encoder.rb +7 -29
  7. data/lib/fontisan/models/validation_report.rb +227 -0
  8. data/lib/fontisan/pipeline/transformation_pipeline.rb +4 -8
  9. data/lib/fontisan/tables/cmap.rb +82 -2
  10. data/lib/fontisan/tables/glyf.rb +118 -0
  11. data/lib/fontisan/tables/head.rb +60 -0
  12. data/lib/fontisan/tables/hhea.rb +74 -0
  13. data/lib/fontisan/tables/maxp.rb +60 -0
  14. data/lib/fontisan/tables/name.rb +76 -0
  15. data/lib/fontisan/tables/os2.rb +113 -0
  16. data/lib/fontisan/tables/post.rb +57 -0
  17. data/lib/fontisan/validators/basic_validator.rb +85 -0
  18. data/lib/fontisan/validators/font_book_validator.rb +130 -0
  19. data/lib/fontisan/validators/opentype_validator.rb +112 -0
  20. data/lib/fontisan/validators/profile_loader.rb +139 -0
  21. data/lib/fontisan/validators/validator.rb +484 -0
  22. data/lib/fontisan/validators/web_font_validator.rb +102 -0
  23. data/lib/fontisan/version.rb +1 -1
  24. data/lib/fontisan.rb +78 -6
  25. metadata +7 -11
  26. data/lib/fontisan/config/validation_rules.yml +0 -149
  27. data/lib/fontisan/validation/checksum_validator.rb +0 -170
  28. data/lib/fontisan/validation/consistency_validator.rb +0 -197
  29. data/lib/fontisan/validation/structure_validator.rb +0 -198
  30. data/lib/fontisan/validation/table_validator.rb +0 -158
  31. data/lib/fontisan/validation/validator.rb +0 -152
  32. data/lib/fontisan/validation/variable_font_validator.rb +0 -218
  33. data/lib/fontisan/validation/woff2_header_validator.rb +0 -278
  34. data/lib/fontisan/validation/woff2_table_validator.rb +0 -270
  35. data/lib/fontisan/validation/woff2_validator.rb +0 -248
@@ -157,6 +157,66 @@ module Fontisan
157
157
  true
158
158
  end
159
159
 
160
+ # Validation helper: Check if version is valid (0.5 or 1.0)
161
+ #
162
+ # @return [Boolean] True if version is 0.5 or 1.0
163
+ def valid_version?
164
+ version_0_5? || version_1_0?
165
+ end
166
+
167
+ # Validation helper: Check if number of glyphs is valid
168
+ #
169
+ # Must be at least 1 (.notdef glyph must exist)
170
+ #
171
+ # @return [Boolean] True if num_glyphs >= 1
172
+ def valid_num_glyphs?
173
+ num_glyphs && num_glyphs >= 1
174
+ end
175
+
176
+ # Validation helper: Check if maxZones is valid (version 1.0 only)
177
+ #
178
+ # For TrueType fonts, maxZones must be 1 or 2
179
+ #
180
+ # @return [Boolean] True if maxZones is valid or not applicable
181
+ def valid_max_zones?
182
+ return true if version_0_5? # Not applicable for CFF
183
+
184
+ max_zones && max_zones.between?(1, 2)
185
+ end
186
+
187
+ # Validation helper: Check if all TrueType metrics are present
188
+ #
189
+ # For version 1.0, all max* fields should be present
190
+ #
191
+ # @return [Boolean] True if all required fields are present
192
+ def has_truetype_metrics?
193
+ version_1_0? &&
194
+ !max_points.nil? &&
195
+ !max_contours.nil? &&
196
+ !max_composite_points.nil? &&
197
+ !max_composite_contours.nil?
198
+ end
199
+
200
+ # Validation helper: Check if metrics are reasonable
201
+ #
202
+ # Checks that values don't exceed reasonable limits
203
+ #
204
+ # @return [Boolean] True if metrics are within reasonable bounds
205
+ def reasonable_metrics?
206
+ # num_glyphs should not exceed 65535
207
+ return false if num_glyphs > 65535
208
+
209
+ if version_1_0?
210
+ # Check reasonable limits for TrueType metrics
211
+ # These are generous limits to allow for complex fonts
212
+ return false if max_points && max_points > 50000
213
+ return false if max_contours && max_contours > 10000
214
+ return false if max_stack_elements && max_stack_elements > 1000
215
+ end
216
+
217
+ true
218
+ end
219
+
160
220
  # Validate the table and raise error if invalid
161
221
  #
162
222
  # @raise [Fontisan::CorruptedTableError] If table is invalid
@@ -199,6 +199,82 @@ module Fontisan
199
199
  false
200
200
  end
201
201
 
202
+ # Validation helper: Check if version is valid (0 or 1)
203
+ #
204
+ # @return [Boolean] True if version is 0 or 1
205
+ def valid_version?
206
+ format == 0 || format == 1
207
+ end
208
+
209
+ # Validation helper: Check if encoding combinations are valid
210
+ #
211
+ # According to OpenType spec, certain platform/encoding combinations are valid:
212
+ # - Platform 0 (Unicode): encoding 0-6
213
+ # - Platform 1 (Mac): encoding 0-32
214
+ # - Platform 3 (Windows): encoding 0-10
215
+ #
216
+ # @return [Boolean] True if all encoding heuristics are valid
217
+ def valid_encoding_heuristics?
218
+ name_records.all? do |rec|
219
+ case rec.platform_id
220
+ when PLATFORM_UNICODE
221
+ rec.encoding_id.between?(0, 6)
222
+ when PLATFORM_MACINTOSH
223
+ rec.encoding_id.between?(0, 32)
224
+ when PLATFORM_WINDOWS
225
+ rec.encoding_id.between?(0, 10)
226
+ else
227
+ # Unknown platform - consider invalid
228
+ false
229
+ end
230
+ end
231
+ end
232
+
233
+ # Validation helper: Check if required platform combinations exist
234
+ #
235
+ # @param combos [Array<Array<Integer>>] Array of [platform_id, encoding_id, language_id] arrays
236
+ # @return [Boolean] True if all required combinations are present
237
+ #
238
+ # @example Check for Windows English name
239
+ # name.has_valid_platform_combos?([3, 1, 0x0409])
240
+ def has_valid_platform_combos?(*combos)
241
+ combos.all? do |platform_id, encoding_id, language_id|
242
+ name_records.any? do |rec|
243
+ rec.platform_id == platform_id &&
244
+ rec.encoding_id == encoding_id &&
245
+ rec.language_id == language_id
246
+ end
247
+ end
248
+ end
249
+
250
+ # Validation helper: Check if family name is present and non-empty
251
+ #
252
+ # @return [Boolean] True if family name exists and is not empty
253
+ def family_name_present?
254
+ name = english_name(FAMILY)
255
+ !name.nil? && !name.empty?
256
+ end
257
+
258
+ # Validation helper: Check if PostScript name is present and non-empty
259
+ #
260
+ # @return [Boolean] True if PostScript name exists and is not empty
261
+ def postscript_name_present?
262
+ name = english_name(POSTSCRIPT_NAME)
263
+ !name.nil? && !name.empty?
264
+ end
265
+
266
+ # Validation helper: Check if PostScript name is valid
267
+ #
268
+ # PostScript names must contain only ASCII alphanumerics and hyphens
269
+ #
270
+ # @return [Boolean] True if PostScript name matches the required pattern
271
+ def postscript_name_valid?
272
+ name = english_name(POSTSCRIPT_NAME)
273
+ return false if name.nil? || name.empty?
274
+
275
+ name.match?(/^[A-Za-z0-9-]+$/)
276
+ end
277
+
202
278
  private
203
279
 
204
280
  # Find a name record matching the criteria
@@ -170,6 +170,119 @@ module Fontisan
170
170
 
171
171
  us_upper_optical_point_size / 20.0
172
172
  end
173
+
174
+ # Validation helper: Check if version is valid
175
+ #
176
+ # Valid versions are 0 through 5
177
+ #
178
+ # @return [Boolean] True if version is 0-5
179
+ def valid_version?
180
+ version && version.between?(0, 5)
181
+ end
182
+
183
+ # Validation helper: Check if weight class is valid
184
+ #
185
+ # Valid values are 1-1000, common values are multiples of 100
186
+ #
187
+ # @return [Boolean] True if weight class is valid
188
+ def valid_weight_class?
189
+ us_weight_class && us_weight_class.between?(1, 1000)
190
+ end
191
+
192
+ # Validation helper: Check if width class is valid
193
+ #
194
+ # Valid values are 1-9
195
+ #
196
+ # @return [Boolean] True if width class is 1-9
197
+ def valid_width_class?
198
+ us_width_class && us_width_class.between?(1, 9)
199
+ end
200
+
201
+ # Validation helper: Check if vendor ID is present
202
+ #
203
+ # Vendor ID should be a 4-character code
204
+ #
205
+ # @return [Boolean] True if vendor ID exists and is non-empty
206
+ def has_vendor_id?
207
+ !vendor_id.empty?
208
+ end
209
+
210
+ # Validation helper: Check if typo metrics are reasonable
211
+ #
212
+ # Ascent should be positive, descender negative, line gap non-negative
213
+ #
214
+ # @return [Boolean] True if typo metrics have correct signs
215
+ def valid_typo_metrics?
216
+ s_typo_ascender > 0 && s_typo_descender < 0 && s_typo_line_gap >= 0
217
+ end
218
+
219
+ # Validation helper: Check if Win metrics are valid
220
+ #
221
+ # Both should be positive (unsigned in spec)
222
+ #
223
+ # @return [Boolean] True if Win ascent and descent are positive
224
+ def valid_win_metrics?
225
+ us_win_ascent > 0 && us_win_descent > 0
226
+ end
227
+
228
+ # Validation helper: Check if Unicode ranges are set
229
+ #
230
+ # At least one Unicode range bit should be set
231
+ #
232
+ # @return [Boolean] True if any Unicode range bits are set
233
+ def has_unicode_ranges?
234
+ (ul_unicode_range1 | ul_unicode_range2 | ul_unicode_range3 | ul_unicode_range4) != 0
235
+ end
236
+
237
+ # Validation helper: Check if PANOSE data is present
238
+ #
239
+ # All PANOSE values should not be zero
240
+ #
241
+ # @return [Boolean] True if PANOSE seems to be set
242
+ def has_panose?
243
+ panose && panose.any? { |val| val != 0 }
244
+ end
245
+
246
+ # Validation helper: Check if embedding permissions are set
247
+ #
248
+ # fs_type indicates embedding and subsetting permissions
249
+ #
250
+ # @return [Boolean] True if embedding permissions are defined
251
+ def has_embedding_permissions?
252
+ !fs_type.nil?
253
+ end
254
+
255
+ # Validation helper: Check if selection flags are valid
256
+ #
257
+ # Checks for valid combinations of selection flags
258
+ #
259
+ # @return [Boolean] True if fs_selection has valid flags
260
+ def valid_selection_flags?
261
+ return false if fs_selection.nil?
262
+
263
+ # Bits 0-9 are defined, others should be zero
264
+ (fs_selection & 0xFC00).zero?
265
+ end
266
+
267
+ # Validation helper: Check if x_height and cap_height are present (v2+)
268
+ #
269
+ # For version 2+, these should be set
270
+ #
271
+ # @return [Boolean] True if metrics are present (or not required)
272
+ def has_x_height_cap_height?
273
+ return true if version < 2 # Not required for v0-1
274
+
275
+ !sx_height.nil? && !s_cap_height.nil? && sx_height > 0 && s_cap_height > 0
276
+ end
277
+
278
+ # Validation helper: Check if first/last char indices are reasonable
279
+ #
280
+ # first should be <= last
281
+ #
282
+ # @return [Boolean] True if character range is valid
283
+ def valid_char_range?
284
+ us_first_char_index <= us_last_char_index
285
+ end
173
286
  end
174
287
  end
175
288
  end
@@ -143,6 +143,63 @@ module Fontisan
143
143
  end
144
144
  end
145
145
  # rubocop:enable Metrics/PerceivedComplexity
146
+
147
+ public
148
+
149
+ # Validation helper: Check if version is valid
150
+ #
151
+ # Common versions: 1.0, 2.0, 2.5, 3.0, 4.0
152
+ #
153
+ # @return [Boolean] True if version is recognized
154
+ def valid_version?
155
+ [1.0, 2.0, 2.5, 3.0, 4.0].include?(version)
156
+ end
157
+
158
+ # Validation helper: Check if italic angle is reasonable
159
+ #
160
+ # Italic angle should be between -60 and 60 degrees
161
+ #
162
+ # @return [Boolean] True if italic angle is within reasonable bounds
163
+ def valid_italic_angle?
164
+ italic_angle.abs <= 60.0
165
+ end
166
+
167
+ # Validation helper: Check if underline values are present
168
+ #
169
+ # Both position and thickness should be non-zero for valid underline
170
+ #
171
+ # @return [Boolean] True if underline metrics exist
172
+ def has_underline_metrics?
173
+ underline_position != 0 && underline_thickness != 0
174
+ end
175
+
176
+ # Validation helper: Check if fixed pitch flag is consistent
177
+ #
178
+ # @return [Boolean] True if is_fixed_pitch is 0 or 1
179
+ def valid_fixed_pitch_flag?
180
+ is_fixed_pitch == 0 || is_fixed_pitch == 1
181
+ end
182
+
183
+ # Validation helper: Check if glyph names are available
184
+ #
185
+ # For versions 1.0 and 2.0, glyph names should be accessible
186
+ #
187
+ # @return [Boolean] True if glyph names can be retrieved
188
+ def has_glyph_names?
189
+ names = glyph_names
190
+ !names.nil? && !names.empty?
191
+ end
192
+
193
+ # Validation helper: Check if version 2.0 data is complete
194
+ #
195
+ # For version 2.0, we should have glyph count and name data
196
+ #
197
+ # @return [Boolean] True if version 2.0 data is present and complete
198
+ def complete_version_2_data?
199
+ return true unless version == 2.0
200
+
201
+ !num_glyphs_v2.nil? && num_glyphs_v2 > 0 && !remaining_data.empty?
202
+ end
146
203
  end
147
204
  end
148
205
  end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "validator"
4
+
5
+ module Fontisan
6
+ module Validators
7
+ # BasicValidator provides minimal validation for fast font indexing
8
+ #
9
+ # This validator implements only essential checks needed for font discovery
10
+ # and indexing systems (e.g., Fontist). It is optimized for speed with a
11
+ # target performance of < 50ms per font.
12
+ #
13
+ # The validator checks only critical font identification and structural
14
+ # integrity, making it suitable for:
15
+ # - Font discovery and indexing
16
+ # - Quick font database updates
17
+ # - Large-scale font scanning
18
+ #
19
+ # @example Using BasicValidator
20
+ # validator = BasicValidator.new
21
+ # report = validator.validate(font)
22
+ # puts "Font is valid for indexing" if report.valid?
23
+ #
24
+ # @example Target performance
25
+ # # Should complete in < 50ms
26
+ # start_time = Time.now
27
+ # report = BasicValidator.new.validate(font)
28
+ # elapsed = Time.now - start_time
29
+ # puts "Validated in #{elapsed * 1000}ms"
30
+ class BasicValidator < Validator
31
+ private
32
+
33
+ # Define essential validation checks
34
+ #
35
+ # This validator implements 8 checks covering:
36
+ # - Required tables presence
37
+ # - Name table identification
38
+ # - Head table integrity
39
+ # - Maxp table glyph count
40
+ #
41
+ # All checks use helpers from Week 1 table implementations.
42
+ def define_checks
43
+ # Check 1: Required tables must be present
44
+ check_structure :required_tables, severity: :error do |font|
45
+ %w[name head maxp hhea].all? { |tag| font.table(tag) }
46
+ end
47
+
48
+ # Check 2: Name table version must be valid (0 or 1)
49
+ check_table :name_version, "name", severity: :error do |table|
50
+ table.valid_version?
51
+ end
52
+
53
+ # Check 3: Family name must be present and non-empty
54
+ check_table :family_name, "name", severity: :error do |table|
55
+ table.family_name_present?
56
+ end
57
+
58
+ # Check 4: PostScript name must be valid (alphanumeric + hyphens)
59
+ check_table :postscript_name, "name", severity: :error do |table|
60
+ table.postscript_name_valid?
61
+ end
62
+
63
+ # Check 5: Head table magic number must be correct
64
+ check_table :head_magic, "head", severity: :error do |table|
65
+ table.valid_magic?
66
+ end
67
+
68
+ # Check 6: Units per em must be valid (16-16384)
69
+ check_table :units_per_em, "head", severity: :error do |table|
70
+ table.valid_units_per_em?
71
+ end
72
+
73
+ # Check 7: Number of glyphs must be at least 1 (.notdef)
74
+ check_table :num_glyphs, "maxp", severity: :error do |table|
75
+ table.valid_num_glyphs?
76
+ end
77
+
78
+ # Check 8: Maxp metrics should be reasonable (not absurd values)
79
+ check_table :reasonable_metrics, "maxp", severity: :warning do |table|
80
+ table.reasonable_metrics?
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "basic_validator"
4
+
5
+ module Fontisan
6
+ module Validators
7
+ # FontBookValidator provides macOS Font Book installation compatibility checks
8
+ #
9
+ # This validator extends BasicValidator with additional checks needed for
10
+ # fonts to be successfully installed and used in macOS Font Book. It ensures
11
+ # proper encoding combinations, OS/2 metrics, and other macOS-specific
12
+ # requirements.
13
+ #
14
+ # The validator inherits all 8 checks from BasicValidator and adds 12 new
15
+ # checks focusing on:
16
+ # - Name table encoding combinations (Windows and Mac)
17
+ # - OS/2 table metrics and metadata
18
+ # - Head table bounding box
19
+ # - Hhea table metrics
20
+ # - Post table metadata
21
+ # - Cmap subtables
22
+ #
23
+ # @example Using FontBookValidator
24
+ # validator = FontBookValidator.new
25
+ # report = validator.validate(font)
26
+ # puts "Font is Font Book compatible" if report.valid?
27
+ class FontBookValidator < BasicValidator
28
+ private
29
+
30
+ # Define Font Book compatibility checks
31
+ #
32
+ # Calls super to inherit BasicValidator's 8 checks, then adds 12 new checks.
33
+ # All checks use helpers from Week 1 table implementations.
34
+ def define_checks
35
+ # Inherit BasicValidator checks (8 checks)
36
+ super
37
+
38
+ # Check 9: Name table Windows Unicode English encoding
39
+ check_table :name_windows_encoding, 'name', severity: :error do |table|
40
+ table.has_valid_platform_combos?([3, 1, 0x0409]) # Windows Unicode English
41
+ end
42
+
43
+ # Check 10: Name table Mac Roman English encoding
44
+ check_table :name_mac_encoding, 'name', severity: :warning do |table|
45
+ table.has_valid_platform_combos?([1, 0, 0]) # Mac Roman English
46
+ end
47
+
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
52
+
53
+ # 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
57
+
58
+ # 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
62
+
63
+ # 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
67
+
68
+ # 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
72
+
73
+ # 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
77
+
78
+ # 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
82
+
83
+ # 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
87
+
88
+ # 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
92
+
93
+ # 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
97
+
98
+ # 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
102
+
103
+ # 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
107
+
108
+ # Check 23: Post table version must be valid
109
+ check_table :post_version, 'post', severity: :error do |table|
110
+ table.valid_version?
111
+ end
112
+
113
+ # 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
117
+
118
+ # 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
122
+
123
+ # Check 26: Cmap table must have subtables
124
+ check_table :cmap_subtables, 'cmap', severity: :error do |table|
125
+ table.has_subtables?
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "font_book_validator"
4
+
5
+ module Fontisan
6
+ module Validators
7
+ # OpenTypeValidator provides comprehensive OpenType specification compliance checks
8
+ #
9
+ # This validator extends FontBookValidator with additional checks ensuring full
10
+ # OpenType specification compliance. It validates glyph data, character mappings,
11
+ # and cross-table consistency.
12
+ #
13
+ # The validator inherits all checks from FontBookValidator (18 checks from
14
+ # FontBookValidator + 8 from BasicValidator = 26 total) and adds 10 new checks:
15
+ # - Maxp TrueType metrics validation
16
+ # - Glyf table structure and accessibility
17
+ # - Cmap Unicode mapping and coverage
18
+ # - Cross-table consistency checks
19
+ #
20
+ # @example Using OpenTypeValidator
21
+ # validator = OpenTypeValidator.new
22
+ # report = validator.validate(font)
23
+ # puts "Font is OpenType compliant" if report.valid?
24
+ class OpenTypeValidator < FontBookValidator
25
+ private
26
+
27
+ # Define OpenType specification compliance checks
28
+ #
29
+ # Calls super to inherit FontBookValidator's checks, then adds 10 new checks.
30
+ # All checks use helpers from Week 1 table implementations.
31
+ def define_checks
32
+ # Inherit FontBookValidator checks (26 checks total)
33
+ super
34
+
35
+ # Check 27: Maxp TrueType metrics (only for version 1.0)
36
+ check_table :maxp_truetype_metrics, 'maxp', severity: :warning do |table|
37
+ !table.version_1_0? || table.has_truetype_metrics?
38
+ end
39
+
40
+ # 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
44
+
45
+ # Check 29: Glyf glyphs must be accessible (TrueType fonts only)
46
+ check_glyphs :glyf_accessible, severity: :error do |font|
47
+ glyf = font.table('glyf')
48
+ next true unless glyf # Skip if CFF font
49
+
50
+ loca = font.table('loca')
51
+ head = font.table('head')
52
+ maxp = font.table('maxp')
53
+ glyf.all_glyphs_accessible?(loca, head, maxp.num_glyphs)
54
+ end
55
+
56
+ # Check 30: Glyf glyphs should not be clipped
57
+ check_glyphs :glyf_no_clipping, severity: :warning do |font|
58
+ glyf = font.table('glyf')
59
+ next true unless glyf
60
+
61
+ loca = font.table('loca')
62
+ head = font.table('head')
63
+ maxp = font.table('maxp')
64
+ glyf.no_clipped_glyphs?(loca, head, maxp.num_glyphs)
65
+ end
66
+
67
+ # Check 31: Glyf contour counts must be valid
68
+ check_glyphs :glyf_valid_contours, severity: :error do |font|
69
+ glyf = font.table('glyf')
70
+ next true unless glyf
71
+
72
+ loca = font.table('loca')
73
+ head = font.table('head')
74
+ maxp = font.table('maxp')
75
+
76
+ (0...maxp.num_glyphs).all? do |glyph_id|
77
+ glyf.valid_contour_count?(glyph_id, loca, head)
78
+ end
79
+ end
80
+
81
+ # 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
85
+
86
+ # 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
90
+
91
+ # 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
95
+
96
+ # Check 35: Cmap glyph indices must be valid
97
+ check_structure :cmap_glyph_indices, severity: :error do |font|
98
+ cmap = font.table('cmap')
99
+ maxp = font.table('maxp')
100
+ cmap.valid_glyph_indices?(maxp.num_glyphs)
101
+ end
102
+
103
+ # Check 36: Table checksums (info level - many fonts have mismatches)
104
+ check_structure :checksum_valid, severity: :info do |font|
105
+ # Table checksum validation (info level - for reference)
106
+ # Most fonts have checksum mismatches, so we make it info not error
107
+ true # Placeholder - actual checksum validation if desired
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end