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.
- checksums.yaml +4 -4
- data/.rubocop.yml +103 -0
- data/.rubocop_todo.yml +65 -361
- data/CHANGELOG.md +116 -0
- data/Gemfile +1 -1
- data/README.adoc +106 -27
- data/Rakefile +12 -7
- data/benchmark/variation_quick_bench.rb +1 -1
- data/docs/APPLE_LEGACY_FONTS.adoc +173 -0
- data/docs/COLLECTION_VALIDATION.adoc +143 -0
- data/docs/COLOR_FONTS.adoc +127 -0
- data/docs/DOCUMENTATION_SUMMARY.md +141 -0
- data/docs/FONT_HINTING.adoc +9 -1
- data/docs/VALIDATION.adoc +254 -0
- data/docs/WOFF_WOFF2_FORMATS.adoc +94 -0
- data/lib/fontisan/cli.rb +45 -13
- data/lib/fontisan/collection/dfont_builder.rb +2 -1
- data/lib/fontisan/commands/convert_command.rb +2 -4
- data/lib/fontisan/commands/info_command.rb +3 -3
- data/lib/fontisan/commands/pack_command.rb +2 -1
- data/lib/fontisan/commands/validate_command.rb +157 -6
- data/lib/fontisan/converters/collection_converter.rb +22 -13
- data/lib/fontisan/converters/svg_generator.rb +2 -1
- data/lib/fontisan/converters/woff2_encoder.rb +6 -6
- data/lib/fontisan/converters/woff_writer.rb +3 -1
- data/lib/fontisan/font_loader.rb +7 -6
- data/lib/fontisan/formatters/text_formatter.rb +18 -14
- data/lib/fontisan/hints/hint_converter.rb +1 -1
- data/lib/fontisan/hints/hint_validator.rb +13 -10
- data/lib/fontisan/hints/truetype_instruction_analyzer.rb +15 -8
- data/lib/fontisan/hints/truetype_instruction_generator.rb +1 -1
- data/lib/fontisan/models/collection_validation_report.rb +104 -0
- data/lib/fontisan/models/font_report.rb +24 -0
- data/lib/fontisan/models/validation_report.rb +7 -2
- data/lib/fontisan/open_type_font.rb +18 -425
- data/lib/fontisan/optimizers/charstring_rewriter.rb +1 -1
- data/lib/fontisan/optimizers/subroutine_optimizer.rb +6 -2
- data/lib/fontisan/sfnt_font.rb +699 -0
- data/lib/fontisan/sfnt_table.rb +264 -0
- data/lib/fontisan/subset/glyph_mapping.rb +2 -0
- data/lib/fontisan/subset/table_subsetter.rb +2 -2
- data/lib/fontisan/tables/cblc.rb +8 -4
- data/lib/fontisan/tables/cff/index.rb +2 -0
- data/lib/fontisan/tables/cff.rb +6 -3
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +1 -1
- data/lib/fontisan/tables/cff2.rb +1 -1
- data/lib/fontisan/tables/cmap.rb +5 -5
- data/lib/fontisan/tables/cmap_table.rb +231 -0
- data/lib/fontisan/tables/glyf.rb +8 -10
- data/lib/fontisan/tables/glyf_table.rb +255 -0
- data/lib/fontisan/tables/head.rb +3 -3
- data/lib/fontisan/tables/head_table.rb +111 -0
- data/lib/fontisan/tables/hhea.rb +4 -4
- data/lib/fontisan/tables/hhea_table.rb +255 -0
- data/lib/fontisan/tables/hmtx_table.rb +191 -0
- data/lib/fontisan/tables/loca_table.rb +212 -0
- data/lib/fontisan/tables/maxp.rb +2 -2
- data/lib/fontisan/tables/maxp_table.rb +258 -0
- data/lib/fontisan/tables/name.rb +1 -1
- data/lib/fontisan/tables/name_table.rb +176 -0
- data/lib/fontisan/tables/os2.rb +8 -8
- data/lib/fontisan/tables/os2_table.rb +329 -0
- data/lib/fontisan/tables/post.rb +2 -2
- data/lib/fontisan/tables/post_table.rb +183 -0
- data/lib/fontisan/tables/sbix.rb +5 -4
- data/lib/fontisan/true_type_font.rb +12 -464
- data/lib/fontisan/utilities/checksum_calculator.rb +0 -44
- data/lib/fontisan/validation/collection_validator.rb +4 -2
- data/lib/fontisan/validators/basic_validator.rb +11 -21
- data/lib/fontisan/validators/font_book_validator.rb +29 -50
- data/lib/fontisan/validators/opentype_validator.rb +24 -28
- data/lib/fontisan/validators/validator.rb +87 -66
- data/lib/fontisan/validators/web_font_validator.rb +16 -21
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/glyf_transformer.rb +31 -8
- data/lib/fontisan/woff2/hmtx_transformer.rb +2 -1
- data/lib/fontisan/woff2/table_transformer.rb +4 -2
- data/lib/fontisan/woff2_font.rb +4 -2
- data/lib/fontisan/woff_font.rb +46 -30
- data/lib/fontisan.rb +2 -2
- data/scripts/compare_stack_aware.rb +1 -1
- data/scripts/measure_optimization.rb +1 -2
- 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,
|
|
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,
|
|
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,
|
|
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,
|
|
55
|
-
|
|
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,
|
|
60
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
75
|
-
|
|
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,
|
|
80
|
-
|
|
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,
|
|
85
|
-
|
|
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,
|
|
90
|
-
|
|
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,
|
|
95
|
-
|
|
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,
|
|
100
|
-
|
|
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,
|
|
105
|
-
|
|
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,
|
|
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,
|
|
115
|
-
|
|
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,
|
|
120
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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(
|
|
46
|
+
glyf = font.table("glyf")
|
|
48
47
|
next true unless glyf # Skip if CFF font
|
|
49
48
|
|
|
50
|
-
loca = font.table(
|
|
51
|
-
head = font.table(
|
|
52
|
-
maxp = font.table(
|
|
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(
|
|
57
|
+
glyf = font.table("glyf")
|
|
59
58
|
next true unless glyf
|
|
60
59
|
|
|
61
|
-
loca = font.table(
|
|
62
|
-
head = font.table(
|
|
63
|
-
maxp = font.table(
|
|
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(
|
|
68
|
+
glyf = font.table("glyf")
|
|
70
69
|
next true unless glyf
|
|
71
70
|
|
|
72
|
-
loca = font.table(
|
|
73
|
-
head = font.table(
|
|
74
|
-
maxp = font.table(
|
|
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,
|
|
83
|
-
|
|
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,
|
|
88
|
-
|
|
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,
|
|
93
|
-
|
|
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(
|
|
99
|
-
maxp = font.table(
|
|
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 |
|
|
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,
|
|
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
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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,
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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,
|
|
37
|
-
|
|
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,
|
|
42
|
-
|
|
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(
|
|
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,
|
|
57
|
-
|
|
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,
|
|
62
|
-
|
|
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(
|
|
63
|
+
glyf = font.table("glyf")
|
|
68
64
|
next true unless glyf
|
|
69
65
|
|
|
70
|
-
loca = font.table(
|
|
71
|
-
head = font.table(
|
|
72
|
-
maxp = font.table(
|
|
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,
|
|
78
|
-
|
|
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,
|
|
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
|
|
data/lib/fontisan/version.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|