fontisan 0.2.22 → 0.3.0

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.
Files changed (138) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/.rubocop_todo.yml +93 -17
  4. data/CHANGELOG.md +12 -2
  5. data/README.adoc +6 -210
  6. data/fontisan.gemspec +48 -0
  7. data/lib/fontisan/cldr/unicode_set_parser.rb +23 -6
  8. data/lib/fontisan/cldr/version_resolver.rb +1 -1
  9. data/lib/fontisan/cli.rb +4 -168
  10. data/lib/fontisan/commands.rb +0 -3
  11. data/lib/fontisan/formatters/text_formatter.rb +0 -6
  12. data/lib/fontisan/formatters.rb +0 -3
  13. data/lib/fontisan/hints.rb +6 -3
  14. data/lib/fontisan/models.rb +4 -4
  15. data/lib/fontisan/pipeline/strategies.rb +4 -2
  16. data/lib/fontisan/pipeline.rb +2 -1
  17. data/lib/fontisan/stitcher/selector/codepoints.rb +29 -0
  18. data/lib/fontisan/stitcher/selector/gid.rb +25 -0
  19. data/lib/fontisan/stitcher/selector/range.rb +30 -0
  20. data/lib/fontisan/stitcher/selector.rb +26 -0
  21. data/lib/fontisan/stitcher/source.rb +97 -0
  22. data/lib/fontisan/stitcher.rb +182 -0
  23. data/lib/fontisan/stitcher_cli.rb +69 -0
  24. data/lib/fontisan/tables/cff.rb +2 -1
  25. data/lib/fontisan/tables.rb +2 -1
  26. data/lib/fontisan/ufo/anchor.rb +17 -0
  27. data/lib/fontisan/ufo/cli.rb +85 -0
  28. data/lib/fontisan/ufo/compile/base_compiler.rb +81 -0
  29. data/lib/fontisan/ufo/compile/cff.rb +224 -0
  30. data/lib/fontisan/ufo/compile/cmap.rb +129 -0
  31. data/lib/fontisan/ufo/compile/filters/cubic_to_quadratic.rb +174 -0
  32. data/lib/fontisan/ufo/compile/filters/decompose_components.rb +33 -0
  33. data/lib/fontisan/ufo/compile/filters/flatten_components.rb +22 -0
  34. data/lib/fontisan/ufo/compile/filters/reverse_contour_direction.rb +27 -0
  35. data/lib/fontisan/ufo/compile/filters.rb +57 -0
  36. data/lib/fontisan/ufo/compile/glyf_loca.rb +145 -0
  37. data/lib/fontisan/ufo/compile/head.rb +98 -0
  38. data/lib/fontisan/ufo/compile/hhea.rb +36 -0
  39. data/lib/fontisan/ufo/compile/hmtx.rb +27 -0
  40. data/lib/fontisan/ufo/compile/maxp.rb +57 -0
  41. data/lib/fontisan/ufo/compile/name.rb +79 -0
  42. data/lib/fontisan/ufo/compile/os2.rb +81 -0
  43. data/lib/fontisan/ufo/compile/otf_compiler.rb +43 -0
  44. data/lib/fontisan/ufo/compile/post.rb +32 -0
  45. data/lib/fontisan/ufo/compile/ttf_compiler.rb +69 -0
  46. data/lib/fontisan/ufo/compile.rb +48 -0
  47. data/lib/fontisan/ufo/component.rb +18 -0
  48. data/lib/fontisan/ufo/contour.rb +29 -0
  49. data/lib/fontisan/ufo/convert/from_bin_data.rb +246 -0
  50. data/lib/fontisan/ufo/convert.rb +18 -0
  51. data/lib/fontisan/ufo/data_set.rb +21 -0
  52. data/lib/fontisan/ufo/features.rb +17 -0
  53. data/lib/fontisan/ufo/font.rb +61 -0
  54. data/lib/fontisan/ufo/glyph.rb +421 -0
  55. data/lib/fontisan/ufo/guideline.rb +19 -0
  56. data/lib/fontisan/ufo/image.rb +16 -0
  57. data/lib/fontisan/ufo/image_set.rb +19 -0
  58. data/lib/fontisan/ufo/info.rb +79 -0
  59. data/lib/fontisan/ufo/kerning.rb +32 -0
  60. data/lib/fontisan/ufo/layer.rb +38 -0
  61. data/lib/fontisan/ufo/layer_set.rb +37 -0
  62. data/lib/fontisan/ufo/lib.rb +24 -0
  63. data/lib/fontisan/ufo/plist.rb +118 -0
  64. data/lib/fontisan/ufo/point.rb +38 -0
  65. data/lib/fontisan/ufo/reader.rb +144 -0
  66. data/lib/fontisan/ufo/transformation.rb +39 -0
  67. data/lib/fontisan/ufo/writer.rb +115 -0
  68. data/lib/fontisan/ufo.rb +44 -0
  69. data/lib/fontisan/version.rb +1 -1
  70. data/lib/fontisan.rb +3 -3
  71. metadata +57 -70
  72. data/lib/fontisan/audit/codepoint_range_coalescer.rb +0 -41
  73. data/lib/fontisan/audit/context.rb +0 -122
  74. data/lib/fontisan/audit/differ.rb +0 -124
  75. data/lib/fontisan/audit/extractors/aggregations.rb +0 -54
  76. data/lib/fontisan/audit/extractors/base.rb +0 -26
  77. data/lib/fontisan/audit/extractors/color_capabilities.rb +0 -141
  78. data/lib/fontisan/audit/extractors/coverage.rb +0 -48
  79. data/lib/fontisan/audit/extractors/hinting.rb +0 -197
  80. data/lib/fontisan/audit/extractors/identity.rb +0 -52
  81. data/lib/fontisan/audit/extractors/language_coverage.rb +0 -37
  82. data/lib/fontisan/audit/extractors/licensing.rb +0 -79
  83. data/lib/fontisan/audit/extractors/metrics.rb +0 -103
  84. data/lib/fontisan/audit/extractors/opentype_layout.rb +0 -69
  85. data/lib/fontisan/audit/extractors/provenance.rb +0 -29
  86. data/lib/fontisan/audit/extractors/style.rb +0 -32
  87. data/lib/fontisan/audit/extractors/variation_detail.rb +0 -99
  88. data/lib/fontisan/audit/extractors.rb +0 -27
  89. data/lib/fontisan/audit/library_aggregator.rb +0 -83
  90. data/lib/fontisan/audit/library_auditor.rb +0 -90
  91. data/lib/fontisan/audit/registry.rb +0 -60
  92. data/lib/fontisan/audit/style_extractor.rb +0 -80
  93. data/lib/fontisan/audit.rb +0 -20
  94. data/lib/fontisan/cli/ucd_cli.rb +0 -97
  95. data/lib/fontisan/commands/audit_command.rb +0 -123
  96. data/lib/fontisan/commands/audit_compare_command.rb +0 -66
  97. data/lib/fontisan/commands/audit_library_command.rb +0 -46
  98. data/lib/fontisan/config/ucd.yml +0 -23
  99. data/lib/fontisan/formatters/audit_diff_text_renderer.rb +0 -122
  100. data/lib/fontisan/formatters/audit_text_renderer.rb +0 -324
  101. data/lib/fontisan/formatters/library_summary_text_renderer.rb +0 -99
  102. data/lib/fontisan/models/audit/audit_axis.rb +0 -30
  103. data/lib/fontisan/models/audit/audit_block.rb +0 -32
  104. data/lib/fontisan/models/audit/audit_diff.rb +0 -77
  105. data/lib/fontisan/models/audit/audit_report.rb +0 -153
  106. data/lib/fontisan/models/audit/codepoint_range.rb +0 -40
  107. data/lib/fontisan/models/audit/codepoint_set_diff.rb +0 -34
  108. data/lib/fontisan/models/audit/color_capabilities.rb +0 -93
  109. data/lib/fontisan/models/audit/duplicate_group.rb +0 -23
  110. data/lib/fontisan/models/audit/embedding_type.rb +0 -76
  111. data/lib/fontisan/models/audit/field_change.rb +0 -28
  112. data/lib/fontisan/models/audit/fs_selection_flags.rb +0 -61
  113. data/lib/fontisan/models/audit/gasp_range.rb +0 -63
  114. data/lib/fontisan/models/audit/hinting.rb +0 -93
  115. data/lib/fontisan/models/audit/library_summary.rb +0 -40
  116. data/lib/fontisan/models/audit/licensing.rb +0 -48
  117. data/lib/fontisan/models/audit/metrics.rb +0 -111
  118. data/lib/fontisan/models/audit/named_instance.rb +0 -41
  119. data/lib/fontisan/models/audit/opentype_layout.rb +0 -40
  120. data/lib/fontisan/models/audit/script_coverage_row.rb +0 -26
  121. data/lib/fontisan/models/audit/script_features.rb +0 -28
  122. data/lib/fontisan/models/audit/variation_detail.rb +0 -44
  123. data/lib/fontisan/models/audit.rb +0 -33
  124. data/lib/fontisan/models/ucd/ucd.rb +0 -38
  125. data/lib/fontisan/models/ucd/ucd_char.rb +0 -67
  126. data/lib/fontisan/models/ucd.rb +0 -19
  127. data/lib/fontisan/ucd/aggregator.rb +0 -73
  128. data/lib/fontisan/ucd/cache_manager.rb +0 -111
  129. data/lib/fontisan/ucd/config.rb +0 -59
  130. data/lib/fontisan/ucd/download_error.rb +0 -9
  131. data/lib/fontisan/ucd/downloader.rb +0 -88
  132. data/lib/fontisan/ucd/error.rb +0 -8
  133. data/lib/fontisan/ucd/index.rb +0 -103
  134. data/lib/fontisan/ucd/index_builder.rb +0 -107
  135. data/lib/fontisan/ucd/range_entry.rb +0 -56
  136. data/lib/fontisan/ucd/unknown_version_error.rb +0 -9
  137. data/lib/fontisan/ucd/version_resolver.rb +0 -79
  138. data/lib/fontisan/ucd.rb +0 -23
