fontisan 0.4.36 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3957074ba3c679c22715fa1f1aabf4dbd3cc7334891f729d64988ec231237f3b
4
- data.tar.gz: 18129617714b672947d8f39a88e7c06a7644e4ff12875c383321635a6e051377
3
+ metadata.gz: aebdd37610f8ce36210375f348bd5a0f1aea12d0a12c951005d018e64cac290c
4
+ data.tar.gz: 33e81566249885ae646e91e727abedff605619e6deb30f742fcf1dff570cd641
5
5
  SHA512:
6
- metadata.gz: 3315d7c7f3cd824dadcf4ecddc63319a5661ac13ebf8383dfab8f2eda3fffd2713a1a039df922d58073cc0b99615d0d608de5641f67528e48f2aa70678e5b8f1
7
- data.tar.gz: 1bd98a99dc9902aeae6e5ceb3c40f16d01c7a61c16ae2c94f1c97f212602de2675602ab736545869640500dc49ac5f4bef6302bf3b43c34593071e94b513c448
6
+ metadata.gz: 8e43a54dc2421202b7d780e10e37fb4b363c835d261257677728dd99602f6df0190414c1ed59c0d5c331e8b977d75326961f05b5cea2f7c32431c9cee39c094b
7
+ data.tar.gz: cce7fd930ab3d5da138c0f4756468b0494f21b7dcf0b515df5e38498770493a399fa10d862c5e2948efc35dfcae84c8a937ad5d8209ed1eddd4a033491934c37
@@ -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, OpenType conformance foundation; CLI `fontisan audit --validate` with profiles: default, structural, ots, layout, variable, hinting, web, spec)
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
 
@@ -16,17 +16,25 @@ module Fontisan
16
16
  # @example Run a specific profile
17
17
  # CheckRegistry.for(:ots) # => [OtsCompatibilityCheck]
18
18
  class CheckRegistry
19
+ # Per-table OT conformance checks (axis 14 — full OT spec coverage).
20
+ OT_TABLE_CHECKS = %i[ot_head ot_hhea ot_maxp ot_os2 ot_name ot_post
21
+ ot_kern ot_cff ot_glyf ot_layout].freeze
22
+
19
23
  PROFILES = {
20
24
  default: %i[table_directory glyph_names cmap ots_compatibility
21
25
  collection_integrity variable_font hinting woff2_validation
22
- format_round_trip opentype_conformance],
26
+ format_round_trip opentype_conformance
27
+ ot_head ot_hhea ot_maxp ot_os2 ot_name ot_post ot_kern
28
+ ot_cff ot_glyf ot_layout],
23
29
  structural: %i[table_directory collection_integrity opentype_conformance],
24
30
  ots: %i[ots_compatibility],
25
- layout: %i[glyph_names cmap],
31
+ layout: %i[glyph_names cmap ot_layout],
26
32
  variable: %i[variable_font],
27
33
  hinting: %i[hinting],
28
34
  web: %i[ots_compatibility woff2_validation],
29
- spec: %i[opentype_conformance],
35
+ spec: %i[opentype_conformance ot_head ot_hhea ot_maxp ot_os2
36
+ ot_name ot_post ot_kern ot_cff ot_glyf ot_layout],
37
+ per_table: OT_TABLE_CHECKS,
30
38
  }.freeze
31
39
 
32
40
  # @param profile [Symbol]
@@ -50,6 +58,16 @@ module Fontisan
50
58
  when :woff2_validation then Checks::Woff2ValidationCheck
51
59
  when :format_round_trip then Checks::FormatRoundTripCheck
52
60
  when :opentype_conformance then Checks::OpenTypeConformanceCheck
61
+ when :ot_head then Checks::HeadCheck
62
+ when :ot_hhea then Checks::HheaCheck
63
+ when :ot_maxp then Checks::MaxpCheck
64
+ when :ot_os2 then Checks::Os2Check
65
+ when :ot_name then Checks::NameTableCheck
66
+ when :ot_post then Checks::PostCheck
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
53
71
  end
54
72
  end
