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
@@ -46,6 +46,34 @@ module Fontisan
46
46
  end
47
47
  end
48
48
 
49
+ # Individual check result from DSL-based validation
50
+ class CheckResult < Lutaml::Model::Serializable
51
+ attribute :check_id, :string
52
+ attribute :passed, :boolean
53
+ attribute :severity, :string
54
+ attribute :messages, :string, collection: true, default: -> { [] }
55
+ attribute :table, :string
56
+ attribute :field, :string
57
+
58
+ yaml do
59
+ map "check_id", to: :check_id
60
+ map "passed", to: :passed
61
+ map "severity", to: :severity
62
+ map "messages", to: :messages
63
+ map "table", to: :table
64
+ map "field", to: :field
65
+ end
66
+
67
+ json do
68
+ map "check_id", to: :check_id
69
+ map "passed", to: :passed
70
+ map "severity", to: :severity
71
+ map "messages", to: :messages
72
+ map "table", to: :table
73
+ map "field", to: :field
74
+ end
75
+ end
76
+
49
77
  # Validation summary counts
50
78
  class Summary < Lutaml::Model::Serializable
51
79
  attribute :errors, :integer, default: -> { 0 }
@@ -69,12 +97,22 @@ module Fontisan
69
97
  attribute :valid, :boolean
70
98
  attribute :issues, Issue, collection: true, default: -> { [] }
71
99
  attribute :summary, Summary, default: -> { Summary.new }
100
+ attribute :profile, :string
101
+ attribute :status, :string
102
+ attribute :use_case, :string
103
+ attribute :checks_performed, :string, collection: true, default: -> { [] }
104
+ attribute :check_results, CheckResult, collection: true, default: -> { [] }
72
105
 
73
106
  yaml do
74
107
  map "font_path", to: :font_path
75
108
  map "valid", to: :valid
76
109
  map "summary", to: :summary
77
110
  map "issues", to: :issues
111
+ map "profile", to: :profile
112
+ map "status", to: :status
113
+ map "use_case", to: :use_case
114
+ map "checks_performed", to: :checks_performed
115
+ map "check_results", to: :check_results
78
116
  end
79
117
 
80
118
  json do
@@ -82,6 +120,11 @@ module Fontisan
82
120
  map "valid", to: :valid
83
121
  map "summary", to: :summary
84
122
  map "issues", to: :issues
123
+ map "profile", to: :profile
124
+ map "status", to: :status
125
+ map "use_case", to: :use_case
126
+ map "checks_performed", to: :checks_performed
127
+ map "check_results", to: :check_results
85
128
  end
86
129
 
87
130
  # Add an error to the report
@@ -198,6 +241,190 @@ module Fontisan
198
241
 
199
242
  lines.join("\n")
200
243
  end
