fontisan 0.2.22 → 0.2.23

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 (88) 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 +0 -170
  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/tables/cff.rb +2 -1
  18. data/lib/fontisan/tables.rb +2 -1
  19. data/lib/fontisan/version.rb +1 -1
  20. data/lib/fontisan.rb +0 -3
  21. metadata +7 -70
  22. data/lib/fontisan/audit/codepoint_range_coalescer.rb +0 -41
  23. data/lib/fontisan/audit/context.rb +0 -122
  24. data/lib/fontisan/audit/differ.rb +0 -124
  25. data/lib/fontisan/audit/extractors/aggregations.rb +0 -54
  26. data/lib/fontisan/audit/extractors/base.rb +0 -26
  27. data/lib/fontisan/audit/extractors/color_capabilities.rb +0 -141
  28. data/lib/fontisan/audit/extractors/coverage.rb +0 -48
  29. data/lib/fontisan/audit/extractors/hinting.rb +0 -197
  30. data/lib/fontisan/audit/extractors/identity.rb +0 -52
  31. data/lib/fontisan/audit/extractors/language_coverage.rb +0 -37
  32. data/lib/fontisan/audit/extractors/licensing.rb +0 -79
  33. data/lib/fontisan/audit/extractors/metrics.rb +0 -103
  34. data/lib/fontisan/audit/extractors/opentype_layout.rb +0 -69
  35. data/lib/fontisan/audit/extractors/provenance.rb +0 -29
  36. data/lib/fontisan/audit/extractors/style.rb +0 -32
  37. data/lib/fontisan/audit/extractors/variation_detail.rb +0 -99
  38. data/lib/fontisan/audit/extractors.rb +0 -27
  39. data/lib/fontisan/audit/library_aggregator.rb +0 -83
  40. data/lib/fontisan/audit/library_auditor.rb +0 -90
  41. data/lib/fontisan/audit/registry.rb +0 -60
  42. data/lib/fontisan/audit/style_extractor.rb +0 -80
  43. data/lib/fontisan/audit.rb +0 -20
  44. data/lib/fontisan/cli/ucd_cli.rb +0 -97
  45. data/lib/fontisan/commands/audit_command.rb +0 -123
  46. data/lib/fontisan/commands/audit_compare_command.rb +0 -66
  47. data/lib/fontisan/commands/audit_library_command.rb +0 -46
  48. data/lib/fontisan/config/ucd.yml +0 -23
  49. data/lib/fontisan/formatters/audit_diff_text_renderer.rb +0 -122
  50. data/lib/fontisan/formatters/audit_text_renderer.rb +0 -324
  51. data/lib/fontisan/formatters/library_summary_text_renderer.rb +0 -99
  52. data/lib/fontisan/models/audit/audit_axis.rb +0 -30
  53. data/lib/fontisan/models/audit/audit_block.rb +0 -32
  54. data/lib/fontisan/models/audit/audit_diff.rb +0 -77
  55. data/lib/fontisan/models/audit/audit_report.rb +0 -153
  56. data/lib/fontisan/models/audit/codepoint_range.rb +0 -40
  57. data/lib/fontisan/models/audit/codepoint_set_diff.rb +0 -34
  58. data/lib/fontisan/models/audit/color_capabilities.rb +0 -93
  59. data/lib/fontisan/models/audit/duplicate_group.rb +0 -23
  60. data/lib/fontisan/models/audit/embedding_type.rb +0 -76
  61. data/lib/fontisan/models/audit/field_change.rb +0 -28
  62. data/lib/fontisan/models/audit/fs_selection_flags.rb +0 -61
  63. data/lib/fontisan/models/audit/gasp_range.rb +0 -63
  64. data/lib/fontisan/models/audit/hinting.rb +0 -93
  65. data/lib/fontisan/models/audit/library_summary.rb +0 -40
  66. data/lib/fontisan/models/audit/licensing.rb +0 -48
  67. data/lib/fontisan/models/audit/metrics.rb +0 -111
  68. data/lib/fontisan/models/audit/named_instance.rb +0 -41
  69. data/lib/fontisan/models/audit/opentype_layout.rb +0 -40
  70. data/lib/fontisan/models/audit/script_coverage_row.rb +0 -26
  71. data/lib/fontisan/models/audit/script_features.rb +0 -28
  72. data/lib/fontisan/models/audit/variation_detail.rb +0 -44
  73. data/lib/fontisan/models/audit.rb +0 -33
  74. data/lib/fontisan/models/ucd/ucd.rb +0 -38
  75. data/lib/fontisan/models/ucd/ucd_char.rb +0 -67
  76. data/lib/fontisan/models/ucd.rb +0 -19
  77. data/lib/fontisan/ucd/aggregator.rb +0 -73
  78. data/lib/fontisan/ucd/cache_manager.rb +0 -111
  79. data/lib/fontisan/ucd/config.rb +0 -59
  80. data/lib/fontisan/ucd/download_error.rb +0 -9
  81. data/lib/fontisan/ucd/downloader.rb +0 -88
  82. data/lib/fontisan/ucd/error.rb +0 -8
  83. data/lib/fontisan/ucd/index.rb +0 -103
  84. data/lib/fontisan/ucd/index_builder.rb +0 -107
  85. data/lib/fontisan/ucd/range_entry.rb +0 -56
  86. data/lib/fontisan/ucd/unknown_version_error.rb +0 -9
  87. data/lib/fontisan/ucd/version_resolver.rb +0 -79
  88. data/lib/fontisan/ucd.rb +0 -23
