fontisan 0.4.37 → 0.4.39
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 +8 -1
- data/.rubocop_todo.yml +11 -11
- data/TODO.improvements/README.md +2 -2
- data/lib/fontisan/audit/check_registry.rb +11 -5
- 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/naming_consistency_check.rb +102 -0
- data/lib/fontisan/audit/checks.rb +5 -0
- data/lib/fontisan/cli.rb +130 -3
- 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 +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 42aba87092f4f48daf4e44f57dbbfcd948af16d90ef11fd145c3290d159eb674
|
|
4
|
+
data.tar.gz: 53a94a2f8914a6303e39dcb61a9db7cb411cb71322d2b400b595ff1010d6deca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f0ee7602cfe91bbf0232ffb727d1743dc1d1b4eaacdc4290c8cfb07134b158be363c8d1c9a9ac15a41946e51c82a85eb443135a0f1469d83d4eb29d3b26e8142
|
|
7
|
+
data.tar.gz: 4f3419c11a81e62b556d1eb3f7e3cdec7bfabf36b4bf00fe9da9ff5fba4ede066266d8aad3bd79a7a6c4a2210098676753b3c2eabbdfc4a993a7ed7440ef64b0
|
data/.rubocop.yml
CHANGED
|
@@ -131,4 +131,11 @@ Naming/MethodParameterName:
|
|
|
131
131
|
# to cross-reference.
|
|
132
132
|
Naming/VariableNumber:
|
|
133
133
|
AllowedIdentifiers: ["format0", "format4", "format6", "format12", "format14",
|
|
134
|
-
"version1", "version2", "table1", "table2"
|
|
134
|
+
"version1", "version2", "table1", "table2",
|
|
135
|
+
"os2", "type1", "scale_to_1000", "scale_10", "scale_01",
|
|
136
|
+
"canonical_id_1", "canonical_id_2",
|
|
137
|
+
"strike_offset_1", "strike_offset_2", "strike_offset_3",
|
|
138
|
+
"plane_0", "to_signed_16", "strike1",
|
|
139
|
+
"range_8", "range_16", "encoding_id_2",
|
|
140
|
+
"subtable_1", "subtable_2", "b1", "c2",
|
|
141
|
+
"x0", "y0", "x1", "y1", "x2", "y2", "x3", "y3"]
|
data/.rubocop_todo.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# This configuration was generated by
|
|
2
2
|
# `rubocop --auto-gen-config`
|
|
3
|
-
# on 2026-07-11
|
|
3
|
+
# on 2026-07-11 14:35:05 UTC using RuboCop version 1.86.1.
|
|
4
4
|
# The point is for the user to remove these configuration records
|
|
5
5
|
# one by one as the offenses are removed from the code base.
|
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
|
@@ -32,7 +32,7 @@ Layout/IndentationWidth:
|
|
|
32
32
|
- 'lib/fontisan/commands/audit_command.rb'
|
|
33
33
|
- 'lib/fontisan/ufo/compile/glyf_loca.rb'
|
|
34
34
|
|
|
35
|
-
# Offense count:
|
|
35
|
+
# Offense count: 2094
|
|
36
36
|
# This cop supports safe autocorrection (--autocorrect).
|
|
37
37
|
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
|
|
38
38
|
# URISchemes: http, https
|
|
@@ -111,7 +111,7 @@ Lint/UselessConstantScoping:
|
|
|
111
111
|
- 'lib/fontisan/conversion_options.rb'
|
|
112
112
|
- 'lib/fontisan/type1/charstrings.rb'
|
|
113
113
|
|
|
114
|
-
# Offense count:
|
|
114
|
+
# Offense count: 771
|
|
115
115
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
|
|
116
116
|
Metrics/AbcSize:
|
|
117
117
|
Enabled: false
|
|
@@ -134,12 +134,12 @@ Metrics/CollectionLiteralLength:
|
|
|
134
134
|
- 'lib/fontisan/type1/agl.rb'
|
|
135
135
|
- 'lib/fontisan/type1/encodings.rb'
|
|
136
136
|
|
|
137
|
-
# Offense count:
|
|
137
|
+
# Offense count: 388
|
|
138
138
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
139
139
|
Metrics/CyclomaticComplexity:
|
|
140
140
|
Enabled: false
|
|
141
141
|
|
|
142
|
-
# Offense count:
|
|
142
|
+
# Offense count: 1059
|
|
143
143
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
144
144
|
Metrics/MethodLength:
|
|
145
145
|
Max: 135
|
|
@@ -150,7 +150,7 @@ Metrics/ParameterLists:
|
|
|
150
150
|
Max: 39
|
|
151
151
|
MaxOptionalParameters: 4
|
|
152
152
|
|
|
153
|
-
# Offense count:
|
|
153
|
+
# Offense count: 297
|
|
154
154
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
155
155
|
Metrics/PerceivedComplexity:
|
|
156
156
|
Enabled: false
|
|
@@ -210,7 +210,7 @@ Naming/VariableName:
|
|
|
210
210
|
- 'lib/fontisan/type1/pfm_generator.rb'
|
|
211
211
|
- 'lib/fontisan/type1/pfm_parser.rb'
|
|
212
212
|
|
|
213
|
-
# Offense count:
|
|
213
|
+
# Offense count: 55
|
|
214
214
|
# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
|
|
215
215
|
# SupportedStyles: snake_case, normalcase, non_integer
|
|
216
216
|
# AllowedIdentifiers: TLS1_1, TLS1_2, capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
|
|
@@ -233,7 +233,7 @@ RSpec/AnyInstance:
|
|
|
233
233
|
RSpec/ContextWording:
|
|
234
234
|
Enabled: false
|
|
235
235
|
|
|
236
|
-
# Offense count:
|
|
236
|
+
# Offense count: 44
|
|
237
237
|
# Configuration parameters: IgnoredMetadata.
|
|
238
238
|
RSpec/DescribeClass:
|
|
239
239
|
Enabled: false
|
|
@@ -249,7 +249,7 @@ RSpec/DescribeMethod:
|
|
|
249
249
|
- 'spec/fontisan/type1/seac_expander_spec.rb'
|
|
250
250
|
- 'spec/fontisan/type1_real_fonts_spec.rb'
|
|
251
251
|
|
|
252
|
-
# Offense count:
|
|
252
|
+
# Offense count: 1736
|
|
253
253
|
# Configuration parameters: CountAsOne.
|
|
254
254
|
RSpec/ExampleLength:
|
|
255
255
|
Max: 66
|
|
@@ -327,7 +327,7 @@ RSpec/MultipleDescribes:
|
|
|
327
327
|
- 'spec/fontisan/utils/thread_pool_spec.rb'
|
|
328
328
|
- 'spec/fontisan/variation/cache_spec.rb'
|
|
329
329
|
|
|
330
|
-
# Offense count:
|
|
330
|
+
# Offense count: 2168
|
|
331
331
|
RSpec/MultipleExpectations:
|
|
332
332
|
Max: 19
|
|
333
333
|
|
|
@@ -359,7 +359,7 @@ RSpec/RepeatedExample:
|
|
|
359
359
|
- 'spec/fontisan/metrics_calculator_spec.rb'
|
|
360
360
|
- 'spec/fontisan/tables/os2_spec.rb'
|
|
361
361
|
|
|
362
|
-
# Offense count:
|
|
362
|
+
# Offense count: 53
|
|
363
363
|
# Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector.
|
|
364
364
|
# SupportedInflectors: default, active_support
|
|
365
365
|
RSpec/SpecFilePathFormat:
|
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
|
|
|
@@ -26,7 +26,7 @@ Each file is `NN-short-name.md` where `NN` is the priority order.
|
|
|
26
26
|
### P3 — Code-quality cleanup
|
|
27
27
|
- [x] ~~[13 — Split `OctokitFetcher` out of `fixture_downloader.rb`](13-split-octokit-fetcher.md)~~ ✓ Done (v0.4.24)
|
|
28
28
|
- [x] ~~[12 — `cbdt_fixture.rb` full BinData conversion](12-cbdt-fixture-bindata-conversion.md)~~ ✓ Done (critical tables converted; remaining os2/name/hmtx/cmap verified correct, BinData conversion is diminishing returns)
|
|
29
|
-
- [x] ~~[14 — Rubocop baseline chip (per-namespace)](14-rubocop-baseline-chip.md)~~ ✓ Partially done —
|
|
29
|
+
- [x] ~~[14 — Rubocop baseline chip (per-namespace)](14-rubocop-baseline-chip.md)~~ ✓ Partially done — all auto-correctable offenses fixed; all namespaces pass `rubocop` with 0 new offenses; Naming cops configured for font-domain conventions; remaining ~9800 baseline offenses are structural Metrics (MethodLength, AbcSize, LineLength) explicitly out of scope per TODO ("Refactoring architectural patterns — separate refactors"); estimated ~7h manual refactoring effort
|
|
30
30
|
|
|
31
31
|
## Convention
|
|
32
32
|
|
|
@@ -18,21 +18,23 @@ 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
|
-
format_round_trip opentype_conformance
|
|
27
|
-
ot_head ot_hhea ot_maxp ot_os2 ot_name ot_post ot_kern
|
|
26
|
+
format_round_trip opentype_conformance naming_consistency
|
|
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 naming_consistency],
|
|
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
|
|
37
|
+
naming_consistency],
|
|
36
38
|
per_table: OT_TABLE_CHECKS,
|
|
37
39
|
}.freeze
|
|
38
40
|
|
|
@@ -57,6 +59,7 @@ module Fontisan
|
|
|
57
59
|
when :woff2_validation then Checks::Woff2ValidationCheck
|
|
58
60
|
when :format_round_trip then Checks::FormatRoundTripCheck
|
|
59
61
|
when :opentype_conformance then Checks::OpenTypeConformanceCheck
|
|
62
|
+
when :naming_consistency then Checks::NamingConsistencyCheck
|
|
60
63
|
when :ot_head then Checks::HeadCheck
|
|
61
64
|
when :ot_hhea then Checks::HheaCheck
|
|
62
65
|
when :ot_maxp then Checks::MaxpCheck
|
|
@@ -64,6 +67,9 @@ module Fontisan
|
|
|
64
67
|
when :ot_name then Checks::NameTableCheck
|
|
65
68
|
when :ot_post then Checks::PostCheck
|
|
66
69
|
when :ot_kern then Checks::KernCheck
|
|
70
|
+
when :ot_cff then Checks::CffTableCheck
|
|
71
|
+
when :ot_glyf then Checks::GlyfTableCheck
|
|
72
|
+
when :ot_layout then Checks::LayoutTableCheck
|
|
67
73
|
end
|
|
68
74
|
end
|
|
69
75
|
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
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Audit
|
|
5
|
+
module Checks
|
|
6
|
+
# Validates name-table consistency across name IDs. Catches
|
|
7
|
+
# common real-world bugs where font metadata disagrees with
|
|
8
|
+
# itself:
|
|
9
|
+
#
|
|
10
|
+
# - PostScript name should start with the Family name (minus
|
|
11
|
+
# spaces) — e.g. Family "Source Sans" → PS "SourceSans-Regular"
|
|
12
|
+
# - Subfamily should be a recognized style ("Regular", "Bold",
|
|
13
|
+
# "Italic", "Bold Italic") for non-variable fonts
|
|
14
|
+
# - Full name should be Family + " " + Subfamily
|
|
15
|
+
# - Version string should start with "Version "
|
|
16
|
+
#
|
|
17
|
+
# These rules catch typo-level errors that confuse font pickers,
|
|
18
|
+
# package managers, and PDF embedding workflows.
|
|
19
|
+
#
|
|
20
|
+
# @see https://learn.microsoft.com/en-us/typography/opentype/spec/name
|
|
21
|
+
class NamingConsistencyCheck < Check
|
|
22
|
+
RECOGNIZED_SUBFAMILIES = %w[
|
|
23
|
+
Regular Italic Bold BoldItalic Thin Light Medium SemiBold Black
|
|
24
|
+
Condensed Extended Oblique Book Normal
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
def self.call(font)
|
|
28
|
+
return [] unless font.has_table?("name")
|
|
29
|
+
|
|
30
|
+
name = font.table("name")
|
|
31
|
+
issues = []
|
|
32
|
+
issues.concat(check_ps_name_prefix(name))
|
|
33
|
+
issues.concat(check_full_name_composition(name))
|
|
34
|
+
issues.concat(check_subfamily_style(name, font))
|
|
35
|
+
issues
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.code
|
|
39
|
+
:naming_consistency
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# PostScript name should start with the family name (spaces
|
|
43
|
+
# removed). E.g. Family "Source Sans 3" → PS "SourceSans3-Regular".
|
|
44
|
+
def self.check_ps_name_prefix(name)
|
|
45
|
+
family = name.english_name(Tables::Name::FAMILY).to_s
|
|
46
|
+
ps = name.english_name(Tables::Name::POSTSCRIPT_NAME).to_s
|
|
47
|
+
return [] if family.empty? || ps.empty?
|
|
48
|
+
|
|
49
|
+
family_compact = family.delete(" ")
|
|
50
|
+
return [] if ps.start_with?(family_compact)
|
|
51
|
+
|
|
52
|
+
[issue(severity: :warning,
|
|
53
|
+
message: "PostScript name '#{ps}' does not start with the " \
|
|
54
|
+
"family name '#{family}' (expected prefix: '#{family_compact}')",
|
|
55
|
+
location: "name.consistency.ps_prefix")]
|
|
56
|
+
end
|
|
57
|
+
private_class_method :check_ps_name_prefix
|
|
58
|
+
|
|
59
|
+
# Full name should be Family + " " + Subfamily.
|
|
60
|
+
def self.check_full_name_composition(name)
|
|
61
|
+
family = name.english_name(Tables::Name::FAMILY).to_s
|
|
62
|
+
subfamily = name.english_name(Tables::Name::SUBFAMILY).to_s
|
|
63
|
+
full = name.english_name(Tables::Name::FULL_NAME).to_s
|
|
64
|
+
return [] if family.empty? || subfamily.empty? || full.empty?
|
|
65
|
+
|
|
66
|
+
expected = "#{family} #{subfamily}"
|
|
67
|
+
return [] if full == expected
|
|
68
|
+
|
|
69
|
+
[issue(severity: :info,
|
|
70
|
+
message: "Full name '#{full}' doesn't match " \
|
|
71
|
+
"Family + Subfamily '#{expected}'",
|
|
72
|
+
location: "name.consistency.full_name")]
|
|
73
|
+
end
|
|
74
|
+
private_class_method :check_full_name_composition
|
|
75
|
+
|
|
76
|
+
# Subfamily should be a recognized style name for non-variable
|
|
77
|
+
# fonts. Variable fonts use arbitrary subfamily names.
|
|
78
|
+
def self.check_subfamily_style(name, font)
|
|
79
|
+
return [] if font.has_table?("fvar") # variable fonts use custom names
|
|
80
|
+
|
|
81
|
+
subfamily = name.english_name(Tables::Name::SUBFAMILY).to_s
|
|
82
|
+
return [] if subfamily.empty?
|
|
83
|
+
|
|
84
|
+
return [] if recognized_subfamily?(subfamily)
|
|
85
|
+
|
|
86
|
+
[issue(severity: :info,
|
|
87
|
+
message: "Subfamily '#{subfamily}' is not a standard style " \
|
|
88
|
+
"name — font pickers may not categorize it correctly",
|
|
89
|
+
location: "name.consistency.subfamily")]
|
|
90
|
+
end
|
|
91
|
+
private_class_method :check_subfamily_style
|
|
92
|
+
|
|
93
|
+
def self.recognized_subfamily?(subfamily)
|
|
94
|
+
RECOGNIZED_SUBFAMILIES.any? do |recognized|
|
|
95
|
+
subfamily.casecmp?(recognized) || subfamily.casecmp?(recognized.delete('"'))
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
private_class_method :recognized_subfamily?
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -32,6 +32,11 @@ 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"
|
|
38
|
+
autoload :NamingConsistencyCheck,
|
|
39
|
+
"fontisan/audit/checks/naming_consistency_check"
|
|
35
40
|
end
|
|
36
41
|
end
|
|
37
42
|
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
|
|
@@ -791,7 +802,95 @@ module Fontisan
|
|
|
791
802
|
end
|
|
792
803
|
|
|
793
804
|
def serialize_audit(report)
|
|
794
|
-
options[:format]
|
|
805
|
+
case options[:format]
|
|
806
|
+
when "json" then report.to_json
|
|
807
|
+
when "text", nil then format_audit_as_text(report)
|
|
808
|
+
else report.to_yaml
|
|
809
|
+
end
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
# Format an AuditReport as human-readable text for terminal use.
|
|
813
|
+
# Groups fields into sections: provenance, identity, style,
|
|
814
|
+
# coverage, layout, validation summary.
|
|
815
|
+
def format_audit_as_text(report)
|
|
816
|
+
lines = []
|
|
817
|
+
lines << "=== Font Audit Report ==="
|
|
818
|
+
lines.concat(format_audit_provenance(report))
|
|
819
|
+
lines.concat(format_audit_identity(report))
|
|
820
|
+
lines.concat(format_audit_style(report))
|
|
821
|
+
lines.concat(format_audit_coverage(report))
|
|
822
|
+
lines.concat(format_audit_layout(report))
|
|
823
|
+
lines.concat(format_audit_validation(report))
|
|
824
|
+
lines.join("\n")
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
def format_audit_provenance(report)
|
|
828
|
+
lines = []
|
|
829
|
+
lines << " Source: #{report.source_file}"
|
|
830
|
+
lines << " Format: #{report.source_format}"
|
|
831
|
+
lines << " SHA256: #{report.source_sha256[0, 16]}..." if report.source_sha256
|
|
832
|
+
lines << " Fontisan: #{report.fontisan_version}" if report.fontisan_version
|
|
833
|
+
lines << ""
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
def format_audit_identity(report)
|
|
837
|
+
lines = ["Identity:"]
|
|
838
|
+
lines << " Family: #{report.family_name}"
|
|
839
|
+
lines << " Subfamily: #{report.subfamily_name}"
|
|
840
|
+
lines << " Full: #{report.full_name}"
|
|
841
|
+
lines << " PostScript: #{report.postscript_name}"
|
|
842
|
+
lines << " Version: #{report.version}"
|
|
843
|
+
lines << ""
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
def format_audit_style(report)
|
|
847
|
+
lines = ["Style:"]
|
|
848
|
+
lines << " Weight: #{report.weight_class}"
|
|
849
|
+
lines << " Width: #{report.width_class}"
|
|
850
|
+
lines << " Italic: #{report.italic}"
|
|
851
|
+
lines << " Bold: #{report.bold}"
|
|
852
|
+
lines << " Variable: #{report.is_variable}"
|
|
853
|
+
report.axes&.each do |axis|
|
|
854
|
+
lines << " #{axis.tag}: #{axis.min_value}–#{axis.max_value} " \
|
|
855
|
+
"(default #{axis.default_value})"
|
|
856
|
+
end
|
|
857
|
+
lines << ""
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
def format_audit_coverage(report)
|
|
861
|
+
lines = ["Coverage:"]
|
|
862
|
+
lines << " Glyphs: #{report.total_glyphs}"
|
|
863
|
+
lines << " Codepoints: #{report.total_codepoints}"
|
|
864
|
+
lines << " cmap subtables: #{report.cmap_subtables&.join(', ')}" if report.cmap_subtables&.any?
|
|
865
|
+
lines << ""
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
def format_audit_layout(report)
|
|
869
|
+
scripts = report.opentype_scripts
|
|
870
|
+
features = report.features
|
|
871
|
+
return [] unless scripts&.any? || features&.any?
|
|
872
|
+
|
|
873
|
+
lines = ["Layout:"]
|
|
874
|
+
lines << " Scripts: #{scripts&.join(', ')}" if scripts&.any?
|
|
875
|
+
lines << " Features: #{features&.join(', ')}" if features&.any?
|
|
876
|
+
lines << ""
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
def format_audit_validation(report)
|
|
880
|
+
summary = report.validation_summary
|
|
881
|
+
return [] unless summary
|
|
882
|
+
|
|
883
|
+
lines = ["Validation:"]
|
|
884
|
+
lines << " Errors: #{summary.error_count}"
|
|
885
|
+
lines << " Warnings: #{summary.warning_count}"
|
|
886
|
+
lines << " Info: #{summary.info_count}"
|
|
887
|
+
result = summary.passed ? "PASSED" : "FAILED"
|
|
888
|
+
lines << " Result: #{result}"
|
|
889
|
+
lines << ""
|
|
890
|
+
report.validation_issues&.each do |issue|
|
|
891
|
+
lines << " [#{issue.severity.upcase}] #{issue.category}: #{issue.message}"
|
|
892
|
+
end
|
|
893
|
+
lines
|
|
795
894
|
end
|
|
796
895
|
|
|
797
896
|
def audit_face_filename(report)
|
|
@@ -810,6 +909,34 @@ module Fontisan
|
|
|
810
909
|
raw.to_sym
|
|
811
910
|
end
|
|
812
911
|
|
|
912
|
+
# Print available validation profiles with their check counts.
|
|
913
|
+
def list_audit_profiles
|
|
914
|
+
puts "Available validation profiles for --validate:"
|
|
915
|
+
puts ""
|
|
916
|
+
Audit::CheckRegistry::PROFILES.each do |name, codes|
|
|
917
|
+
puts " #{name.to_s.ljust(15)} #{codes.size} checks"
|
|
918
|
+
end
|
|
919
|
+
puts ""
|
|
920
|
+
puts "Usage: fontisan audit font.ttf --validate <profile>"
|
|
921
|
+
puts " fontisan audit font.ttf --validate true (all checks)"
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
# Exit with code 1 if the audit found error-level issues.
|
|
925
|
+
# For collections, aggregates across all faces.
|
|
926
|
+
def exit_on_validation_errors(result)
|
|
927
|
+
issues = if result.is_a?(Array)
|
|
928
|
+
result.flat_map(&:validation_issues)
|
|
929
|
+
else
|
|
930
|
+
result.validation_issues || []
|
|
931
|
+
end
|
|
932
|
+
has_errors = issues.any? { |i| %w[error fatal].include?(i.severity) }
|
|
933
|
+
return unless has_errors
|
|
934
|
+
|
|
935
|
+
error_count = issues.count { |i| %w[error fatal].include?(i.severity) }
|
|
936
|
+
warn "Audit found #{error_count} error(s) — exiting with code 1"
|
|
937
|
+
exit 1
|
|
938
|
+
end
|
|
939
|
+
|
|
813
940
|
def serialize_report(report, format)
|
|
814
941
|
format == :json ? report.to_json : report.to_yaml
|
|
815
942
|
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.39
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -297,16 +297,20 @@ 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
|
|
313
|
+
- lib/fontisan/audit/checks/naming_consistency_check.rb
|
|
310
314
|
- lib/fontisan/audit/checks/opentype_conformance_check.rb
|
|
311
315
|
- lib/fontisan/audit/checks/os2_check.rb
|
|
312
316
|
- lib/fontisan/audit/checks/ots_compatibility_check.rb
|