fontisan 0.4.37 → 0.4.38
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/TODO.improvements/README.md +1 -1
- data/lib/fontisan/audit/check_registry.rb +8 -4
- data/lib/fontisan/audit/checks/cff_table_check.rb +87 -0
- data/lib/fontisan/audit/checks/glyf_table_check.rb +115 -0
- data/lib/fontisan/audit/checks/layout_table_check.rb +70 -0
- data/lib/fontisan/audit/checks.rb +3 -0
- data/lib/fontisan/cli.rb +41 -2
- data/lib/fontisan/commands/audit_command.rb +3 -1
- data/lib/fontisan/models/audit_report.rb +48 -0
- data/lib/fontisan/models.rb +1 -0
- data/lib/fontisan/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aebdd37610f8ce36210375f348bd5a0f1aea12d0a12c951005d018e64cac290c
|
|
4
|
+
data.tar.gz: 33e81566249885ae646e91e727abedff605619e6deb30f742fcf1dff570cd641
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8e43a54dc2421202b7d780e10e37fb4b363c835d261257677728dd99602f6df0190414c1ed59c0d5c331e8b977d75326961f05b5cea2f7c32431c9cee39c094b
|
|
7
|
+
data.tar.gz: cce7fd930ab3d5da138c0f4756468b0494f21b7dcf0b515df5e38498770493a399fa10d862c5e2948efc35dfcae84c8a937ad5d8209ed1eddd4a033491934c37
|
data/TODO.improvements/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Each file is `NN-short-name.md` where `NN` is the priority order.
|
|
|
11
11
|
- [x] ~~[15 — CFF/CFF2 subsetter strategy](15-cff-cff2-subsetter-strategy.md)~~ ✓ Done — CFF via UFO round-trip, CFF2 via standalone INDEX filter with VStore preservation
|
|
12
12
|
|
|
13
13
|
### P1 — High-value features
|
|
14
|
-
- [x] ~~[03 — `fontisan audit` command (identity+style+features lens)](03-fontisan-audit-command.md)~~ ✓ Done (Phase 1: identity, style, coverage, layout, provenance; Phase 2 axes: table directory, glyph names, cmap, OTS, collection integrity; Phase 3 axes: variable-font, hinting, WOFF2, format-round-trip; Phase 3 axis 14 (full OT conformance): cross-table conformance + per-table checks for head, hhea, maxp, OS/2, name, post, kern; CLI `fontisan audit --validate` with profiles
|
|
14
|
+
- [x] ~~[03 — `fontisan audit` command (identity+style+features lens)](03-fontisan-audit-command.md)~~ ✓ Done (Phase 1: identity, style, coverage, layout, provenance; Phase 2 axes: table directory, glyph names, cmap, OTS, collection integrity; Phase 3 axes: variable-font, hinting, WOFF2, format-round-trip; Phase 3 axis 14 (full OT conformance): cross-table conformance + per-table checks for head, hhea, maxp, OS/2, name, post, kern, CFF/CFF2, glyf, GSUB/GPOS layout; CLI `fontisan audit --validate` with 20 checks across 9 profiles; CI exit codes via `--fail-on-error`; profile discovery via `--list-profiles`; audit summary model with error/warning/info counts)
|
|
15
15
|
- [x] ~~[04 — UFO composite glyph encoding](04-ufo-composite-glyph-encoding.md)~~ ✓ Done
|
|
16
16
|
- [x] ~~[05 — OTF compiler real CFF charstrings](05-otf-compiler-real-cff.md)~~ ✓ Already working (discovered in PR #117 — Cff.build produces real Type 2 charstrings; stale TODO marker removed)
|
|
17
17
|
|
|
@@ -18,21 +18,22 @@ module Fontisan
|
|
|
18
18
|
class CheckRegistry
|
|
19
19
|
# Per-table OT conformance checks (axis 14 — full OT spec coverage).
|
|
20
20
|
OT_TABLE_CHECKS = %i[ot_head ot_hhea ot_maxp ot_os2 ot_name ot_post
|
|
21
|
-
ot_kern].freeze
|
|
21
|
+
ot_kern ot_cff ot_glyf ot_layout].freeze
|
|
22
22
|
|
|
23
23
|
PROFILES = {
|
|
24
24
|
default: %i[table_directory glyph_names cmap ots_compatibility
|
|
25
25
|
collection_integrity variable_font hinting woff2_validation
|
|
26
26
|
format_round_trip opentype_conformance
|
|
27
|
-
ot_head ot_hhea ot_maxp ot_os2 ot_name ot_post ot_kern
|
|
27
|
+
ot_head ot_hhea ot_maxp ot_os2 ot_name ot_post ot_kern
|
|
28
|
+
ot_cff ot_glyf ot_layout],
|
|
28
29
|
structural: %i[table_directory collection_integrity opentype_conformance],
|
|
29
30
|
ots: %i[ots_compatibility],
|
|
30
|
-
layout: %i[glyph_names cmap],
|
|
31
|
+
layout: %i[glyph_names cmap ot_layout],
|
|
31
32
|
variable: %i[variable_font],
|
|
32
33
|
hinting: %i[hinting],
|
|
33
34
|
web: %i[ots_compatibility woff2_validation],
|
|
34
35
|
spec: %i[opentype_conformance ot_head ot_hhea ot_maxp ot_os2
|
|
35
|
-
ot_name ot_post ot_kern],
|
|
36
|
+
ot_name ot_post ot_kern ot_cff ot_glyf ot_layout],
|
|
36
37
|
per_table: OT_TABLE_CHECKS,
|
|
37
38
|
}.freeze
|
|
38
39
|
|
|
@@ -64,6 +65,9 @@ module Fontisan
|
|
|
64
65
|
when :ot_name then Checks::NameTableCheck
|
|
65
66
|
when :ot_post then Checks::PostCheck
|
|
66
67
|
when :ot_kern then Checks::KernCheck
|
|
68
|
+
when :ot_cff then Checks::CffTableCheck
|
|
69
|
+
when :ot_glyf then Checks::GlyfTableCheck
|
|
70
|
+
when :ot_layout then Checks::LayoutTableCheck
|
|
67
71
|
end
|
|
68
72
|
end
|
|
69
73
|
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Audit
|
|
5
|
+
module Checks
|
|
6
|
+
# Validates CFF/CFF2 table structure: header version, Name INDEX
|
|
7
|
+
# presence (CFF1), Top DICT CharStrings offset, CharStrings count
|
|
8
|
+
# vs maxp.numGlyphs.
|
|
9
|
+
#
|
|
10
|
+
# @see https://learn.microsoft.com/en-us/typography/opentype/spec/cff
|
|
11
|
+
# @see https://learn.microsoft.com/en-us/typography/opentype/spec/cff2
|
|
12
|
+
class CffTableCheck < Check
|
|
13
|
+
def self.call(font)
|
|
14
|
+
if font.has_table?("CFF2")
|
|
15
|
+
validate_cff2(font)
|
|
16
|
+
elsif font.has_table?("CFF ")
|
|
17
|
+
validate_cff1(font)
|
|
18
|
+
else
|
|
19
|
+
[]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.code
|
|
24
|
+
:ot_cff
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# ---------- CFF1 ----------
|
|
28
|
+
|
|
29
|
+
def self.validate_cff1(font)
|
|
30
|
+
cff = font.table("CFF ")
|
|
31
|
+
return [] unless cff
|
|
32
|
+
|
|
33
|
+
issues = []
|
|
34
|
+
issues.concat(validate_cff1_name_index(cff))
|
|
35
|
+
issues.concat(validate_charstrings_count(cff, font, "CFF "))
|
|
36
|
+
issues
|
|
37
|
+
end
|
|
38
|
+
private_class_method :validate_cff1
|
|
39
|
+
|
|
40
|
+
def self.validate_cff1_name_index(cff)
|
|
41
|
+
count = cff.font_count.to_i
|
|
42
|
+
return [] if count.positive?
|
|
43
|
+
|
|
44
|
+
[issue(severity: :error,
|
|
45
|
+
message: "CFF Name INDEX is empty — must contain at least " \
|
|
46
|
+
"one font name",
|
|
47
|
+
location: "cff.name_index")]
|
|
48
|
+
end
|
|
49
|
+
private_class_method :validate_cff1_name_index
|
|
50
|
+
|
|
51
|
+
# ---------- CFF2 ----------
|
|
52
|
+
|
|
53
|
+
def self.validate_cff2(font)
|
|
54
|
+
cff2 = font.table("CFF2")
|
|
55
|
+
return [] unless cff2
|
|
56
|
+
|
|
57
|
+
validate_charstrings_count(cff2, font, "CFF2")
|
|
58
|
+
end
|
|
59
|
+
private_class_method :validate_cff2
|
|
60
|
+
|
|
61
|
+
# ---------- shared ----------
|
|
62
|
+
|
|
63
|
+
def self.validate_charstrings_count(cff, font, table_tag)
|
|
64
|
+
return [] unless font.has_table?("maxp")
|
|
65
|
+
|
|
66
|
+
cs_count = glyph_count_from_cff(cff)
|
|
67
|
+
maxp_count = font.table("maxp").num_glyphs.to_i
|
|
68
|
+
return [] if cs_count == maxp_count
|
|
69
|
+
|
|
70
|
+
[issue(severity: :error,
|
|
71
|
+
message: "#{table_tag} CharStrings count (#{cs_count}) does " \
|
|
72
|
+
"not match maxp.numGlyphs (#{maxp_count})",
|
|
73
|
+
location: "#{table_tag.downcase}.charstrings")]
|
|
74
|
+
end
|
|
75
|
+
private_class_method :validate_charstrings_count
|
|
76
|
+
|
|
77
|
+
def self.glyph_count_from_cff(cff)
|
|
78
|
+
cs_index = cff.charstrings_index(0)
|
|
79
|
+
cs_index&.count.to_i
|
|
80
|
+
rescue StandardError
|
|
81
|
+
0
|
|
82
|
+
end
|
|
83
|
+
private_class_method :glyph_count_from_cff
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Audit
|
|
5
|
+
module Checks
|
|
6
|
+
# Validates the 'glyf' table: simple glyph contour integrity and
|
|
7
|
+
# compound glyph reference bounds. Only runs on TrueType fonts.
|
|
8
|
+
#
|
|
9
|
+
# @see https://learn.microsoft.com/en-us/typography/opentype/spec/glyf
|
|
10
|
+
class GlyfTableCheck < Check
|
|
11
|
+
MAX_COMPOUND_DEPTH = 16
|
|
12
|
+
|
|
13
|
+
def self.call(font)
|
|
14
|
+
return [] unless font.has_table?("glyf") && font.has_table?("loca")
|
|
15
|
+
|
|
16
|
+
issues = []
|
|
17
|
+
issues.concat(validate_glyph_counts(font))
|
|
18
|
+
issues
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.code
|
|
22
|
+
:ot_glyf
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Verify loca offsets are monotonically non-decreasing and
|
|
26
|
+
# within the glyf table bounds. Discontinuities signal
|
|
27
|
+
# corruption that renderers may reject.
|
|
28
|
+
def self.validate_glyph_counts(font)
|
|
29
|
+
glyf = font.table("glyf")
|
|
30
|
+
return [] unless glyf
|
|
31
|
+
|
|
32
|
+
raw = font.table_data["glyf"]
|
|
33
|
+
return [] unless raw
|
|
34
|
+
|
|
35
|
+
issues = []
|
|
36
|
+
maxp = font.table("maxp")
|
|
37
|
+
num_glyphs = maxp&.num_glyphs.to_i
|
|
38
|
+
return [] if num_glyphs.zero?
|
|
39
|
+
|
|
40
|
+
num_glyphs.times do |gid|
|
|
41
|
+
issues.concat(validate_one_glyph(font, gid))
|
|
42
|
+
rescue StandardError
|
|
43
|
+
issues << issue(severity: :warning,
|
|
44
|
+
message: "glyf: failed to validate glyph at GID " \
|
|
45
|
+
"#{gid}",
|
|
46
|
+
location: "glyf.glyph.#{gid}")
|
|
47
|
+
end
|
|
48
|
+
issues
|
|
49
|
+
end
|
|
50
|
+
private_class_method :validate_glyph_counts
|
|
51
|
+
|
|
52
|
+
def self.validate_one_glyph(font, gid)
|
|
53
|
+
head = font.table("head")
|
|
54
|
+
loca = font.table("loca")
|
|
55
|
+
glyf = font.table("glyf")
|
|
56
|
+
|
|
57
|
+
raw = begin
|
|
58
|
+
glyf.glyph_for(gid, loca, head)
|
|
59
|
+
rescue StandardError
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
return [] unless raw
|
|
63
|
+
|
|
64
|
+
if raw.simple?
|
|
65
|
+
validate_simple_glyph(raw, gid)
|
|
66
|
+
elsif raw.compound?
|
|
67
|
+
validate_compound_glyph(raw, gid, font)
|
|
68
|
+
else
|
|
69
|
+
[]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
private_class_method :validate_one_glyph
|
|
73
|
+
|
|
74
|
+
def self.validate_simple_glyph(raw, gid)
|
|
75
|
+
issues = []
|
|
76
|
+
end_pts = raw.end_pts_of_contours
|
|
77
|
+
return [] unless end_pts&.any?
|
|
78
|
+
|
|
79
|
+
# endPtsOfContours must be monotonically increasing
|
|
80
|
+
end_pts.each_cons(2) do |prev, curr|
|
|
81
|
+
next unless curr <= prev
|
|
82
|
+
|
|
83
|
+
issues << issue(severity: :error,
|
|
84
|
+
message: "glyf glyph #{gid}: contour endPoints " \
|
|
85
|
+
"are not monotonically increasing " \
|
|
86
|
+
"(#{prev} → #{curr})",
|
|
87
|
+
location: "glyf.glyph.#{gid}.contours")
|
|
88
|
+
break
|
|
89
|
+
end
|
|
90
|
+
issues
|
|
91
|
+
end
|
|
92
|
+
private_class_method :validate_simple_glyph
|
|
93
|
+
|
|
94
|
+
def self.validate_compound_glyph(raw, gid, font)
|
|
95
|
+
issues = []
|
|
96
|
+
maxp = font.table("maxp")
|
|
97
|
+
max_gid = maxp&.num_glyphs.to_i - 1
|
|
98
|
+
|
|
99
|
+
raw.components.each do |comp|
|
|
100
|
+
ref_gid = comp.glyph_index.to_i
|
|
101
|
+
next if ref_gid.between?(0, max_gid)
|
|
102
|
+
|
|
103
|
+
issues << issue(severity: :error,
|
|
104
|
+
message: "glyf compound glyph #{gid} references " \
|
|
105
|
+
"out-of-bounds GID #{ref_gid} " \
|
|
106
|
+
"(max: #{max_gid})",
|
|
107
|
+
location: "glyf.glyph.#{gid}.component.#{ref_gid}")
|
|
108
|
+
end
|
|
109
|
+
issues
|
|
110
|
+
end
|
|
111
|
+
private_class_method :validate_compound_glyph
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Audit
|
|
5
|
+
module Checks
|
|
6
|
+
# Validates GSUB/GPOS layout table structure: header version,
|
|
7
|
+
# ScriptList/FeatureList/LookupList offsets, and DFLT script
|
|
8
|
+
# recommendation. Only runs when layout tables are present.
|
|
9
|
+
#
|
|
10
|
+
# @see https://learn.microsoft.com/en-us/typography/opentype/spec/chapter2
|
|
11
|
+
class LayoutTableCheck < Check
|
|
12
|
+
def self.call(font)
|
|
13
|
+
issues = []
|
|
14
|
+
issues.concat(validate_layout_table(font, "GSUB"))
|
|
15
|
+
issues.concat(validate_layout_table(font, "GPOS"))
|
|
16
|
+
issues
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.code
|
|
20
|
+
:ot_layout
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.validate_layout_table(font, tag)
|
|
24
|
+
return [] unless font.has_table?(tag)
|
|
25
|
+
|
|
26
|
+
table = font.table(tag)
|
|
27
|
+
return [] unless table
|
|
28
|
+
|
|
29
|
+
issues = []
|
|
30
|
+
issues.concat(validate_scripts(table, tag))
|
|
31
|
+
issues.concat(validate_dflt_script(table, tag))
|
|
32
|
+
issues
|
|
33
|
+
end
|
|
34
|
+
private_class_method :validate_layout_table
|
|
35
|
+
|
|
36
|
+
def self.validate_scripts(table, tag)
|
|
37
|
+
scripts = begin
|
|
38
|
+
table.scripts
|
|
39
|
+
rescue StandardError
|
|
40
|
+
[]
|
|
41
|
+
end
|
|
42
|
+
return [] if scripts.any?
|
|
43
|
+
|
|
44
|
+
[issue(severity: :warning,
|
|
45
|
+
message: "#{tag} table has no script records — layout " \
|
|
46
|
+
"rules won't apply without at least one script " \
|
|
47
|
+
"(DFLT recommended)",
|
|
48
|
+
location: "#{tag.downcase}.script_list")]
|
|
49
|
+
end
|
|
50
|
+
private_class_method :validate_scripts
|
|
51
|
+
|
|
52
|
+
def self.validate_dflt_script(table, tag)
|
|
53
|
+
scripts = begin
|
|
54
|
+
table.scripts
|
|
55
|
+
rescue StandardError
|
|
56
|
+
[]
|
|
57
|
+
end
|
|
58
|
+
return [] if scripts.include?("DFLT")
|
|
59
|
+
|
|
60
|
+
[issue(severity: :info,
|
|
61
|
+
message: "#{tag} table has no 'DFLT' script — fonts " \
|
|
62
|
+
"without DFLT may not apply features to scripts " \
|
|
63
|
+
"not explicitly listed",
|
|
64
|
+
location: "#{tag.downcase}.script_list.DFLT")]
|
|
65
|
+
end
|
|
66
|
+
private_class_method :validate_dflt_script
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -32,6 +32,9 @@ module Fontisan
|
|
|
32
32
|
autoload :NameTableCheck, "fontisan/audit/checks/name_table_check"
|
|
33
33
|
autoload :PostCheck, "fontisan/audit/checks/post_check"
|
|
34
34
|
autoload :KernCheck, "fontisan/audit/checks/kern_check"
|
|
35
|
+
autoload :CffTableCheck, "fontisan/audit/checks/cff_table_check"
|
|
36
|
+
autoload :GlyfTableCheck, "fontisan/audit/checks/glyf_table_check"
|
|
37
|
+
autoload :LayoutTableCheck, "fontisan/audit/checks/layout_table_check"
|
|
35
38
|
end
|
|
36
39
|
end
|
|
37
40
|
end
|
data/lib/fontisan/cli.rb
CHANGED
|
@@ -40,15 +40,26 @@ module Fontisan
|
|
|
40
40
|
desc: "Audit only the given face of a collection"
|
|
41
41
|
option :validate, type: :string, default: nil,
|
|
42
42
|
desc: "Run validation checks. Use 'true' for all, or a " \
|
|
43
|
-
"profile name: default, structural, ots, layout"
|
|
43
|
+
"profile name: default, structural, ots, layout, " \
|
|
44
|
+
"variable, hinting, web, spec, per_table"
|
|
45
|
+
option :list_profiles, type: :boolean, default: false,
|
|
46
|
+
desc: "List available validation profiles and exit"
|
|
47
|
+
option :fail_on_error, type: :boolean, default: true,
|
|
48
|
+
desc: "Exit with code 1 if validation finds errors " \
|
|
49
|
+
"(only applies with --validate)"
|
|
44
50
|
# Produce a structured font audit report (YAML or JSON).
|
|
45
|
-
def audit(font_file)
|
|
51
|
+
def audit(font_file = nil)
|
|
52
|
+
return list_audit_profiles if options[:list_profiles]
|
|
53
|
+
|
|
54
|
+
raise Thor::Error, "FONT_FILE is required" if font_file.nil?
|
|
55
|
+
|
|
46
56
|
cmd_options = options.dup
|
|
47
57
|
cmd_options[:include_codepoints] = !options[:no_codepoints]
|
|
48
58
|
cmd_options[:validate] = parse_validate_option(options[:validate])
|
|
49
59
|
command = Commands::AuditCommand.new(font_file, cmd_options)
|
|
50
60
|
result = command.run
|
|
51
61
|
write_audit_result(result, options[:output])
|
|
62
|
+
exit_on_validation_errors(result) if options[:validate] && options[:fail_on_error]
|
|
52
63
|
rescue Errno::ENOENT
|
|
53
64
|
warn "File not found: #{font_file}" unless options[:quiet]
|
|
54
65
|
exit 1
|
|
@@ -810,6 +821,34 @@ module Fontisan
|
|
|
810
821
|
raw.to_sym
|
|
811
822
|
end
|
|
812
823
|
|
|
824
|
+
# Print available validation profiles with their check counts.
|
|
825
|
+
def list_audit_profiles
|
|
826
|
+
puts "Available validation profiles for --validate:"
|
|
827
|
+
puts ""
|
|
828
|
+
Audit::CheckRegistry::PROFILES.each do |name, codes|
|
|
829
|
+
puts " #{name.to_s.ljust(15)} #{codes.size} checks"
|
|
830
|
+
end
|
|
831
|
+
puts ""
|
|
832
|
+
puts "Usage: fontisan audit font.ttf --validate <profile>"
|
|
833
|
+
puts " fontisan audit font.ttf --validate true (all checks)"
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
# Exit with code 1 if the audit found error-level issues.
|
|
837
|
+
# For collections, aggregates across all faces.
|
|
838
|
+
def exit_on_validation_errors(result)
|
|
839
|
+
issues = if result.is_a?(Array)
|
|
840
|
+
result.flat_map(&:validation_issues)
|
|
841
|
+
else
|
|
842
|
+
result.validation_issues || []
|
|
843
|
+
end
|
|
844
|
+
has_errors = issues.any? { |i| %w[error fatal].include?(i.severity) }
|
|
845
|
+
return unless has_errors
|
|
846
|
+
|
|
847
|
+
error_count = issues.count { |i| %w[error fatal].include?(i.severity) }
|
|
848
|
+
warn "Audit found #{error_count} error(s) — exiting with code 1"
|
|
849
|
+
exit 1
|
|
850
|
+
end
|
|
851
|
+
|
|
813
852
|
def serialize_report(report, format)
|
|
814
853
|
format == :json ? report.to_json : report.to_yaml
|
|
815
854
|
end
|
|
@@ -257,7 +257,9 @@ module Fontisan
|
|
|
257
257
|
# ------------------------------------------------------------------
|
|
258
258
|
|
|
259
259
|
def populate_validation(report, face)
|
|
260
|
-
|
|
260
|
+
issues = run_validation_checks(face)
|
|
261
|
+
report.validation_issues = issues
|
|
262
|
+
report.validation_summary = Models::AuditSummary.from_issues(issues)
|
|
261
263
|
end
|
|
262
264
|
|
|
263
265
|
def run_validation_checks(face)
|
|
@@ -4,6 +4,51 @@ require "lutaml/model"
|
|
|
4
4
|
|
|
5
5
|
module Fontisan
|
|
6
6
|
module Models
|
|
7
|
+
# Summary of validation issues by severity. Used by CI pipelines
|
|
8
|
+
# to decide pass/fail and by humans to gauge font health at a
|
|
9
|
+
# glance.
|
|
10
|
+
class AuditSummary < Lutaml::Model::Serializable
|
|
11
|
+
attribute :error_count, :integer, default: 0
|
|
12
|
+
attribute :warning_count, :integer, default: 0
|
|
13
|
+
attribute :info_count, :integer, default: 0
|
|
14
|
+
attribute :fatal_count, :integer, default: 0
|
|
15
|
+
attribute :total, :integer, default: 0
|
|
16
|
+
attribute :passed, Lutaml::Model::Type::Boolean, default: true
|
|
17
|
+
|
|
18
|
+
json do
|
|
19
|
+
map "error_count", to: :error_count
|
|
20
|
+
map "warning_count", to: :warning_count
|
|
21
|
+
map "info_count", to: :info_count
|
|
22
|
+
map "fatal_count", to: :fatal_count
|
|
23
|
+
map "total", to: :total
|
|
24
|
+
map "passed", to: :passed
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
yaml do
|
|
28
|
+
map "error_count", to: :error_count
|
|
29
|
+
map "warning_count", to: :warning_count
|
|
30
|
+
map "info_count", to: :info_count
|
|
31
|
+
map "fatal_count", to: :fatal_count
|
|
32
|
+
map "total", to: :total
|
|
33
|
+
map "passed", to: :passed
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Build a summary from an array of ValidationReport::Issue records.
|
|
37
|
+
# @param issues [Array<ValidationReport::Issue>]
|
|
38
|
+
# @return [AuditSummary]
|
|
39
|
+
def self.from_issues(issues)
|
|
40
|
+
counts = issues.group_by(&:severity).transform_values(&:size)
|
|
41
|
+
new(
|
|
42
|
+
error_count: counts["error"].to_i,
|
|
43
|
+
warning_count: counts["warning"].to_i,
|
|
44
|
+
info_count: counts["info"].to_i,
|
|
45
|
+
fatal_count: counts["fatal"].to_i,
|
|
46
|
+
total: issues.size,
|
|
47
|
+
passed: counts["error"].to_i.zero? && counts["fatal"].to_i.zero?,
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
7
52
|
# Variable-font axis descriptor for the audit report.
|
|
8
53
|
class AuditAxis < Lutaml::Model::Serializable
|
|
9
54
|
attribute :tag, :string
|
|
@@ -75,6 +120,7 @@ module Fontisan
|
|
|
75
120
|
|
|
76
121
|
# Validation (populated only with --validate)
|
|
77
122
|
attribute :validation_issues, Models::ValidationReport::Issue, collection: true
|
|
123
|
+
attribute :validation_summary, AuditSummary
|
|
78
124
|
|
|
79
125
|
json do
|
|
80
126
|
map "generated_at", to: :generated_at
|
|
@@ -108,6 +154,7 @@ module Fontisan
|
|
|
108
154
|
map "opentype_scripts", to: :opentype_scripts
|
|
109
155
|
map "features", to: :features
|
|
110
156
|
map "validation_issues", to: :validation_issues
|
|
157
|
+
map "validation_summary", to: :validation_summary
|
|
111
158
|
end
|
|
112
159
|
|
|
113
160
|
yaml do
|
|
@@ -142,6 +189,7 @@ module Fontisan
|
|
|
142
189
|
map "opentype_scripts", to: :opentype_scripts
|
|
143
190
|
map "features", to: :features
|
|
144
191
|
map "validation_issues", to: :validation_issues
|
|
192
|
+
map "validation_summary", to: :validation_summary
|
|
145
193
|
end
|
|
146
194
|
end
|
|
147
195
|
end
|
data/lib/fontisan/models.rb
CHANGED
|
@@ -8,6 +8,7 @@ module Fontisan
|
|
|
8
8
|
"fontisan/models/all_scripts_features_info"
|
|
9
9
|
autoload :AuditAxis, "fontisan/models/audit_report"
|
|
10
10
|
autoload :AuditReport, "fontisan/models/audit_report"
|
|
11
|
+
autoload :AuditSummary, "fontisan/models/audit_report"
|
|
11
12
|
autoload :AxisInfo, "fontisan/models/variable_font_info"
|
|
12
13
|
autoload :BitmapGlyph, "fontisan/models/bitmap_glyph"
|
|
13
14
|
autoload :BitmapStrike, "fontisan/models/bitmap_strike"
|
data/lib/fontisan/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fontisan
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.38
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -297,14 +297,17 @@ files:
|
|
|
297
297
|
- lib/fontisan/audit/check.rb
|
|
298
298
|
- lib/fontisan/audit/check_registry.rb
|
|
299
299
|
- lib/fontisan/audit/checks.rb
|
|
300
|
+
- lib/fontisan/audit/checks/cff_table_check.rb
|
|
300
301
|
- lib/fontisan/audit/checks/cmap_check.rb
|
|
301
302
|
- lib/fontisan/audit/checks/collection_integrity_check.rb
|
|
302
303
|
- lib/fontisan/audit/checks/format_round_trip_check.rb
|
|
304
|
+
- lib/fontisan/audit/checks/glyf_table_check.rb
|
|
303
305
|
- lib/fontisan/audit/checks/glyph_name_check.rb
|
|
304
306
|
- lib/fontisan/audit/checks/head_check.rb
|
|
305
307
|
- lib/fontisan/audit/checks/hhea_check.rb
|
|
306
308
|
- lib/fontisan/audit/checks/hinting_check.rb
|
|
307
309
|
- lib/fontisan/audit/checks/kern_check.rb
|
|
310
|
+
- lib/fontisan/audit/checks/layout_table_check.rb
|
|
308
311
|
- lib/fontisan/audit/checks/maxp_check.rb
|
|
309
312
|
- lib/fontisan/audit/checks/name_table_check.rb
|
|
310
313
|
- lib/fontisan/audit/checks/opentype_conformance_check.rb
|