@@ -1,69 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fontisan
4
- module Audit
5
- module Extractors
6
- # OpenType layout summary: union of GSUB + GPOS scripts and features,
7
- # plus a per-script breakdown preserving which feature belongs to
8
- # which script under which table.
9
- #
10
- # Returned fields:
11
- # opentype_layout: Models::Audit::OpenTypeLayout, or nil for
12
- # Type 1
13
- #
14
- # Owned here (MECE split from Aggregations, which is UCD-only).
15
- class OpenTypeLayout < Base
16
- def extract(context)
17
- font = context.font
18
- return { opentype_layout: nil } unless sfnt?(font)
19
-
20
- gsub_scripts = scripts_in(font, Constants::GSUB_TAG)
21
- gpos_scripts = scripts_in(font, Constants::GPOS_TAG)
22
- all_scripts = (gsub_scripts + gpos_scripts).uniq.sort
23
-
24
- by_script = all_scripts.map do |tag|
25
- Models::Audit::ScriptFeatures.new(
26
- script: tag,
27
- gsub_features: features_for(font, Constants::GSUB_TAG, tag),
28
- gpos_features: features_for(font, Constants::GPOS_TAG, tag),
29
- )
30
- end
31
-
32
- { opentype_layout: Models::Audit::OpenTypeLayout.new(
33
- scripts: all_scripts,
34
- features: aggregate_features(by_script),
35
- by_script: by_script,
36
- has_gsub: font.has_table?(Constants::GSUB_TAG),
37
- has_gpos: font.has_table?(Constants::GPOS_TAG),
38
- ) }
39
- end
40
-
41
- protected
42
-
43
- def sfnt?(font)
44
- font.is_a?(SfntFont)
45
- end
46
-
47
- private
48
-
49
- def scripts_in(font, tag)
50
- return [] unless font.has_table?(tag)
51
-
52
- font.table(tag).scripts
53
- end
54
-
55
- def features_for(font, tag, script)
56
- return [] unless font.has_table?(tag)
57
-
58
- font.table(tag).features(script_tag: script).sort
59
- end
60
-
61
- def aggregate_features(by_script)
62
- gsub = by_script.flat_map(&:gsub_features)
63
- gpos = by_script.flat_map(&:gpos_features)
64
- (gsub + gpos).uniq.sort
65
- end
66
- end
67
- end
68
- end
69
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "digest"
4
- require "time"
5
-
6
- module Fontisan
7
- module Audit
8
- module Extractors
9
- # Provenance fields: who generated this report, when, from what.
10
- #
11
- # Returned fields:
12
- # generated_at, fontisan_version, source_file, source_sha256,
13
- # source_format, font_index, num_fonts_in_source
14
- class Provenance < Base
15
- def extract(context)
16
- {
17
- generated_at: Time.now.utc.iso8601,
18
- fontisan_version: Fontisan::VERSION,
19
- source_file: File.expand_path(context.font_path),
20
- source_sha256: Digest::SHA256.file(context.font_path).hexdigest,
21
- source_format: context.source_format,
22
- font_index: context.font_index,
23
- num_fonts_in_source: context.num_fonts_in_source,
24
- }
25
- end
26
- end
27
- end
28
- end
29
- end
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fontisan
4
- module Audit
5
- module Extractors
6
- # Style fields: weight, width, italic/bold flags, Panose family
7
- # classification.
8
- #
9
- # Returned fields:
10
- # weight_class, width_class, italic, bold, panose
11
- #
12
- # Variable-font axis inventory lives in {Extractors::VariationDetail}
13
- # (MECE: this extractor is the OS/2 + head specialist, that one owns
14
- # everything fvar-derived).
15
- #
16
- # Delegates to {Audit::StyleExtractor} — the existing specialist
17
- # class that owns the OS/2 + head interpretation rules.
18
- class Style < Base
19
- def extract(context)
20
- style = StyleExtractor.new(context.font)
21
- {
22
- weight_class: style.weight_class,
23
- width_class: style.width_class,
24
- italic: style.italic,
25
- bold: style.bold,
26
- panose: style.panose,
27
- }
28
- end
29
- end
30
- end
31
- end
32
- end
@@ -1,99 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fontisan
4
- module Audit
5
- module Extractors
6
- # Variable-font detail: fvar axes + named instances + presence flags
7
- # for every variation side-table (avar, cvar, HVAR, VVAR, MVAR, gvar).
8
- #
9
- # Returned fields:
10
- # variation: Models::Audit::VariationDetail, or nil for non-variable
11
- # faces and Type 1 fonts
12
- #
13
- # A face is considered variable iff the fvar table is present. CFF2
14
- # outlines without fvar are not "variable" by this definition (they
15
- # may carry variation data but no user-facing axes).
16
- class VariationDetail < Base
17
- def extract(context)
18
- font = context.font
19
- return { variation: nil } unless variable?(font)
20
-
21
- fvar = font.table(Constants::FVAR_TAG)
22
- return { variation: nil } unless fvar
23
-
24
- name_table = font.has_table?(Constants::NAME_TAG) ? font.table(Constants::NAME_TAG) : nil
25
- axis_tags = axis_tags_from(fvar)
26
-
27
- { variation: Models::Audit::VariationDetail.new(
28
- axes: build_axes(name_table, fvar),
29
- named_instances: build_instances(name_table, fvar, axis_tags),
30
- has_avar: font.has_table?(Constants::AVAR_TAG),
31
- has_cvar: font.has_table?(Constants::CVAR_TAG),
32
- has_hvar: font.has_table?(Constants::HVAR_TAG),
33
- has_vvar: font.has_table?(Constants::VVAR_TAG),
34
- has_mvar: font.has_table?(Constants::MVAR_TAG),
35
- has_gvar: font.has_table?(Constants::GVAR_TAG),
36
- ) }
37
- end
38
-
39
- protected
40
-
41
- def variable?(font)
42
- font.is_a?(SfntFont) && font.has_table?(Constants::FVAR_TAG)
43
- end
44
-
45
- private
46
-
47
- def build_axes(name_table, fvar)
48
- return [] unless fvar.axes
49
-
50
- fvar.axes.map do |axis|
51
- Models::Audit::AuditAxis.new(
52
- tag: axis.axis_tag,
53
- min_value: axis.min_value,
54
- default_value: axis.default_value,
55
- max_value: axis.max_value,
56
- name: english_name(name_table, axis.axis_name_id),
57
- )
58
- end
59
- end
60
-
61
- def build_instances(name_table, fvar, axis_tags)
62
- instances = fvar.instances
63
- return [] unless instances
64
-
65
- instances.map do |instance|
66
- build_instance(name_table, instance, axis_tags)
67
- end
68
- end
69
-
70
- def build_instance(name_table, instance, axis_tags)
71
- subfamily_name = english_name(name_table, instance[:name_id])
72
- ps_name_id = instance[:postscript_name_id]
73
- ps_name = ps_name_id ? english_name(name_table, ps_name_id) : nil
74
- coords = Models::Audit::NamedInstance.format_coordinates(
75
- axis_tags, instance[:coordinates]
76
- )
77
-
78
- Models::Audit::NamedInstance.new(
79
- subfamily_name: subfamily_name,
80
- postscript_name: ps_name,
81
- coordinates: coords,
82
- )
83
- end
84
-
85
- def english_name(name_table, name_id)
86
- return nil unless name_table && name_id
87
-
88
- name_table.english_name(name_id)
89
- end
90
-
91
- def axis_tags_from(fvar)
92
- return [] unless fvar.axes
93
-
94
- fvar.axes.map(&:axis_tag)
95
- end
96
- end
97
- end
98
- end
99
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Autoload hub for the Fontisan::Audit::Extractors namespace.
4
- #
5
- # Each extractor is a small MECE class with a single `#extract(context)`
6
- # method returning a hash of AuditReport fields. The Audit::Registry
7
- # declares the ordered list.
8
-
9
- module Fontisan
10
- module Audit
11
- module Extractors
12
- autoload :Base, "fontisan/audit/extractors/base"
13
- autoload :Provenance, "fontisan/audit/extractors/provenance"
14
- autoload :Identity, "fontisan/audit/extractors/identity"
15
- autoload :Style, "fontisan/audit/extractors/style"
16
- autoload :Licensing, "fontisan/audit/extractors/licensing"
17
- autoload :Metrics, "fontisan/audit/extractors/metrics"
18
- autoload :Hinting, "fontisan/audit/extractors/hinting"
19
- autoload :ColorCapabilities, "fontisan/audit/extractors/color_capabilities"
20
- autoload :VariationDetail, "fontisan/audit/extractors/variation_detail"
21
- autoload :OpenTypeLayout, "fontisan/audit/extractors/opentype_layout"
22
- autoload :Coverage, "fontisan/audit/extractors/coverage"
23
- autoload :Aggregations, "fontisan/audit/extractors/aggregations"
24
- autoload :LanguageCoverage, "fontisan/audit/extractors/language_coverage"
25
- end
26
- end
27
- end
@@ -1,83 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fontisan
4
- module Audit
5
- # Pure cross-face aggregation over a list of AuditReports.
6
- #
7
- # No I/O, no font parsing — operates only on already-built reports.
8
- # Easy to spec with synthetic reports and trivially testable. The
9
- # orchestrator ({LibraryAuditor}) handles file discovery and per-face
10
- # auditing; this class owns the rollups that span faces.
11
- #
12
- # Aggregates:
13
- # - aggregate_metrics: sum of total_codepoints and total_glyphs.
14
- # - script_coverage: one ScriptCoverageRow per Unicode script,
15
- # listing faces that cover it.
16
- # - duplicate_groups: files bucketed by source_sha256 (size > 1).
17
- # - license_distribution: face counts keyed by license_url.
18
- class LibraryAggregator
19
- # @param reports [Array<Models::Audit::AuditReport>]
20
- # @return [Hash{Symbol => Object}] keys: :aggregate_metrics,
21
- # :script_coverage, :duplicate_groups, :license_distribution
22
- def aggregate(reports)
23
- {
24
- aggregate_metrics: aggregate_metrics(reports),
25
- script_coverage: build_script_coverage(reports),
26
- duplicate_groups: find_duplicates(reports),
27
- license_distribution: license_distribution(reports),
28
- }
29
- end
30
-
31
- private
32
-
33
- def aggregate_metrics(reports)
34
- {
35
- total_codepoints: reports.sum(&:total_codepoints),
36
- total_glyphs: reports.sum(&:total_glyphs),
37
- }
38
- end
39
-
40
- def build_script_coverage(reports)
41
- by_script = Hash.new { |h, k| h[k] = [] }
42
- reports.each do |report|
43
- face = report.postscript_name || report.source_file
44
- scripts_for(report).each { |script| by_script[script] << face }
45
- end
46
- by_script.map do |script, faces|
47
- Models::Audit::ScriptCoverageRow.new(
48
- script: script,
49
- face_count: faces.size,
50
- faces: faces.uniq.sort,
51
- )
52
- end.sort_by { |row| [-row.face_count, row.script] }
53
- end
54
-
55
- def find_duplicates(reports)
56
- reports.group_by(&:source_sha256)
57
- .select { |_sha, group| group.size > 1 }
58
- .map do |sha, group|
59
- Models::Audit::DuplicateGroup.new(
60
- source_sha256: sha,
61
- files: group.map(&:source_file).sort,
62
- )
63
- end
64
- .sort_by(&:source_sha256)
65
- end
66
-
67
- def license_distribution(reports)
68
- reports.each_with_object({}) do |report, counts|
69
- url = license_url_for(report)
70
- counts[url] = counts.fetch(url, 0) + 1
71
- end
72
- end
73
-
74
- def scripts_for(report)
75
- Array(report.unicode_scripts)
76
- end
77
-
78
- def license_url_for(report)
79
- report.licensing&.license_url || "(none)"
80
- end
81
- end
82
- end
83
- end
@@ -1,90 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "pathname"
4
-
5
- module Fontisan
6
- module Audit
7
- # Orchestrates a library-wide audit pass.
8
- #
9
- # Owns the file-system side: discovers font files under a root path
10
- # (recursively or not), audits each via {Commands::AuditCommand},
11
- # and assembles a {Models::Audit::LibrarySummary} combining the
12
- # per-face reports with cross-face rollups from {LibraryAggregator}.
13
- #
14
- # Aggregation logic lives in the pure {LibraryAggregator}; this
15
- # class stays focused on discovery + per-face auditing + summary
16
- # assembly. Errors auditing a single file are logged and skipped so
17
- # a corrupt file doesn't abort the whole pass.
18
- class LibraryAuditor
19
- FONT_EXTENSIONS = %w[.ttf .otf .ttc .otc .dfont .woff .woff2
20
- .pfb .pfa .svg].freeze
21
-
22
- # @param root_path [String, Pathname] directory containing fonts
23
- # @param recursive [Boolean] walk into subdirectories
24
- # @param options [Hash] forwarded to AuditCommand (minus library-only keys)
25
- def initialize(root_path, recursive:, options:)
26
- @root_path = Pathname.new(root_path)
27
- @recursive = recursive
28
- @options = options
29
- @aggregator = LibraryAggregator.new
30
- @skipped = []
31
- end
32
-
33
- # @return [Models::Audit::LibrarySummary]
34
- def audit
35
- paths = discover_font_paths
36
- reports = paths.flat_map { |p| audit_one(p) }
37
- rolled_up = aggregates(reports)
38
-
39
- Models::Audit::LibrarySummary.new(
40
- root_path: @root_path.to_s,
41
- total_files: paths.size,
42
- total_faces: reports.size,
43
- scanned_extensions: scanned_extensions(paths),
44
- aggregate_metrics: rolled_up[:aggregate_metrics].merge(
45
- total_size_bytes: paths.sum { |p| File.size(p) },
46
- ),
47
- script_coverage: rolled_up[:script_coverage],
48
- duplicate_groups: rolled_up[:duplicate_groups],
49
- license_distribution: rolled_up[:license_distribution],
50
- per_face_reports: reports,
51
- )
52
- end
53
-
54
- # @return [Array<String>] source files that could not be audited
55
- attr_reader :skipped
56
-
57
- private
58
-
59
- def discover_font_paths
60
- method = @recursive ? :find : :children
61
- @root_path.public_send(method).select do |entry|
62
- next false unless entry.file?
63
- next false if entry.symlink?
64
-
65
- FONT_EXTENSIONS.include?(entry.extname.downcase)
66
- end.map(&:to_s).sort
67
- end
68
-
69
- def audit_one(path)
70
- Array(Commands::AuditCommand.new(path, audit_options).run)
71
- rescue StandardError => e
72
- @skipped << "#{path}: #{e.message}"
73
- []
74
- end
75
-
76
- # Drop library-only options before forwarding to AuditCommand.
77
- def audit_options
78
- @options.except(:recursive, :summary, :output)
79
- end
80
-
81
- def scanned_extensions(paths)
82
- paths.map { |p| File.extname(p).downcase }.uniq.sort
83
- end
84
-
85
- def aggregates(reports)
86
- @aggregator.aggregate(reports)
87
- end
88
- end
89
- end
90
- end
@@ -1,60 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fontisan
4
- module Audit
5
- # Ordered list of extractor classes run for every audit face.
6
- #
7
- # Order matters only for human-readable output (text formatter).
8
- # All extractors are independent; their outputs are merged into
9
- # one big hash before constructing the AuditReport.
10
- #
11
- # Add new extractors here. AuditCommand never enumerates them
12
- # directly (OCP: adding a concern = one line here + one file).
13
- module Registry
14
- # Full audit: every concern.
15
- ORDERED_EXTRACTORS = [
16
- Extractors::Provenance,
17
- Extractors::Identity,
18
- Extractors::Style,
19
- Extractors::Licensing,
20
- Extractors::Metrics,
21
- Extractors::Hinting,
22
- Extractors::ColorCapabilities,
23
- Extractors::VariationDetail,
24
- Extractors::OpenTypeLayout,
25
- Extractors::Coverage,
26
- Extractors::Aggregations,
27
- Extractors::LanguageCoverage,
28
- ].freeze
29
-
30
- # Brief audit: only the cheap, name-table-only extractors. Skips
31
- # metrics/hinting/color/variation/layout (extra table loads) and
32
- # aggregations/language coverage (need UCD/CLDR indices). Used by
33
- # `fontisan audit --brief` for a fast inventory pass.
34
- BRIEF_EXTRACTORS = [
35
- Extractors::Provenance,
36
- Extractors::Identity,
37
- Extractors::Style,
38
- Extractors::Licensing,
39
- Extractors::Coverage,
40
- ].freeze
41
-
42
- # Iterate the extractors appropriate for the given mode.
43
- #
44
- # @param mode [Symbol] :full (default) or :brief
45
- # @yieldparam extractor_class [Class]
46
- def self.each(mode: :full, &)
47
- extractors_for(mode).each(&)
48
- end
49
-
50
- # @param mode [Symbol] :full or :brief
51
- # @return [Array<Class>] the extractor list for the given mode
52
- def self.extractors_for(mode)
53
- case mode
54
- when :brief then BRIEF_EXTRACTORS
55
- else ORDERED_EXTRACTORS
56
- end
57
- end
58
- end
59
- end
60
- end
@@ -1,80 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fontisan
4
- module Audit
5
- # Extracts style descriptors from a loaded font's OS/2 and head tables.
6
- # One extractor per font; cheap to construct.
7
- #
8
- # All fields return nil when the underlying table is absent or the
9
- # value is unset (e.g., Type 1 fonts have no OS/2). Callers must
10
- # tolerate nils.
11
- #
12
- # Scope: OS/2 + head only. fvar-derived fields (axes, named instances,
13
- # variable presence) live on {Extractors::VariationDetail} — this is
14
- # the MECE split between static style metadata and variation metadata.
15
- #
16
- # Duck typing: uses only `font.has_table?(tag)` and `font.table(tag)`.
17
- # No class-specific branching — any object that honors the SFNT
18
- # contract works (TrueTypeFont, OpenTypeFont, WoffFont, Woff2Font,
19
- # and individual faces from collections).
20
- class StyleExtractor
21
- FS_SELECTION_ITALIC_BIT = 0
22
- MAC_STYLE_BOLD_BIT = 0
23
- private_constant :FS_SELECTION_ITALIC_BIT, :MAC_STYLE_BOLD_BIT
24
-
25
- # @param font [Object] an SFNT-compatible font object
26
- def initialize(font)
27
- @font = font
28
- end
29
-
30
- def weight_class
31
- os2&.us_weight_class&.to_i
32
- end
33
-
34
- def width_class
35
- os2&.us_width_class&.to_i
36
- end
37
-
38
- # OS/2.fsSelection bit 0 (ITALIC).
39
- def italic
40
- return nil unless os2
41
-
42
- (os2.fs_selection.to_i & (1 << FS_SELECTION_ITALIC_BIT)).nonzero?
43
- end
44
-
45
- # head.macStyle bit 0 (BOLD). Per OpenType convention, bold is read
46
- # from head, not OS/2.
47
- def bold
48
- return nil unless head
49
-
50
- (head.mac_style.to_i & (1 << MAC_STYLE_BOLD_BIT)).nonzero?
51
- end
52
-
53
- # OS/2.panose as a space-joined 10-digit string, e.g. "2 0 5 3 0 0 0 0 0 0".
54
- # Returns nil if there is no OS/2 table.
55
- def panose
56
- bytes = os2&.panose
57
- return nil if bytes.nil?
58
-
59
- bytes = bytes.to_a
60
- return nil if bytes.empty?
61
-
62
- bytes.join(" ")
63
- end
64
-
65
- private
66
-
67
- def os2
68
- return @os2 if defined?(@os2)
69
-
70
- @os2 = @font.has_table?("OS/2") ? @font.table("OS/2") : nil
71
- end
72
-
73
- def head
74
- return @head if defined?(@head)
75
-
76
- @head = @font.has_table?("head") ? @font.table("head") : nil
77
- end
78
- end
79
- end
80
- end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Autoload hub for the Fontisan::Audit namespace.
4
- #
5
- # AuditCommand (under Commands::AuditCommand) builds a Context and
6
- # runs every extractor in Audit::Registry, merging their outputs
7
- # into a single AuditReport.
8
-
9
- module Fontisan
10
- module Audit
11
- autoload :Context, "fontisan/audit/context"
12
- autoload :CodepointRangeCoalescer, "fontisan/audit/codepoint_range_coalescer"
13
- autoload :Differ, "fontisan/audit/differ"
14
- autoload :LibraryAggregator, "fontisan/audit/library_aggregator"
15
- autoload :LibraryAuditor, "fontisan/audit/library_auditor"
16
- autoload :Registry, "fontisan/audit/registry"
17
- autoload :Extractors, "fontisan/audit/extractors"
18
- autoload :StyleExtractor, "fontisan/audit/style_extractor"
19
- end
20
- end
@@ -1,97 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "thor"
4
-
5
- module Fontisan
6
- # Thor subcommand for managing the local UCD (Unicode Character
7
- # Database) cache used by `fontisan audit`.
8
- #
9
- # fontisan ucd download [VERSION] fetch + index UCDXML
10
- # fontisan ucd status show what's cached
11
- # fontisan ucd path [VERSION] print local cache path
12
- # fontisan ucd list list known versions
13
- # fontisan ucd remove VERSION delete a cached version
14
- #
15
- # With no arguments, `download` resolves the configured default version
16
- # (see lib/fontisan/config/ucd.yml).
17
- class UcdCli < Thor
18
- desc "download [VERSION]",
19
- "Download and index UCDXML (default: configured default version)"
20
- option :force, type: :boolean, default: false,
21
- desc: "Re-download even if already cached"
22
- option :latest, type: :boolean, default: false,
23
- desc: "Probe unicode.org for the latest version"
24
- # Download (and index) UCDXML for a version.
25
- #
26
- # @param version [String, nil] explicit version, or omit for default
27
- def download(version = nil)
28
- intent = resolve_intent(version, options[:latest])
29
- actual = Ucd::VersionResolver.resolve(intent)
30
-
31
- path = Ucd::Downloader.download(actual, force: options[:force])
32
- Ucd::IndexBuilder.build(actual) unless index_present?(actual)
33
- puts "UCD #{actual} ready at: #{path}"
34
- rescue Ucd::Error => e
35
- warn "ERROR: #{e.message}"
36
- exit 1
37
- end
38
-
39
- desc "status", "Show cached UCD versions and default version"
40
- # Print a one-screen summary of the local cache state.
41
- def status
42
- cached = Ucd::CacheManager.cached_versions
43
- puts "Default version: #{Ucd::Config.default_version}"
44
- puts "Cache root: #{Ucd::CacheManager.root}"
45
- puts "Cached versions: #{cached.empty? ? '(none)' : cached.join(', ')}"
46
- end
47
-
48
- desc "path [VERSION]", "Print local cache directory for a version"
49
- # Print the cache directory path for a version (default: default version).
50
- #
51
- # @param version [String, nil]
52
- def path(version = nil)
53
- actual = Ucd::VersionResolver.resolve(version)
54
- puts Ucd::CacheManager.version_dir(actual)
55
- rescue Ucd::UnknownVersionError => e
56
- warn "ERROR: #{e.message}"
57
- exit 1
58
- end
59
-
60
- desc "list", "List UCD versions known to this Fontisan release"
61
- # Print the curated list of versions this Fontisan release supports.
62
- def list
63
- Ucd::Config.known_versions.each { |v| puts v }
64
- end
65
-
66
- desc "remove VERSION", "Remove a cached UCD version"
67
- # Delete one cached version. No-op if absent.
68
- #
69
- # @param version [String]
70
- def remove(version)
71
- Ucd::VersionResolver.validate!(version)
72
- unless Ucd::CacheManager.cached?(version)
73
- warn "Version #{version} is not cached; nothing to remove."
74
- return
75
- end
76
-
77
- Ucd::CacheManager.remove_version(version)
78
- puts "Removed UCD #{version}."
79
- rescue Ucd::UnknownVersionError => e
80
- warn "ERROR: #{e.message}"
81
- exit 1
82
- end
83
-
84
- private
85
-
86
- def resolve_intent(version, latest)
87
- return :latest if latest && version.nil?
88
-
89
- version
90
- end
91
-
92
- def index_present?(version)
93
- Ucd::CacheManager.blocks_index_path(version).exist? &&
94
- Ucd::CacheManager.scripts_index_path(version).exist?
95
- end
96
- end
97
- end