55
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,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Audit
5
+ module Checks
6
+ # Validates the 'head' table per the OpenType spec.
7
+ #
8
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/head
9
+ class HeadCheck < Check
10
+ HEAD_MAGIC = 0x5F0F3CF5
11
+ MIN_UPM = 16
12
+ MAX_UPM = 16_384
13
+ MAC_STYLE_DEFINED_BITS = 0x00FF
14
+ MAC_STYLE_RESERVED_BITS = 0xFF00
15
+
16
+ def self.call(font)
17
+ return [] unless font.has_table?("head")
18
+
19
+ head = font.table("head")
20
+ issues = []
21
+ issues.concat(validate_magic(head))
22
+ issues.concat(validate_upm(head))
23
+ issues.concat(validate_loc_format(head))
24
+ issues.concat(validate_dates(head))
25
+ issues.concat(validate_mac_style(head))
26
+ issues
27
+ end
28
+
29
+ def self.code
30
+ :ot_head
31
+ end
32
+
33
+ def self.validate_magic(head)
34
+ return [] if head.magic_number == HEAD_MAGIC
35
+
36
+ [issue(severity: :error,
37
+ message: "head.magicNumber is 0x#{head.magic_number.to_i.to_s(16)} " \
38
+ "but must be 0x#{HEAD_MAGIC.to_s(16)}",
39
+ location: "head.magic_number")]
40
+ end
41
+ private_class_method :validate_magic
42
+
43
+ def self.validate_upm(head)
44
+ upm = head.units_per_em.to_i
45
+ return [] if upm.between?(MIN_UPM, MAX_UPM)
46
+
47
+ [issue(severity: :error,
48
+ message: "head.unitsPerEm is #{upm} but must be between " \
49
+ "#{MIN_UPM} and #{MAX_UPM}",
50
+ location: "head.units_per_em")]
51
+ end
52
+ private_class_method :validate_upm
53
+
54
+ def self.validate_loc_format(head)
55
+ fmt = head.index_to_loc_format.to_i
56
+ return [] if [0, 1].include?(fmt)
57
+
58
+ [issue(severity: :error,
59
+ message: "head.indexToLocFormat is #{fmt} but must be 0 " \
60
+ "(short) or 1 (long)",
61
+ location: "head.index_to_loc_format")]
62
+ end
63
+ private_class_method :validate_loc_format
64
+
65
+ def self.validate_dates(head)
66
+ issues = []
67
+ if head.created_raw.to_i.zero?
68
+ issues << issue(severity: :info,
69
+ message: "head.created is 0 — creation date not set",
70
+ location: "head.created")
71
+ end
72
+ if head.modified_raw.to_i.zero?
73
+ issues << issue(severity: :info,
74
+ message: "head.modified is 0 — modification date not set",
75
+ location: "head.modified")
76
+ end
77
+ issues
78
+ end
79
+ private_class_method :validate_dates
80
+
81
+ def self.validate_mac_style(head)
82
+ style = head.mac_style.to_i
83
+ issues = []
84
+ unless (style & MAC_STYLE_RESERVED_BITS).zero?
85
+ issues << issue(severity: :warning,
86
+ message: "head.macStyle uses reserved bits " \
87
+ "(0x#{style.to_s(16)}, bits 8-15 must be 0)",
88
+ location: "head.mac_style")
89
+ end
90
+ issues
91
+ end
92
+ private_class_method :validate_mac_style
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Audit
5
+ module Checks
6
+ # Validates the 'hhea' table per the OpenType spec.
7
+ #
8
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/hhea
9
+ class HheaCheck < Check
10
+ def self.call(font)
11
+ return [] unless font.has_table?("hhea")
12
+
13
+ hhea = font.table("hhea")
14
+ issues = []
15
+ issues.concat(validate_ascent(hhea))
16
+ issues.concat(validate_descent(hhea))
17
+ issues.concat(validate_line_gap(hhea))
18
+ issues.concat(validate_advance_width_max(hhea))
19
+ issues
20
+ end
21
+
22
+ def self.code
23
+ :ot_hhea
24
+ end
25
+
26
+ def self.validate_ascent(hhea)
27
+ return [] if hhea.ascent.to_i.positive?
28
+
29
+ [issue(severity: :warning,
30
+ message: "hhea.ascent is #{hhea.ascent} but should be positive " \
31
+ "(ascender typically rises above the baseline)",
32
+ location: "hhea.ascent")]
33
+ end
34
+ private_class_method :validate_ascent
35
+
36
+ def self.validate_descent(hhea)
37
+ return [] if hhea.descent.to_i <= 0
38
+
39
+ [issue(severity: :warning,
40
+ message: "hhea.descent is #{hhea.descent} but should be ≤ 0 " \
41
+ "(descender typically falls below the baseline)",
42
+ location: "hhea.descent")]
43
+ end
44
+ private_class_method :validate_descent
45
+
46
+ def self.validate_line_gap(hhea)
47
+ return [] if hhea.line_gap.to_i >= 0
48
+
49
+ [issue(severity: :info,
50
+ message: "hhea.lineGap is #{hhea.line_gap} — negative line " \
51
+ "gap is unusual but not spec-prohibited",
52
+ location: "hhea.line_gap")]
53
+ end
54
+ private_class_method :validate_line_gap
55
+
56
+ def self.validate_advance_width_max(hhea)
57
+ return [] unless hhea.advance_width_max.to_i <= 0
58
+
59
+ [issue(severity: :warning,
60
+ message: "hhea.advanceWidthMax is #{hhea.advance_width_max} " \
61
+ "but should be positive (the widest glyph advance)",
62
+ location: "hhea.advance_width_max")]
63
+ end
64
+ private_class_method :validate_advance_width_max
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Audit
5
+ module Checks
6
+ # Validates the 'kern' table if present. The kern table is
7
+ # deprecated by the OpenType spec — GPOS is the replacement.
8
+ # Many legacy fonts still ship kern; this check warns about
9
+ # deprecation and validates basic structure.
10
+ #
11
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/kern
12
+ class KernCheck < Check
13
+ def self.call(font)
14
+ return [] unless font.has_table?("kern")
15
+
16
+ issues = [deprecation_warning]
17
+ issues.concat(validate_gpos_presence(font))
18
+ issues
19
+ end
20
+
21
+ def self.code
22
+ :ot_kern
23
+ end
24
+
25
+ def self.deprecation_warning
26
+ issue(severity: :warning,
27
+ message: "kern table is present but deprecated by the " \
28
+ "OpenType spec — use GPOS instead. Some modern " \
29
+ "renderers (HarfBuzz, Core Text) ignore kern",
30
+ location: "tables.kern")
31
+ end
32
+ private_class_method :deprecation_warning
33
+
34
+ def self.validate_gpos_presence(font)
35
+ return [] if font.has_table?("GPOS")
36
+
37
+ [issue(severity: :warning,
38
+ message: "kern table present but no GPOS table — " \
39
+ "kerning data should be migrated to a GPOS " \
40
+ "'kern' feature for full renderer support",
41
+ location: "tables.GPOS")]
42
+ end
43
+ private_class_method :validate_gpos_presence
44
+ end
45
+ end
46
+ end
47
+ 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
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Audit
5
+ module Checks
6
+ # Validates the 'maxp' table per the OpenType spec.
7
+ #
8
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/maxp
9
+ class MaxpCheck < Check
10
+ MAX_GLYPHS = 0xFFFF
11
+ MAX_STACK_ELEMENTS = 1000
12
+
13
+ def self.call(font)
14
+ return [] unless font.has_table?("maxp")
15
+
16
+ maxp = font.table("maxp")
17
+ issues = []
18
+ issues.concat(validate_version(maxp, font))
19
+ issues.concat(validate_num_glyphs(maxp))
20
+ issues.concat(validate_hint_capacity(maxp))
21
+ issues
22
+ end
23
+
24
+ def self.code
25
+ :ot_maxp
26
+ end
27
+
28
+ def self.validate_version(maxp, font)
29
+ version_raw = maxp.version_raw
30
+ is_truetype = font.has_table?("glyf")
31
+ tt_version = 0x00010000 # 1.0 as Fixed 16.16
32
+ cff_version = 0x00005000 # 0.5 as Fixed 16.16
33
+ if is_truetype && version_raw != tt_version
34
+ [issue(severity: :error,
35
+ message: "maxp.version is 0x#{version_raw.to_s(16)} but must " \
36
+ "be 1.0 (0x#{tt_version.to_s(16)}) for TrueType fonts",
37
+ location: "maxp.version")]
38
+ elsif !is_truetype && version_raw != cff_version
39
+ [issue(severity: :warning,
40
+ message: "maxp.version is 0x#{version_raw.to_s(16)} but " \
41
+ "should be 0.5 (0x#{cff_version.to_s(16)}) for " \
42
+ "CFF-based fonts",
43
+ location: "maxp.version")]
44
+ else
45
+ []
46
+ end
47
+ end
48
+ private_class_method :validate_version
49
+
50
+ def self.validate_num_glyphs(maxp)
51
+ n = maxp.num_glyphs.to_i
52
+ return [] if n.between?(1, MAX_GLYPHS)
53
+
54
+ [issue(severity: :error,
55
+ message: "maxp.numGlyphs is #{n} but must be between 1 and " \
56
+ "#{MAX_GLYPHS}",
57
+ location: "maxp.num_glyphs")]
58
+ end
59
+ private_class_method :validate_num_glyphs
60
+
61
+ def self.validate_hint_capacity(maxp)
62
+ return [] unless maxp.version_1_0?
63
+
64
+ issues = []
65
+ zones = maxp.max_zones.to_i
66
+ unless zones.between?(1, 2)
67
+ issues << issue(severity: :warning,
68
+ message: "maxp.maxZones is #{zones} but must be 1 or 2 " \
69
+ "per the OpenType spec",
70
+ location: "maxp.max_zones")
71
+ end
72
+ stack = maxp.max_stack_elements.to_i
73
+ if stack > MAX_STACK_ELEMENTS
74
+ issues << issue(severity: :warning,
75
+ message: "maxp.maxStackElements is #{stack} — " \
76
+ "excessively large values waste interpreter memory",
77
+ location: "maxp.max_stack_elements")
78
+ end
79
+ issues
80
+ end
81
+ private_class_method :validate_hint_capacity
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Audit
5
+ module Checks
6
+ # Validates the 'name' table per the OpenType spec.
7
+ #
8
+ # Covers: required nameIDs, PostScript name character validity,
9
+ # version string format. Glyph-name rules are in GlyphNameCheck.
10
+ #
11
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/name
12
+ class NameTableCheck < Check
13
+ REQUIRED_NAME_IDS = {
14
+ 1 => "Family",
15
+ 2 => "Subfamily",
16
+ 4 => "Full",
17
+ 6 => "PostScript",
18
+ }.freeze
19
+ PS_NAME_PATTERN = /\A[A-Za-z][A-Za-z0-9._-]*\z/
20
+
21
+ def self.call(font)
22
+ return [] unless font.has_table?("name")
23
+
24
+ name = font.table("name")
25
+ issues = []
26
+ issues.concat(validate_required_ids(name))
27
+ issues.concat(validate_ps_name(name))
28
+ issues.concat(validate_version_string(name))
29
+ issues
30
+ end
31
+
32
+ def self.code
33
+ :ot_name
34
+ end
35
+
36
+ def self.validate_required_ids(name)
37
+ REQUIRED_NAME_IDS.each_with_object([]) do |(id, label), issues|
38
+ val = name.english_name(id).to_s
39
+ next unless val.empty?
40
+
41
+ issues << issue(severity: :error,
42
+ message: "Required name ID #{id} (#{label}) is " \
43
+ "missing from the name table",
44
+ location: "name.nameID.#{id}")
45
+ end
46
+ end
47
+ private_class_method :validate_required_ids
48
+
49
+ def self.validate_ps_name(name)
50
+ ps = name.english_name(6).to_s
51
+ return [] if ps.empty? || ps.match?(PS_NAME_PATTERN)
52
+
53
+ [issue(severity: :warning,
54
+ message: "PostScript name '#{ps}' contains characters outside " \
55
+ "the OT spec grammar (must start with a letter, then " \
56
+ "A–Z a–z 0–9 . - _)",
57
+ location: "name.nameID.6")]
58
+ end
59
+ private_class_method :validate_ps_name
60
+
61
+ def self.validate_version_string(name)
62
+ ver = name.english_name(5).to_s
63
+ return [] if ver.empty?
64
+
65
+ unless ver.match?(/\AVersion\s/i)
66
+ return [issue(severity: :info,
67
+ message: "name ID 5 (version) is '#{ver}' but should " \
68
+ "start with 'Version ' per OT spec convention",
69
+ location: "name.nameID.5")]
70
+ end
71
+
72
+ []
73
+ end
74
+ private_class_method :validate_version_string
75
+ end
76
+ end
77
+ end
78
+ end
@@ -3,44 +3,25 @@
3
3
  module Fontisan