244
+
245
+ # Check if font passed validation (alias for valid)
246
+ #
247
+ # @return [Boolean] true if font passed validation
248
+ def passed?
249
+ valid
250
+ end
251
+
252
+ # Check if font is valid (alias for valid attribute)
253
+ #
254
+ # @return [Boolean] true if font is valid
255
+ def valid?
256
+ valid
257
+ end
258
+
259
+ # Get result for a specific check by ID
260
+ #
261
+ # @param check_id [Symbol, String] The check identifier
262
+ # @return [CheckResult, nil] The check result or nil if not found
263
+ def result_of(check_id)
264
+ check_results.find { |cr| cr.check_id == check_id.to_s }
265
+ end
266
+
267
+ # Get all passed checks
268
+ #
269
+ # @return [Array<CheckResult>] Array of passed checks
270
+ def passed_checks
271
+ check_results.select(&:passed)
272
+ end
273
+
274
+ # Get all failed checks
275
+ #
276
+ # @return [Array<CheckResult>] Array of failed checks
277
+ def failed_checks
278
+ check_results.reject(&:passed)
279
+ end
280
+
281
+ # Severity filtering methods
282
+
283
+ # Get issues by severity level
284
+ #
285
+ # @param severity [Symbol, String] Severity level
286
+ # @return [Array<Issue>] Array of issues with the specified severity
287
+ def issues_by_severity(severity)
288
+ issues.select { |issue| issue.severity == severity.to_s }
289
+ end
290
+
291
+ # Get fatal error issues
292
+ #
293
+ # @return [Array<Issue>] Array of fatal error issues
294
+ def fatal_errors
295
+ issues_by_severity(:fatal)
296
+ end
297
+
298
+ # Get error issues only
299
+ #
300
+ # @return [Array<Issue>] Array of error issues
301
+ def errors_only
302
+ issues_by_severity(:error)
303
+ end
304
+
305
+ # Get warning issues only
306
+ #
307
+ # @return [Array<Issue>] Array of warning issues
308
+ def warnings_only
309
+ issues_by_severity(:warning)
310
+ end
311
+
312
+ # Get info issues only
313
+ #
314
+ # @return [Array<Issue>] Array of info issues
315
+ def info_only
316
+ issues_by_severity(:info)
317
+ end
318
+
319
+ # Category filtering methods
320
+
321
+ # Get issues by category
322
+ #
323
+ # @param category [String] Category name
324
+ # @return [Array<Issue>] Array of issues in the specified category
325
+ def issues_by_category(category)
326
+ issues.select { |issue| issue.category == category.to_s }
327
+ end
328
+
329
+ # Get check results for a specific table
330
+ #
331
+ # @param table_tag [String] Table tag (e.g., 'name', 'head')
332
+ # @return [Array<CheckResult>] Array of check results for the table
333
+ def table_issues(table_tag)
334
+ check_results.select { |cr| cr.table == table_tag.to_s }
335
+ end
336
+
337
+ # Get check results for a specific field in a table
338
+ #
339
+ # @param table_tag [String] Table tag
340
+ # @param field_name [String, Symbol] Field name
341
+ # @return [Array<CheckResult>] Array of check results for the field
342
+ def field_issues(table_tag, field_name)
343
+ check_results.select { |cr| cr.table == table_tag.to_s && cr.field == field_name.to_s }
344
+ end
345
+
346
+ # Check filtering methods
347
+
348
+ # Get checks by status
349
+ #
350
+ # @param passed [Boolean] true for passed checks, false for failed checks
351
+ # @return [Array<CheckResult>] Array of checks with the specified status
352
+ def checks_by_status(passed:)
353
+ check_results.select { |cr| cr.passed == passed }
354
+ end
355
+
356
+ # Get IDs of failed checks
357
+ #
358
+ # @return [Array<String>] Array of failed check IDs
359
+ def failed_check_ids
360
+ failed_checks.map(&:check_id)
361
+ end
362
+
363
+ # Get IDs of passed checks
364
+ #
365
+ # @return [Array<String>] Array of passed check IDs
366
+ def passed_check_ids
367
+ passed_checks.map(&:check_id)
368
+ end
369
+
370
+ # Statistics methods
371
+
372
+ # Calculate failure rate as percentage
373
+ #
374
+ # @return [Float] Failure rate (0.0 to 1.0)
375
+ def failure_rate
376
+ return 0.0 if check_results.empty?
377
+ failed_checks.length.to_f / check_results.length
378
+ end
379
+
380
+ # Calculate pass rate as percentage
381
+ #
382
+ # @return [Float] Pass rate (0.0 to 1.0)
383
+ def pass_rate
384
+ 1.0 - failure_rate
385
+ end
386
+
387
+ # Get severity distribution
388
+ #
389
+ # @return [Hash] Hash with :errors, :warnings, :info counts
390
+ def severity_distribution
391
+ {
392
+ errors: summary.errors,
393
+ warnings: summary.warnings,
394
+ info: summary.info,
395
+ }
396
+ end
397
+
398
+ # Export format methods
399
+
400
+ # Generate full detailed text report
401
+ #
402
+ # @return [String] Detailed text report
403
+ def to_text_report
404
+ text_summary
405
+ end
406
+
407
+ # Generate brief summary
408
+ #
409
+ # @return [String] Brief summary string
410
+ def to_summary
411
+ "#{summary.errors} errors, #{summary.warnings} warnings, #{summary.info} info"
412
+ end
413
+
414
+ # Generate tabular format for CLI
415
+ #
416
+ # @return [String] Tabular format output
417
+ def to_table_format
418
+ lines = []
419
+ lines << "CHECK_ID | STATUS | SEVERITY | TABLE"
420
+ lines << "-" * 60
421
+ check_results.each do |cr|
422
+ status = cr.passed ? "PASS" : "FAIL"
423
+ table = cr.table || "N/A"
424
+ lines << "#{cr.check_id} | #{status} | #{cr.severity} | #{table}"
425
+ end
426
+ lines.join("\n")
427
+ end
201
428
  end