@@ -1,124 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fontisan
4
- module Audit
5
- # Computes an {Models::Audit::AuditDiff} between two AuditReports.
6
- #
7
- # Pure: no I/O, no font parsing. Both reports must already be built
8
- # (Commands::AuditCompareCommand handles loading reports from disk
9
- # or auditing fresh fonts before invoking the differ).
10
- #
11
- # Comparison shape:
12
- # - Scalar fields: one FieldChange per differing field.
13
- # - Codepoint coverage: CodepointSetDiff built from the cmap range
14
- # lists (expanded to integer sets for set arithmetic, then
15
- # re-coalesced to ranges for output).
16
- # - Structural inventories (features, scripts, blocks, languages):
17
- # simple array set-diffs.
18
- class Differ
19
- # Scalar AuditReport fields compared field-by-field. Excludes
20
- # generated_at / source_sha256 / source_file (per-report identity),
21
- # codepoints / codepoint_ranges (handled via CodepointSetDiff),
22
- # and nested models (surfaced via structural add/remove lists).
23
- COMPARED_FIELDS = %i[
24
- family_name subfamily_name full_name postscript_name version
25
- font_revision weight_class width_class italic bold panose
26
- total_codepoints total_glyphs ucd_version cldr_version
27
- ].freeze
28
-
29
- def initialize(left_report, right_report)
30
- @left = left_report
31
- @right = right_report
32
- end
33
-
34
- # @return [Models::Audit::AuditDiff]
35
- def diff
36
- Models::Audit::AuditDiff.new(
37
- left_source: @left.source_file,
38
- right_source: @right.source_file,
39
- field_changes: field_changes,
40
- codepoints: codepoint_diff,
41
- added_features: set_diff(features(@right), features(@left)),
42
- removed_features: set_diff(features(@left), features(@right)),
43
- added_scripts: set_diff(scripts(@right), scripts(@left)),
44
- removed_scripts: set_diff(scripts(@left), scripts(@right)),
45
- added_blocks: set_diff(blocks(@right), blocks(@left)),
46
- removed_blocks: set_diff(blocks(@left), blocks(@right)),
47
- added_languages: set_diff(languages(@right), languages(@left)),
48
- removed_languages: set_diff(languages(@left), languages(@right)),
49
- )
50
- end
51
-
52
- private
53
-
54
- def field_changes
55
- COMPARED_FIELDS.filter_map do |field|
56
- left_val = @left.public_send(field)
57
- right_val = @right.public_send(field)
58
- next if left_val == right_val
59
-
60
- Models::Audit::FieldChange.new(
61
- field: field.to_s,
62
- left: serialize_value(left_val),
63
- right: serialize_value(right_val),
64
- )
65
- end
66
- end
67
-
68
- def codepoint_diff
69
- left_set = codepoints_from_ranges(@left)
70
- right_set = codepoints_from_ranges(@right)
71
- added = right_set - left_set
72
- removed = left_set - right_set
73
- unchanged = left_set & right_set
74
-
75
- Models::Audit::CodepointSetDiff.new(
76
- added: CodepointRangeCoalescer.call(added.to_a),
77
- removed: CodepointRangeCoalescer.call(removed.to_a),
78
- added_count: added.size,
79
- removed_count: removed.size,
80
- unchanged_count: unchanged.size,
81
- )
82
- end
83
-
84
- # Expand a report's compact codepoint range list into a Set<Integer>.
85
- # Works for both default reports (range list populated) and
86
- # --all-codepoints reports (range list is also populated).
87
- def codepoints_from_ranges(report)
88
- ranges = report.codepoint_ranges || []
89
- ranges.each_with_object(Set.new) do |range, set|
90
- (range.first_cp..range.last_cp).each { |cp| set << cp }
91
- end
92
- end
93
-
94
- def features(report)
95
- report.opentype_layout&.features || []
96
- end
97
-
98
- def scripts(report)
99
- report.opentype_layout&.scripts || []
100
- end
101
-
102
- def blocks(report)
103
- (report.blocks || []).map(&:name)
104
- end
105
-
106
- def languages(report)
107
- (report.language_coverage || []).map(&:language)
108
- end
109
-
110
- def set_diff(minuend, subtrahend)
111
- (Array(minuend) - Array(subtrahend)).sort
112
- end
113
-
114
- def serialize_value(value)
115
- case value
116
- when nil then ""
117
- when String, Integer, Float then value.to_s
118
- when true, false then value.to_s
119
- else value.to_yaml
120
- end
121
- end
122
- end
123
- end
124
- end
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fontisan
4
- module Audit
5
- module Extractors
6
- # Aggregation fields: UCD block/script coverage.
7
- #
8
- # Returned fields:
9
- # ucd_version, blocks, unicode_scripts
10
- #
11
- # OpenType script/feature inventory lives in {Extractors::OpenTypeLayout}
12
- # (MECE: this extractor is UCD-driven, that one is SFNT-table-driven).
13
- class Aggregations < Base
14
- def extract(context)
15
- ucd = context.ucd
16
- ucd_aggregations(context.codepoints, ucd)
17
- end
18
-
19
- private
20
-
21
- def ucd_aggregations(codepoints, ucd)
22
- return empty_aggregation(ucd) if ucd[:blocks_index].nil?
23
-
24
- blocks_hashes = Ucd::Aggregator.aggregate_blocks(codepoints,
25
- ucd[:blocks_index])
26
- {
27
- ucd_version: ucd[:version],
28
- blocks: blocks_hashes.map { |h| build_audit_block(h) },
29
- unicode_scripts: Ucd::Aggregator.aggregate_scripts(codepoints,
30
- ucd[:scripts_index]),
31
- }
32
- end
33
-
34
- def empty_aggregation(ucd)
35
- { ucd_version: ucd[:version], blocks: [], unicode_scripts: [] }
36
- end
37
-
38
- def build_audit_block(block_hash)
39
- Models::Audit::AuditBlock.new(
40
- name: block_hash[:name],
41
- first_cp: block_hash[:first_cp],
42
- last_cp: block_hash[:last_cp],
43
- range: format("U+%<first>04X-U+%<last>04X",
44
- first: block_hash[:first_cp], last: block_hash[:last_cp]),
45
- total: block_hash[:total],
46
- covered: block_hash[:covered],
47
- fill_ratio: block_hash[:fill_ratio],
48
- complete: block_hash[:complete],
49
- )
50
- end
51
- end
52
- end
53
- end
54
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fontisan
4
- module Audit
5
- module Extractors
6
- # Abstract extractor interface. Subclasses implement `#extract`.
7
- #
8
- # An extractor reads from a Context and returns a hash of fields
9
- # suitable for `Models::Audit::AuditReport.new(**fields)`.
10
- # Returning an empty hash is valid (no-op).
11
- class Base
12
- def extract(context)
13
- raise NotImplementedError,
14
- "#{self.class} must implement #extract"
15
- end
16
-
17
- protected
18
-
19
- # Convenience accessor used by most extractors.
20
- def font(context)
21
- context.font
22
- end
23
- end
24
- end
25
- end
26
- end
@@ -1,141 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fontisan
4
- module Audit
5
- module Extractors
6
- # Color-font capability summary: which color formats a face carries
7
- # (COLR v0/v1, CPAL, SVG, CBDT/CBLC, sbix) plus lightweight counts
8
- # from each table's header.
9
- #
10
- # Returned fields:
11
- # color_capabilities: Models::Audit::ColorCapabilities, or nil
12
- # for Type 1
13
- #
14
- # Counts are best-effort — any table that fails to parse yields nil
15
- # for its corresponding count fields rather than crashing the audit.
16
- class ColorCapabilities < Base
17
- def extract(context)
18
- font = context.font
19
- return { color_capabilities: nil } unless sfnt?(font)
20
-
21
- { color_capabilities: Models::Audit::ColorCapabilities.new(**gather(font)) }
22
- end
23
-
24
- protected
25
-
26
- def sfnt?(font)
27
- font.is_a?(SfntFont)
28
- end
29
-
30
- private
31
-
32
- def gather(font)
33
- colr = colr_fields(font)
34
- cpal = cpal_fields(font)
35
- svg = svg_fields(font)
36
- cbdt = cbdt_fields(font)
37
- sbix = sbix_fields(font)
38
-
39
- formats = Models::Audit::ColorCapabilities.derive_formats(
40
- has_colr: colr[:has_colr], colr_version: colr[:colr_version],
41
- has_cpal: cpal[:has_cpal], has_svg: svg[:has_svg],
42
- has_cbdt: cbdt[:has_cbdt], has_sbix: sbix[:has_sbix]
43
- )
44
-
45
- colr.merge(cpal).merge(svg).merge(cbdt).merge(sbix)
46
- .merge(color_formats: formats)
47
- end
48
-
49
- def colr_fields(font)
50
- return empty_colr unless font.has_table?(Constants::COLR_TAG)
51
-
52
- colr = font.table(Constants::COLR_TAG)
53
- return empty_colr unless colr
54
-
55
- {
56
- has_colr: true,
57
- colr_version: colr.version&.to_i,
58
- colr_base_glyph_count: colr.num_base_glyph_records&.to_i,
59
- colr_layer_count: colr.num_layer_records&.to_i,
60
- }
61
- end
62
-
63
- def empty_colr
64
- { has_colr: false, colr_version: nil,
65
- colr_base_glyph_count: nil, colr_layer_count: nil }
66
- end
67
-
68
- def cpal_fields(font)
69
- return empty_cpal unless font.has_table?(Constants::CPAL_TAG)
70
-
71
- cpal = font.table(Constants::CPAL_TAG)
72
- return empty_cpal unless cpal
73
-
74
- {
75
- has_cpal: true,
76
- cpal_palette_count: cpal.num_palettes&.to_i,
77
- cpal_color_count: cpal.num_color_records&.to_i,
78
- }
79
- end
80
-
81
- def empty_cpal
82
- { has_cpal: false, cpal_palette_count: nil, cpal_color_count: nil }
83
- end
84
-
85
- def svg_fields(font)
86
- return empty_svg unless font.has_table?(Constants::SVG_TAG)
87
-
88
- svg = font.table(Constants::SVG_TAG)
89
- return empty_svg unless svg
90
-
91
- {
92
- has_svg: true,
93
- svg_document_count: svg.num_svg_documents&.to_i,
94
- }
95
- end
96
-
97
- def empty_svg
98
- { has_svg: false, svg_document_count: nil }
99
- end
100
-
101
- # CBDT/CBLC are paired tables: CBLC holds the strike index,
102
- # CBDT holds the bitmap data. has_cbdt vs has_cblc disagreement
103
- # is reported as-is — audit consumers can spot the inconsistency.
104
- def cbdt_fields(font)
105
- has_cbdt = font.has_table?(Constants::CBDT_TAG)
106
- has_cblc = font.has_table?(Constants::CBLC_TAG)
107
- strike_count = cblc_strike_count(font) if has_cblc
108
-
109
- {
110
- has_cbdt: has_cbdt,
111
- has_cblc: has_cblc,
112
- cbdt_strike_count: strike_count,
113
- }
114
- end
115
-
116
- def cblc_strike_count(font)
117
- cblc = font.table(Constants::CBLC_TAG)
118
- return nil unless cblc
119
-
120
- cblc.num_sizes&.to_i
121
- end
122
-
123
- def sbix_fields(font)
124
- return empty_sbix unless font.has_table?(Constants::SBIX_TAG)
125
-
126
- sbix = font.table(Constants::SBIX_TAG)
127
- return empty_sbix unless sbix
128
-
129
- {
130
- has_sbix: true,
131
- sbix_strike_count: sbix.num_strikes&.to_i,
132
- }
133
- end
134
-
135
- def empty_sbix
136
- { has_sbix: false, sbix_strike_count: nil }
137
- end
138
- end
139
- end
140
- end
141
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fontisan
4
- module Audit
5
- module Extractors
6
- # Coverage fields: how many codepoints and glyphs the font ships,
7
- # the compact codepoint-range view (default), and the optional flat
8
- # per-codepoint list (only when `--all-codepoints` is on).
9
- #
10
- # Returned fields:
11
- # total_codepoints, total_glyphs, cmap_subtables,
12
- # codepoint_ranges, codepoints
13
- class Coverage < Base
14
- def extract(context)
15
- font = context.font
16
- codepoints = context.codepoints
17
- {
18
- total_codepoints: codepoints.length,
19
- total_glyphs: total_glyphs(font),
20
- cmap_subtables: cmap_subtable_formats(font),
21
- codepoint_ranges: CodepointRangeCoalescer.call(codepoints),
22
- codepoints: codepoints_for_report(context, codepoints),
23
- }
24
- end
25
-
26
- private
27
-
28
- def total_glyphs(font)
29
- return nil unless font.has_table?(Constants::MAXP_TAG)
30
-
31
- font.table(Constants::MAXP_TAG).num_glyphs
32
- end
33
-
34
- def cmap_subtable_formats(font)
35
- return [] unless font.has_table?(Constants::CMAP_TAG)
36
-
37
- font.table(Constants::CMAP_TAG).subtable_formats
38
- end
39
-
40
- def codepoints_for_report(context, codepoints)
41
- return [] unless context.all_codepoints?
42
-
43
- codepoints.map { |cp| format("U+%<cp>04X", cp: cp) }
44
- end
45
- end
46
- end
47
- end
48
- end
@@ -1,197 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "stringio"
4
-
5
- module Fontisan
6
- module Audit
7
- module Extractors
8
- # Hinting summary: TrueType bytecode counts + gasp policy + CFF stem
9
- # count, with derived `is_unhinted` and `hinting_format` fields.
10
- #
11
- # Returned fields:
12
- # hinting: Models::Audit::Hinting instance, or nil for Type 1
13
- #
14
- # The fpgm/prep/cvt/gasp tables have no BinData classes yet — they
15
- # are read as raw bytes from `font.table_data`. Bytecode is one byte
16
- # per instruction; cvt is an array of FWord (int16), so the entry
17
- # count is bytesize / 2.
18
- class Hinting < Base
19
- # Raw CFF2 / CFF2 charstring operator bytes that declare stem hints.
20
- HSTEM = 1
21
- VSTEM = 3
22
- HSTEMHM = 18
23
- VSTEMHM = 23
24
- HINTMASK = 19
25
- CNTRMASK = 20
26
-
27
- def extract(context)
28
- font = context.font
29
- return { hinting: nil } unless sfnt?(font)
30
-
31
- { hinting: Models::Audit::Hinting.new(**gather(font)) }
32
- end
33
-
34
- protected
35
-
36
- def sfnt?(font)
37
- font.is_a?(SfntFont)
38
- end
39
-
40
- private
41
-
42
- def gather(font)
43
- tt = truetype_fields(font)
44
- cff = cff_fields(font)
45
- gasp = parse_gasp(font)
46
-
47
- derived = Models::Audit::Hinting.derive_flags(
48
- has_tt: tt[:has_fpgm] || tt[:has_prep] || tt[:has_cvt],
49
- has_cff: cff[:cff_has_private_dict],
50
- has_gasp: !gasp.empty?,
51
- )
52
-
53
- tt.merge(cff).merge(gasp_ranges: gasp).merge(derived)
54
- end
55
-
56
- def truetype_fields(font)
57
- {
58
- has_fpgm: font.has_table?(Constants::FPGM_TAG),
59
- fpgm_instruction_count: byte_count(font, Constants::FPGM_TAG),
60
- has_prep: font.has_table?(Constants::PREP_TAG),
61
- prep_instruction_count: byte_count(font, Constants::PREP_TAG),
62
- has_cvt: font.has_table?(Constants::CVT_TAG),
63
- cvt_entry_count: cvt_entry_count(font),
64
- has_cvar: font.has_table?(Constants::CVAR_TAG),
65
- }
66
- end
67
-
68
- def cff_fields(font)
69
- has_cff1 = font.has_table?(Constants::CFF_TAG)
70
- has_cff2 = font.has_table?(Constants::CFF2_TAG)
71
- has_private = has_cff1 || has_cff2
72
-
73
- {
74
- cff_has_private_dict: has_private,
75
- cff_hint_count: has_cff1 ? count_cff_stems(font) : nil,
76
- }
77
- end
78
-
79
- def byte_count(font, tag)
80
- return nil unless font.has_table?(tag)
81
-
82
- font.table_data[tag]&.bytesize
83
- end
84
-
85
- def cvt_entry_count(font)
86
- return nil unless font.has_table?(Constants::CVT_TAG)
87
-
88
- bytes = font.table_data[Constants::CVT_TAG]
89
- return nil unless bytes
90
-
91
- bytes.bytesize / 2
92
- end
93
-
94
- # Parse the gasp table from raw bytes. Format: uint16 version,
95
- # uint16 numRanges, then numRanges × (uint16 rangeMaxPPEM,
96
- # uint16 rangeFlags). Returns [] if gasp is absent or truncated.
97
- def parse_gasp(font)
98
- return [] unless font.has_table?(Constants::GASP_TAG)
99
-
100
- data = font.table_data[Constants::GASP_TAG]
101
- return [] unless data && data.bytesize >= 4
102
-
103
- _version, num_ranges = data.unpack("nn")
104
- ranges = []
105
- offset = 4
106
- num_ranges.times do
107
- break if offset + 4 > data.bytesize
108
-
109
- max_ppem, flags = data[offset, 4].unpack("nn")
110
- ranges << Models::Audit::GaspRange.from_flags(max_ppem, flags)
111
- offset += 4
112
- end
113
- ranges
114
- end
115
-
116
- def count_cff_stems(font)
117
- return nil unless font.has_table?(Constants::CFF_TAG)
118
-
119
- cff = font.table(Constants::CFF_TAG)
120
- return nil unless cff
121
-
122
- index = cff.charstrings_index(0)
123
- return nil unless index
124
-
125
- total = 0
126
- index.count.times do |glyph_index|
127
- data = index[glyph_index]
128
- next unless data
129
-
130
- total += count_stems_in_charstring(data)
131
- end
132
- total
133
- rescue CorruptedTableError
134
- nil
135
- end
136
-
137
- # Lightweight Type-2 CharString scanner that counts stem hints
138
- # without instantiating a full CharString (which needs a Private
139
- # DICT, global/local subrs, etc.). Operates purely on bytes.
140
- def count_stems_in_charstring(data)
141
- io = StringIO.new(data)
142
- stack = 0
143
- stems = 0
144
-
145
- until io.eof?
146
- byte = io.getbyte
147
- next if byte.nil?
148
-
149
- stack, stems = process_byte(io, byte, stack, stems)
150
- end
151
-
152
- stems
153
- end
154
-
155
- def process_byte(io, byte, stack, stems)
156
- if operator_byte?(byte)
157
- apply_operator(io, byte, stack, stems)
158
- else
159
- [consume_operand(io, byte, stack), stems]
160
- end
161
- end
162
-
163
- def operator_byte?(byte)
164
- byte <= 31 && byte != 28
165
- end
166
-
167
- def apply_operator(io, byte, stack, stems)
168
- case byte
169
- when 12
170
- io.getbyte
171
- [0, stems]
172
- when HSTEM, VSTEM, HSTEMHM, VSTEMHM
173
- [0, stems + stack / 2]
174
- when HINTMASK, CNTRMASK
175
- new_stems = stems + stack / 2
176
- io.read((new_stems + 7) / 8)
177
- [0, new_stems]
178
- else
179
- [0, stems]
180
- end
181
- end
182
-
183
- def consume_operand(io, byte, stack)
184
- case byte
185
- when 28
186
- io.read(2)
187
- when 255
188
- io.read(4)
189
- when 247..254
190
- io.getbyte
191
- end
192
- stack + 1
193
- end
194
- end
195
- end
196
- end
197
- end
@@ -1,52 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fontisan
4
- module Audit
5
- module Extractors
6
- # Identity fields: the human-readable names a font uses to describe
7
- # itself, drawn from the `name` table (SFNT) or font dictionary
8
- # (Type 1).
9
- #
10
- # Returned fields:
11
- # family_name, subfamily_name, full_name, postscript_name,
12
- # version, font_revision
13
- class Identity < Base
14
- def extract(context)
15
- if context.font.is_a?(Type1Font)
16
- type1_identity(context.font)
17
- else
18
- sfnt_identity(context.font)
19
- end
20
- end
21
-
22
- private
23
-
24
- def sfnt_identity(font)
25
- name_table = font.table(Constants::NAME_TAG) if font.has_table?(Constants::NAME_TAG)
26
- head_table = font.table(Constants::HEAD_TAG) if font.has_table?(Constants::HEAD_TAG)
27
-
28
- {
29
- family_name: name_table&.english_name(Tables::Name::FAMILY),
30
- subfamily_name: name_table&.english_name(Tables::Name::SUBFAMILY),
31
- full_name: name_table&.english_name(Tables::Name::FULL_NAME),
32
- postscript_name: name_table&.english_name(Tables::Name::POSTSCRIPT_NAME),
33
- version: name_table&.english_name(Tables::Name::VERSION),
34
- font_revision: head_table&.font_revision,
35
- }
36
- end
37
-
38
- def type1_identity(font)
39
- font_info = font.font_dictionary&.font_info
40
- {
41
- family_name: font_info&.family_name,
42
- subfamily_name: nil,
43
- full_name: font_info&.full_name,
44
- postscript_name: font.font_name,
45
- version: font_info&.version,
46
- font_revision: nil,
47
- }
48
- end
49
- end
50
- end
51
- end
52
- end
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fontisan
4
- module Audit
5
- module Extractors
6
- # Per-language CLDR coverage for one face.
7
- #
8
- # Returned fields:
9
- # language_coverage, cldr_version
10
- #
11
- # Opt-in only — `--with-language-coverage`. When off, Context#cldr
12
- # returns nil and this extractor emits an empty array + nil version.
13
- # MECE: this extractor is CLDR-driven; UCD block/script coverage
14
- # lives in {Extractors::Aggregations}.
15
- class LanguageCoverage < Base
16
- def extract(context)
17
- cldr = context.cldr
18
- return empty(nil) if cldr.nil?
19
-
20
- return empty(cldr[:version]) if cldr[:index].nil?
21
-
22
- {
23
- language_coverage: Cldr::Aggregator.aggregate(context.codepoints,
24
- cldr[:index]),
25
- cldr_version: cldr[:version],
26
- }
27
- end
28
-
29
- private
30
-
31
- def empty(version)
32
- { language_coverage: [], cldr_version: version }
33
- end
34
- end
35
- end
36
- end
37
- end