4
4
  module Audit
5
5
  module Checks
6
- # Foundational OpenType spec conformance checks. Covers the
7
- # most commonly-violated "MUST" and "SHOULD" rules from the OT
8
- # spec that other check domains don't already cover.
6
+ # Foundational cross-table OpenType spec conformance checks.
7
+ # Per-table rules live in their own check modules
8
+ # ({HeadCheck}, {HheaCheck}, {Os2Check}, {NameTableCheck},
9
+ # {PostCheck}, {KernCheck}) to keep each concern MECE.
9
10
  #
10
- # This check is intentionally a growing foundation new OT spec
11
- # rules are added incrementally as real-world fonts surface them.
12
- # Full OT conformance is a long-running effort that tracks the
13
- # spec's evolution (axis 14 per TODO #03).
14
- #
15
- # Current checks:
11
+ # This check owns the rules that span multiple tables:
16
12
  #
17
13
  # - Required tables for each sfnt flavor (TTF vs CFF vs variable)
18
- # - head.indexToLocFormat consistency with loca table size
19
- # - name table must have at least family (1), subfamily (2),
20
- # full (4), PostScript (6) name IDs
21
- # - OS/2 fsSelection must not use reserved bits
14
+ # - head.indexToLocFormat consistency with loca table byte size
22
15
  # - hhea.numberOfHMetrics must be ≤ maxp.numGlyphs