202
429
  end
203
430
  end
@@ -263,16 +263,12 @@ module Fontisan
263
263
  def validate_output
264
264
  return unless File.exist?(@output_path)
265
265
 
266
- require_relative "../validation/validator"
266
+ # Use new validation framework with production profile
267
+ report = Fontisan.validate(@output_path, profile: :production)
267
268
 
268
- # Load font for validation
269
- font = FontLoader.load(@output_path, mode: :full)
270
- validator = Validation::Validator.new
271
- result = validator.validate(font, @output_path)
269
+ return if report.valid?
272
270
 
273
- return if result.valid
274
-
275
- error_messages = result.errors.map(&:message).join(", ")
271
+ error_messages = report.errors.map(&:message).join(", ")
276
272
  raise Error, "Output validation failed: #{error_messages}"
277
273
  end
278
274
 
@@ -36,8 +36,6 @@ module Fontisan
36
36
  @unicode_mappings ||= parse_mappings
37
37
  end
38
38
 
39
- private
40
-
41
39
  # Parse all encoding records and extract Unicode mappings
42
40
  def parse_mappings
43
41
  mappings = {}
@@ -279,6 +277,88 @@ module Fontisan
279
277
  mappings[code] = glyph_index if glyph_index != 0
280
278
  end
281
279
  end
280
+
281
+ public
282
+
283
+ # Validation helper: Check if version is valid
284
+ #
285
+ # cmap version should be 0
286
+ #
287
+ # @return [Boolean] True if version is 0
288
+ def valid_version?
289
+ version == 0
290
+ end
291
+
292
+ # Validation helper: Check if at least one subtable exists
293
+ #
294
+ # @return [Boolean] True if num_tables > 0
295
+ def has_subtables?
296
+ num_tables && num_tables > 0
297
+ end
298
+
299
+ # Validation helper: Check if Unicode mapping exists
300
+ #
301
+ # @return [Boolean] True if Unicode mappings are present
302
+ def has_unicode_mapping?
303
+ !unicode_mappings.nil? && !unicode_mappings.empty?
304
+ end
305
+
306
+ # Validation helper: Check if BMP coverage exists
307
+ #
308
+ # Checks if the Basic Multilingual Plane (U+0000-U+FFFF) has mappings
309
+ #
310
+ # @return [Boolean] True if BMP characters are mapped
311
+ def has_bmp_coverage?
312
+ mappings = unicode_mappings
313
+ return false if mappings.nil? || mappings.empty?
314
+
315
+ # Check if any BMP characters (0x0000-0xFFFF) are mapped
316
+ mappings.keys.any? { |code| code.between?(0x0000, 0xFFFF) }
317
+ end
318
+
319
+ # Validation helper: Check if required characters are mapped
320
+ #
321
+ # Checks for essential characters like space (U+0020)
322
+ #
323
+ # @param required_chars [Array<Integer>] Character codes that must be present
324
+ # @return [Boolean] True if all required characters are mapped
325
+ def has_required_characters?(*required_chars)
326
+ mappings = unicode_mappings
327
+ return false if mappings.nil?
328
+
329
+ required_chars.all? { |code| mappings.key?(code) }
330
+ end
331
+
332
+ # Validation helper: Check if format 4 subtable exists
333
+ #
334
+ # Format 4 is the minimum requirement for Unicode BMP support
335
+ #
336
+ # @return [Boolean] True if format 4 subtable is found
337
+ def has_format_4_subtable?
338
+ data = to_binary_s
339
+ records = read_encoding_records(data)
340
+
341
+ records.any? do |record|
342
+ subtable_data = extract_subtable_data(record, data)
343
+ next false unless subtable_data && subtable_data.length >= 2
344
+
345
+ format = subtable_data[0, 2].unpack1("n")
346
+ format == 4
347
+ end
348
+ rescue StandardError
349
+ false
350
+ end
351
+
352
+ # Validation helper: Check if glyph indices are within bounds
353
+ #
354
+ # @param max_glyph_id [Integer] Maximum valid glyph ID from maxp table
355
+ # @return [Boolean] True if all mapped glyph IDs are valid
356
+ def valid_glyph_indices?(max_glyph_id)
357
+ mappings = unicode_mappings
358
+ return true if mappings.nil? || mappings.empty?
359
+
360
+ mappings.values.all? { |glyph_id| glyph_id >= 0 && glyph_id < max_glyph_id }
361
+ end
282
362
  end
