fontisan 0.4.38 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aebdd37610f8ce36210375f348bd5a0f1aea12d0a12c951005d018e64cac290c
4
- data.tar.gz: 33e81566249885ae646e91e727abedff605619e6deb30f742fcf1dff570cd641
3
+ metadata.gz: 42aba87092f4f48daf4e44f57dbbfcd948af16d90ef11fd145c3290d159eb674
4
+ data.tar.gz: 53a94a2f8914a6303e39dcb61a9db7cb411cb71322d2b400b595ff1010d6deca
5
5
  SHA512:
6
- metadata.gz: 8e43a54dc2421202b7d780e10e37fb4b363c835d261257677728dd99602f6df0190414c1ed59c0d5c331e8b977d75326961f05b5cea2f7c32431c9cee39c094b
7
- data.tar.gz: cce7fd930ab3d5da138c0f4756468b0494f21b7dcf0b515df5e38498770493a399fa10d862c5e2948efc35dfcae84c8a937ad5d8209ed1eddd4a033491934c37
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 08:52:22 UTC using RuboCop version 1.86.1.
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: 2053
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: 766
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: 383
137
+ # Offense count: 388
138
138
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
139
139
  Metrics/CyclomaticComplexity:
140
140
  Enabled: false
141
141
 
142
- # Offense count: 1045
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: 294
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: 124
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: 42
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: 1729
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: 2153
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: 51
362
+ # Offense count: 53
363
363
  # Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector.
364
364
  # SupportedInflectors: default, active_support
365
365
  RSpec/SpecFilePathFormat:
@@ -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 — baseline reduced from 619 to 428 lines; all namespaces pass `rubocop` with 0 offenses; remaining baseline offenses are non-autocorrectable manual fixes (~7h total per TODO estimate)
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
 
@@ -23,17 +23,18 @@ module Fontisan
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
26
+ format_round_trip opentype_conformance naming_consistency
27
27
  ot_head ot_hhea ot_maxp ot_os2 ot_name ot_post ot_kern
28
28
  ot_cff ot_glyf ot_layout],
29
29
  structural: %i[table_directory collection_integrity opentype_conformance],
30
30
  ots: %i[ots_compatibility],
31
- layout: %i[glyph_names cmap ot_layout],
31
+ layout: %i[glyph_names cmap ot_layout naming_consistency],
32
32
  variable: %i[variable_font],
33
33
  hinting: %i[hinting],
34
34
  web: %i[ots_compatibility woff2_validation],
35
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],
36
+ ot_name ot_post ot_kern ot_cff ot_glyf ot_layout
37
+ naming_consistency],
37
38
  per_table: OT_TABLE_CHECKS,
38
39
  }.freeze
39
40
 
@@ -58,6 +59,7 @@ module Fontisan
58
59
  when :woff2_validation then Checks::Woff2ValidationCheck
59
60
  when :format_round_trip then Checks::FormatRoundTripCheck
60
61
  when :opentype_conformance then Checks::OpenTypeConformanceCheck
62
+ when :naming_consistency then Checks::NamingConsistencyCheck
61
63
  when :ot_head then Checks::HeadCheck
62
64
  when :ot_hhea then Checks::HheaCheck
63
65
  when :ot_maxp then Checks::MaxpCheck
@@ -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
@@ -35,6 +35,8 @@ module Fontisan
35
35
  autoload :CffTableCheck, "fontisan/audit/checks/cff_table_check"
36
36
  autoload :GlyfTableCheck, "fontisan/audit/checks/glyf_table_check"
37
37
  autoload :LayoutTableCheck, "fontisan/audit/checks/layout_table_check"
38
+ autoload :NamingConsistencyCheck,
39
+ "fontisan/audit/checks/naming_consistency_check"
38
40
  end
39
41
  end
40
42
  end
data/lib/fontisan/cli.rb CHANGED
@@ -802,7 +802,95 @@ module Fontisan
802
802
  end
803
803
 
804
804
  def serialize_audit(report)
805
- options[:format] == "json" ? report.to_json : report.to_yaml
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
806
894
  end
807
895
 
808
896
  def audit_face_filename(report)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.4.38"
4
+ VERSION = "0.4.39"
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.38
4
+ version: 0.4.39
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -310,6 +310,7 @@ files:
310
310
  - lib/fontisan/audit/checks/layout_table_check.rb
311
311
  - lib/fontisan/audit/checks/maxp_check.rb
312
312
  - lib/fontisan/audit/checks/name_table_check.rb
313
+ - lib/fontisan/audit/checks/naming_consistency_check.rb
313
314
  - lib/fontisan/audit/checks/opentype_conformance_check.rb
314
315
  - lib/fontisan/audit/checks/os2_check.rb
315
316
  - lib/fontisan/audit/checks/ots_compatibility_check.rb