23
- # - post table version must be valid
24
- # - cmap must have at least one Unicode subtable (already
25
- # covered by CmapCheck — skipped here to avoid duplicate issues)
26
16
  #
27
17
  # @see https://learn.microsoft.com/en-us/typography/opentype/spec/otff
28
18
  class OpenTypeConformanceCheck < Check
29
- REQUIRED_NAME_IDS = {
30
- 1 => "Family",
31
- 2 => "Subfamily",
32
- 4 => "Full",
33
- 6 => "PostScript",
34
- }.freeze
35
-
36
19
  # @param font [SfntFont]
37
20
  # @return [Array<Models::ValidationReport::Issue>]
38
21
  def self.call(font)
39
22
  issues = []
40
23
  issues.concat(validate_required_tables(font))
41
24
  issues.concat(validate_loca_format(font))
42
- issues.concat(validate_name_coverage(font))
43
- issues.concat(validate_os2_fsselection(font))
44
25
  issues.concat(validate_hhea_metrics(font))
45
26
  issues
46
27
  end
@@ -103,41 +84,6 @@ module Fontisan
103
84
  end
104
85
  private_class_method :validate_loca_format
105
86
 
106
- # ---------- name table coverage ----------
107
-
108
- def self.validate_name_coverage(font)
109
- return [] unless font.has_table?("name")
110
-
111
- name = font.table("name")
112
- REQUIRED_NAME_IDS.each_with_object([]) do |(id, label), issues|
113
- val = name.english_name(id).to_s
114
- next unless val.empty?
115
-
116
- issues << issue(severity: :error,
117
- message: "Required name ID #{id} (#{label}) is " \
118
- "missing from the name table",
119
- location: "name.nameID.#{id}")
120
- end
121
- end
122
- private_class_method :validate_name_coverage
123
-
124
- # ---------- OS/2 fsSelection reserved bits ----------
125
-
126
- def self.validate_os2_fsselection(font)
127
- return [] unless font.has_table?("OS/2")
128
-
129
- os2 = font.table("OS/2")
130
- fs = os2.fs_selection.to_i
131
- reserved_mask = 0xFC00 # bits 10-15 are reserved
132
- return [] if (fs & reserved_mask).zero?
133
-
134
- [issue(severity: :warning,
135
- message: "OS/2 fsSelection uses reserved bits " \
136
- "(value 0x#{fs.to_s(16)}, bits 10-15 must be 0)",
137
- location: "os2.fs_selection")]
138
- end
139
- private_class_method :validate_os2_fsselection
140
-
141
87
  # ---------- hhea.numberOfHMetrics ----------