283
363
  end
284
364
  end
@@ -158,6 +158,124 @@ module Fontisan
158
158
  @glyphs_cache ||= {}
159
159
  end
160
160
 
161
+ # Validation helper: Check if all non-special glyphs have contours
162
+ #
163
+ # The .notdef glyph (ID 0) can be empty, but other glyphs should have geometry
164
+ #
165
+ # @param loca [Loca] Loca table for glyph access
166
+ # @param head [Head] Head table for context
167
+ # @param num_glyphs [Integer] Total number of glyphs to check
168
+ # @return [Boolean] True if all non-special glyphs have contours
169
+ def no_empty_glyphs_except_special?(loca, head, num_glyphs)
170
+ # Check glyphs 1 through num_glyphs-1 (.notdef at 0 can be empty)
171
+ (1...num_glyphs).all? do |glyph_id|
172
+ size = loca.size_of(glyph_id)
173
+ # Empty glyphs (like space) are allowed, but check if they should be empty
174
+ # This is a basic check - we just ensure non-control glyphs have data
175
+ size.nil? || size.positive?
176
+ end
177
+ rescue StandardError
178
+ false
179
+ end
180
+
181
+ # Validation helper: Check if any glyphs are clipped (exceed bounds)
182
+ #
183
+ # Validates that glyph coordinates don't exceed head table's bounding box
184
+ #
185
+ # @param loca [Loca] Loca table for glyph access
186
+ # @param head [Head] Head table for bounds reference
187
+ # @param num_glyphs [Integer] Total number of glyphs to check
188
+ # @return [Boolean] True if no glyphs exceed the font's bounding box
189
+ def no_clipped_glyphs?(loca, head, num_glyphs)
190
+ font_x_min = head.x_min
191
+ font_y_min = head.y_min
192
+ font_x_max = head.x_max
193
+ font_y_max = head.y_max
194
+
195
+ (0...num_glyphs).all? do |glyph_id|
196
+ glyph = glyph_for(glyph_id, loca, head)
197
+ next true if glyph.nil? # Empty glyphs are OK
198
+
199
+ # Check if glyph bounds are within font bounds
200
+ glyph.x_min >= font_x_min &&
201
+ glyph.y_min >= font_y_min &&
202
+ glyph.x_max <= font_x_max &&
203
+ glyph.y_max <= font_y_max
204
+ end
205
+ rescue StandardError
206
+ false
207
+ end
208
+
209
+ # Validation helper: Check if TrueType instructions are sound
210
+ #
211
+ # Validates that glyph instructions (if present) are parseable
212
+ # This is a basic check that ensures instructions exist and have valid length
213
+ #
214
+ # @param loca [Loca] Loca table for glyph access
215
+ # @param head [Head] Head table for context
216
+ # @param num_glyphs [Integer] Total number of glyphs to check
217
+ # @return [Boolean] True if all instructions are valid or absent
218
+ def instructions_sound?(loca, head, num_glyphs)
219
+ (0...num_glyphs).all? do |glyph_id|
220
+ glyph = glyph_for(glyph_id, loca, head)
221
+ next true if glyph.nil? # Empty glyphs are OK
222
+
223
+ # Simple glyphs have instructions
224
+ if glyph.respond_to?(:instruction_length)
225
+ inst_len = glyph.instruction_length
226
+ # If instructions present, length should be reasonable
227
+ inst_len.nil? || inst_len >= 0
228
+ else
229
+ # Compound glyphs may have instructions too
230
+ true
231
+ end
232
+ end
233
+ rescue StandardError
234
+ false
235
+ end
236
+
237
+ # Validation helper: Check if glyph has valid number of contours
238
+ #
239
+ # @param glyph_id [Integer] Glyph ID to check
240
+ # @param loca [Loca] Loca table for glyph access
241
+ # @param head [Head] Head table for context
242
+ # @return [Boolean] True if contour count is valid
243
+ def valid_contour_count?(glyph_id, loca, head)
244
+ glyph = glyph_for(glyph_id, loca, head)
245
+ return true if glyph.nil? # Empty glyphs are OK
246
+
247
+ # Simple glyphs: contours should be >= 0
248
+ # Compound glyphs: numberOfContours = -1
249
+ if glyph.respond_to?(:num_contours)
250
+ glyph.num_contours >= -1
251
+ else
252
+ true
253
+ end
254
+ rescue StandardError
255
+ false
256
+ end
257
+
258
+ # Validation helper: Check if all glyphs are accessible
259
+ #
260
+ # Attempts to access each glyph to ensure no corruption
261
+ #
262
+ # @param loca [Loca] Loca table for glyph access
263
+ # @param head [Head] Head table for context
264
+ # @param num_glyphs [Integer] Total number of glyphs
265
+ # @return [Boolean] True if all glyphs can be accessed
266
+ def all_glyphs_accessible?(loca, head, num_glyphs)
267
+ (0...num_glyphs).all? do |glyph_id|
268
+ begin
269
+ glyph_for(glyph_id, loca, head)
270
+ true
271
+ rescue Fontisan::CorruptedTableError
272
+ false
273
+ end
274
+ end
275
+ rescue StandardError
276
+ false
277
+ end
278
+
161
279
  private
