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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +150 -30
- data/README.adoc +497 -242
- data/lib/fontisan/cli.rb +67 -6
- data/lib/fontisan/commands/validate_command.rb +107 -151
- data/lib/fontisan/converters/woff2_encoder.rb +7 -29
- data/lib/fontisan/models/validation_report.rb +227 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +4 -8
- data/lib/fontisan/tables/cmap.rb +82 -2
- data/lib/fontisan/tables/glyf.rb +118 -0
- data/lib/fontisan/tables/head.rb +60 -0
- data/lib/fontisan/tables/hhea.rb +74 -0
- data/lib/fontisan/tables/maxp.rb +60 -0
- data/lib/fontisan/tables/name.rb +76 -0
- data/lib/fontisan/tables/os2.rb +113 -0
- data/lib/fontisan/tables/post.rb +57 -0
- data/lib/fontisan/validators/basic_validator.rb +85 -0
- data/lib/fontisan/validators/font_book_validator.rb +130 -0
- data/lib/fontisan/validators/opentype_validator.rb +112 -0
- data/lib/fontisan/validators/profile_loader.rb +139 -0
- data/lib/fontisan/validators/validator.rb +484 -0
- data/lib/fontisan/validators/web_font_validator.rb +102 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +78 -6
- metadata +7 -11
- data/lib/fontisan/config/validation_rules.yml +0 -149
- data/lib/fontisan/validation/checksum_validator.rb +0 -170
- data/lib/fontisan/validation/consistency_validator.rb +0 -197
- data/lib/fontisan/validation/structure_validator.rb +0 -198
- data/lib/fontisan/validation/table_validator.rb +0 -158
- data/lib/fontisan/validation/validator.rb +0 -152
- data/lib/fontisan/validation/variable_font_validator.rb +0 -218
- data/lib/fontisan/validation/woff2_header_validator.rb +0 -278
- data/lib/fontisan/validation/woff2_table_validator.rb +0 -270
- 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
|
-
|
|
266
|
+
# Use new validation framework with production profile
|
|
267
|
+
report = Fontisan.validate(@output_path, profile: :production)
|
|
267
268
|
|
|
268
|
-
|
|
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
|
-
|
|
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
|
|
data/lib/fontisan/tables/cmap.rb
CHANGED
|
@@ -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
|
data/lib/fontisan/tables/glyf.rb
CHANGED
|
@@ -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
|
data/lib/fontisan/tables/head.rb
CHANGED
|
@@ -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
|
data/lib/fontisan/tables/hhea.rb
CHANGED
|
@@ -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
|