142
88
 
143
89
  def self.validate_hhea_metrics(font)
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Audit
5
+ module Checks
6
+ # Validates the 'OS/2' table per the OpenType spec.
7
+ #
8
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/os2
9
+ class Os2Check < Check
10
+ FS_SELECTION_RESERVED = 0xFC00
11
+
12
+ def self.call(font)
13
+ return [] unless font.has_table?("OS/2")
14
+
15
+ os2 = font.table("OS/2")
16
+ issues = []
17
+ issues.concat(validate_weight_class(os2))
18
+ issues.concat(validate_width_class(os2))
19
+ issues.concat(validate_fs_selection(os2))
20
+ issues.concat(validate_typo_metrics(os2))
21
+ issues.concat(validate_win_metrics(os2))
22
+ issues.concat(validate_panose(os2))
23
+ issues
24
+ end
25
+
26
+ def self.code
27
+ :ot_os2
28
+ end
29
+
30
+ def self.validate_weight_class(os2)
31
+ w = os2.us_weight_class.to_i
32
+ return [] if w.between?(1, 1000)
33
+
34
+ [issue(severity: :error,
35
+ message: "OS/2.usWeightClass is #{w} but must be between " \
36
+ "1 and 1000",
37
+ location: "os2.us_weight_class")]
38
+ end
39
+ private_class_method :validate_weight_class
40
+
41
+ def self.validate_width_class(os2)
42
+ w = os2.us_width_class.to_i
43
+ return [] if w.between?(1, 9)
44
+
45
+ [issue(severity: :error,
46
+ message: "OS/2.usWidthClass is #{w} but must be between 1 " \
47
+ "and 9",
48
+ location: "os2.us_width_class")]
49
+ end
50
+ private_class_method :validate_width_class
51
+
52
+ def self.validate_fs_selection(os2)
53
+ fs = os2.fs_selection.to_i
54
+ return [] if (fs & FS_SELECTION_RESERVED).zero?
55
+
56
+ [issue(severity: :warning,
57
+ message: "OS/2.fsSelection uses reserved bits " \
58
+ "(0x#{fs.to_s(16)}, bits 10-15 must be 0)",
59
+ location: "os2.fs_selection")]
60
+ end
61
+ private_class_method :validate_fs_selection
62
+
63
+ def self.validate_typo_metrics(os2)
64
+ issues = []
65
+ asc = os2.s_typo_ascender.to_i
66
+ desc = os2.s_typo_descender.to_i
67
+ if asc <= 0
68
+ issues << issue(severity: :warning,
69
+ message: "OS/2.sTypoAscender is #{asc} but should " \
70
+ "be positive",
71
+ location: "os2.s_typo_ascender")
72
+ end
73
+ if desc.positive?
74
+ issues << issue(severity: :warning,
75
+ message: "OS/2.sTypoDescender is #{desc} but should " \
76
+ "be ≤ 0",
77
+ location: "os2.s_typo_descender")
78
+ end
79
+ issues
80
+ end
81
+ private_class_method :validate_typo_metrics
82
+
83
+ def self.validate_win_metrics(os2)
84
+ issues = []
85
+ if os2.us_win_ascent.to_i <= 0
86
+ issues << issue(severity: :warning,
87
+ message: "OS/2.usWinAscent is #{os2.us_win_ascent} " \
88
+ "but should be positive",
89
+ location: "os2.us_win_ascent")
90
+ end
91
+ if os2.us_win_descent.to_i <= 0
92
+ issues << issue(severity: :warning,
93
+ message: "OS/2.usWinDescent is #{os2.us_win_descent} " \
94
+ "but should be positive",
95
+ location: "os2.us_win_descent")
96
+ end
97
+ issues
98
+ end
99
+ private_class_method :validate_win_metrics
100
+
101
+ def self.validate_panose(os2)
102
+ panose = os2.panose
103
+ return [] unless panose
104
+
105
+ all_zero = panose.to_a.all?(&:zero?)
106
+ return [] unless all_zero
107
+
108
+ [issue(severity: :info,
109
+ message: "OS/2.panose is all zeros — PANOSE classification " \
110
+ "helps font matching in some applications",
111
+ location: "os2.panose")]
112
+ end
113
+ private_class_method :validate_panose
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Audit
5
+ module Checks
6
+ # Validates the 'post' table per the OpenType spec.
7
+ #
8
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/post
9
+ class PostCheck < Check
10
+ VALID_VERSIONS = [1.0, 2.0, 2.5, 3.0, 4.0].freeze
11
+
12
+ def self.call(font)
13
+ return [] unless font.has_table?("post")
14
+
15
+ post = font.table("post")
16
+ head = font.has_table?("head") ? font.table("head") : nil
17
+ issues = []
18
+ issues.concat(validate_version(post))
19
+ issues.concat(validate_italic_angle(post, head))
20
+ issues.concat(validate_is_fixed_pitch(post))
21
+ issues
22
+ end
23
+
24
+ def self.code
25
+ :ot_post
26
+ end
27
+
28
+ def self.validate_version(post)
29
+ version = post.version.to_f
30
+ return [] if VALID_VERSIONS.include?(version)
31
+
32
+ [issue(severity: :error,
33
+ message: "post.version is #{version} but must be one of " \
34
+ "#{VALID_VERSIONS.join(', ')}",
35
+ location: "post.version")]
36
+ end
37
+ private_class_method :validate_version
38
+
39
+ def self.validate_italic_angle(post, head)
40
+ angle = post.italic_angle.to_f
41
+ issues = []
42
+ if head&.mac_style
43
+ is_italic = (head.mac_style.to_i & 0x02).positive?
44
+ if is_italic && angle >= 0
45
+ issues << issue(severity: :warning,
46
+ message: "post.italicAngle is #{angle} but " \
47
+ "head.macStyle indicates italic " \
48
+ "(angle should be negative)",
49
+ location: "post.italic_angle")
50
+ elsif !is_italic && angle != 0
51
+ issues << issue(severity: :info,
52
+ message: "post.italicAngle is #{angle} but " \
53
+ "head.macStyle does not indicate italic " \
54
+ "(angle should be 0)",
55
+ location: "post.italic_angle")
56
+ end
57
+ end
58
+ issues
59
+ end
60
+ private_class_method :validate_italic_angle
61
+
62
+ def self.validate_is_fixed_pitch(post)
63
+ val = post.is_fixed_pitch.to_i
64
+ return [] if [0, 1].include?(val)
65
+
66
+ [issue(severity: :warning,
67
+ message: "post.isFixedPitch is #{val} but must be 0 " \
68
+ "(proportional) or 1 (monospace)",
69
+ location: "post.is_fixed_pitch")]
70
+ end
71
+ private_class_method :validate_is_fixed_pitch
72
+ end
73
+ end
74
+ end
75
+ end
@@ -25,6 +25,16 @@ module Fontisan
25
25
  "fontisan/audit/checks/format_round_trip_check"