162
280
 
163
281
  # Validate context and glyph ID
@@ -82,6 +82,66 @@ module Fontisan
82
82
  magic_number == MAGIC_NUMBER
83
83
  end
84
84
 
85
+ # Validation helper: Check if magic number is valid
86
+ #
87
+ # @return [Boolean] True if magic number equals 0x5F0F3CF5
88
+ def valid_magic?
89
+ magic_number == MAGIC_NUMBER
90
+ end
91
+
92
+ # Validation helper: Check if version is valid
93
+ #
94
+ # OpenType spec requires version to be 1.0
95
+ #
96
+ # @return [Boolean] True if version is 1.0
97
+ def valid_version?
98
+ version_raw == 0x00010000 # Version 1.0
99
+ end
100
+
101
+ # Validation helper: Check if units per em is valid
102
+ #
103
+ # Units per em should be a power of 2 between 16 and 16384
104
+ #
105
+ # @return [Boolean] True if units_per_em is valid
106
+ def valid_units_per_em?
107
+ return false if units_per_em.nil? || units_per_em.zero?
108
+
109
+ # Must be between 16 and 16384
110
+ return false unless units_per_em.between?(16, 16384)
111
+
112
+ # Should be a power of 2 (recommended but not required)
113
+ # Common values: 1000, 1024, 2048
114
+ # We'll allow any value in range for flexibility
115
+ true
116
+ end
117
+
118
+ # Validation helper: Check if bounding box is valid
119
+ #
120
+ # The bounding box should have xMin < xMax and yMin < yMax
121
+ #
122
+ # @return [Boolean] True if bounding box coordinates are valid
123
+ def valid_bounding_box?
124
+ x_min < x_max && y_min < y_max
125
+ end
126
+
127
+ # Validation helper: Check if index_to_loc_format is valid
128
+ #
129
+ # Must be 0 (short) or 1 (long)
130
+ #
131
+ # @return [Boolean] True if format is 0 or 1
132
+ def valid_index_to_loc_format?
133
+ index_to_loc_format == 0 || index_to_loc_format == 1
134
+ end
135
+
136
+ # Validation helper: Check if glyph_data_format is valid
137
+ #
138
+ # Must be 0 for current format
139
+ #
140
+ # @return [Boolean] True if format is 0
141
+ def valid_glyph_data_format?
142
+ glyph_data_format == 0
143
+ end
144
+
85
145
  # Validate magic number and raise error if invalid
86
146
  #
87
147
  # @raise [Fontisan::CorruptedTableError] If magic number is invalid
@@ -97,6 +97,80 @@ module Fontisan
97
97
  true
98
98
  end
99
99
 
100
+ # Validation helper: Check if version is valid
101
+ #
102
+ # OpenType spec requires version to be 1.0
103
+ #
104
+ # @return [Boolean] True if version is 1.0
105
+ def valid_version?
106
+ version_raw == 0x00010000
107
+ end
108
+
109
+ # Validation helper: Check if metric data format is valid
110
+ #
111
+ # Must be 0 for current format
112
+ #
113
+ # @return [Boolean] True if format is 0
114
+ def valid_metric_data_format?
115
+ metric_data_format == 0
116
+ end
117
+
118
+ # Validation helper: Check if number of h metrics is valid
119
+ #
120
+ # Must be at least 1
121
+ #
122
+ # @return [Boolean] True if number_of_h_metrics >= 1
123
+ def valid_number_of_h_metrics?
124
+ number_of_h_metrics && number_of_h_metrics >= 1
125
+ end
126
+
127
+ # Validation helper: Check if ascent/descent values are reasonable
128
+ #
129
+ # Ascent should be positive, descent should be negative
130
+ #
131
+ # @return [Boolean] True if ascent/descent have correct signs
132
+ def valid_ascent_descent?
133
+ ascent > 0 && descent < 0
134
+ end
135
+
136
+ # Validation helper: Check if line gap is non-negative
137
+ #
138
+ # Line gap should be >= 0
139
+ #
140
+ # @return [Boolean] True if line_gap >= 0
141
+ def valid_line_gap?
142
+ line_gap >= 0
143
+ end
144
+
145
+ # Validation helper: Check if advance width max is positive
146
+ #
147
+ # Maximum advance width should be > 0
148
+ #
149
+ # @return [Boolean] True if advance_width_max > 0
150
+ def valid_advance_width_max?
151
+ advance_width_max && advance_width_max > 0
152
+ end
153
+
154
+ # Validation helper: Check if caret slope is valid
155
+ #
156
+ # For vertical text: rise=1, run=0
157
+ # For horizontal italic: both should be non-zero
158
+ #
159
+ # @return [Boolean] True if caret slope values are sensible
160
+ def valid_caret_slope?
161
+ # At least one should be non-zero
162
+ caret_slope_rise != 0 || caret_slope_run != 0
163
+ end
164
+
165
+ # Validation helper: Check if extent is reasonable
166
+ #
167
+ # x_max_extent should be positive
168
+ #
169
+ # @return [Boolean] True if x_max_extent > 0
170
+ def valid_x_max_extent?
171
+ x_max_extent > 0
172
+ end
173
+
100
174
  # Validate the table and raise error if invalid
101
175
  #
102
176
  # @raise [Fontisan::CorruptedTableError] If table is invalid