26
26
  autoload :OpenTypeConformanceCheck,
27
27
  "fontisan/audit/checks/opentype_conformance_check"
28
+ autoload :HeadCheck, "fontisan/audit/checks/head_check"
29
+ autoload :HheaCheck, "fontisan/audit/checks/hhea_check"
30
+ autoload :MaxpCheck, "fontisan/audit/checks/maxp_check"
31
+ autoload :Os2Check, "fontisan/audit/checks/os2_check"
32
+ autoload :NameTableCheck, "fontisan/audit/checks/name_table_check"
33
+ autoload :PostCheck, "fontisan/audit/checks/post_check"
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"
28
38
  end
29
39
  end
30
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
- report.validation_issues = run_validation_checks(face)
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
@@ -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"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.4.36"
4
+ VERSION = "0.4.38"
5
5
  end
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.36
4
+ version: 0.4.38
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -297,13 +297,23 @@ 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
306
+ - lib/fontisan/audit/checks/head_check.rb
307
+ - lib/fontisan/audit/checks/hhea_check.rb
304
308
  - lib/fontisan/audit/checks/hinting_check.rb
309
+ - lib/fontisan/audit/checks/kern_check.rb
310
+ - lib/fontisan/audit/checks/layout_table_check.rb
311
+ - lib/fontisan/audit/checks/maxp_check.rb
312
+ - lib/fontisan/audit/checks/name_table_check.rb
305
313
  - lib/fontisan/audit/checks/opentype_conformance_check.rb
314
+ - lib/fontisan/audit/checks/os2_check.rb
306
315
  - lib/fontisan/audit/checks/ots_compatibility_check.rb
316
+ - lib/fontisan/audit/checks/post_check.rb
307
317
  - lib/fontisan/audit/checks/table_directory_check.rb
308
318
  - lib/fontisan/audit/checks/variable_font_check.rb
309
319
  - lib/fontisan/audit/checks/woff2_validation_check.rb