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.
- checksums.yaml +4 -4
- data/.rubocop.yml +6 -0
- data/.rubocop_todo.yml +93 -17
- data/CHANGELOG.md +12 -2
- data/README.adoc +6 -210
- data/fontisan.gemspec +48 -0
- data/lib/fontisan/cldr/unicode_set_parser.rb +23 -6
- data/lib/fontisan/cldr/version_resolver.rb +1 -1
- data/lib/fontisan/cli.rb +4 -168
- data/lib/fontisan/commands.rb +0 -3
- data/lib/fontisan/formatters/text_formatter.rb +0 -6
- data/lib/fontisan/formatters.rb +0 -3
- data/lib/fontisan/hints.rb +6 -3
- data/lib/fontisan/models.rb +4 -4
- data/lib/fontisan/pipeline/strategies.rb +4 -2
- data/lib/fontisan/pipeline.rb +2 -1
- data/lib/fontisan/stitcher/selector/codepoints.rb +29 -0
- data/lib/fontisan/stitcher/selector/gid.rb +25 -0
- data/lib/fontisan/stitcher/selector/range.rb +30 -0
- data/lib/fontisan/stitcher/selector.rb +26 -0
- data/lib/fontisan/stitcher/source.rb +97 -0
- data/lib/fontisan/stitcher.rb +182 -0
- data/lib/fontisan/stitcher_cli.rb +69 -0
- data/lib/fontisan/tables/cff.rb +2 -1
- data/lib/fontisan/tables.rb +2 -1
- data/lib/fontisan/ufo/anchor.rb +17 -0
- data/lib/fontisan/ufo/cli.rb +85 -0
- data/lib/fontisan/ufo/compile/base_compiler.rb +81 -0
- data/lib/fontisan/ufo/compile/cff.rb +224 -0
- data/lib/fontisan/ufo/compile/cmap.rb +129 -0
- data/lib/fontisan/ufo/compile/filters/cubic_to_quadratic.rb +174 -0
- data/lib/fontisan/ufo/compile/filters/decompose_components.rb +33 -0
- data/lib/fontisan/ufo/compile/filters/flatten_components.rb +22 -0
- data/lib/fontisan/ufo/compile/filters/reverse_contour_direction.rb +27 -0
- data/lib/fontisan/ufo/compile/filters.rb +57 -0
- data/lib/fontisan/ufo/compile/glyf_loca.rb +145 -0
- data/lib/fontisan/ufo/compile/head.rb +98 -0
- data/lib/fontisan/ufo/compile/hhea.rb +36 -0
- data/lib/fontisan/ufo/compile/hmtx.rb +27 -0
- data/lib/fontisan/ufo/compile/maxp.rb +57 -0
- data/lib/fontisan/ufo/compile/name.rb +79 -0
- data/lib/fontisan/ufo/compile/os2.rb +81 -0
- data/lib/fontisan/ufo/compile/otf_compiler.rb +43 -0
- data/lib/fontisan/ufo/compile/post.rb +32 -0
- data/lib/fontisan/ufo/compile/ttf_compiler.rb +69 -0
- data/lib/fontisan/ufo/compile.rb +48 -0
- data/lib/fontisan/ufo/component.rb +18 -0
- data/lib/fontisan/ufo/contour.rb +29 -0
- data/lib/fontisan/ufo/convert/from_bin_data.rb +246 -0
- data/lib/fontisan/ufo/convert.rb +18 -0
- data/lib/fontisan/ufo/data_set.rb +21 -0
- data/lib/fontisan/ufo/features.rb +17 -0
- data/lib/fontisan/ufo/font.rb +61 -0
- data/lib/fontisan/ufo/glyph.rb +421 -0
- data/lib/fontisan/ufo/guideline.rb +19 -0
- data/lib/fontisan/ufo/image.rb +16 -0
- data/lib/fontisan/ufo/image_set.rb +19 -0
- data/lib/fontisan/ufo/info.rb +79 -0
- data/lib/fontisan/ufo/kerning.rb +32 -0
- data/lib/fontisan/ufo/layer.rb +38 -0
- data/lib/fontisan/ufo/layer_set.rb +37 -0
- data/lib/fontisan/ufo/lib.rb +24 -0
- data/lib/fontisan/ufo/plist.rb +118 -0
- data/lib/fontisan/ufo/point.rb +38 -0
- data/lib/fontisan/ufo/reader.rb +144 -0
- data/lib/fontisan/ufo/transformation.rb +39 -0
- data/lib/fontisan/ufo/writer.rb +115 -0
- data/lib/fontisan/ufo.rb +44 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +3 -3
- metadata +57 -70
- data/lib/fontisan/audit/codepoint_range_coalescer.rb +0 -41
- data/lib/fontisan/audit/context.rb +0 -122
- data/lib/fontisan/audit/differ.rb +0 -124
- data/lib/fontisan/audit/extractors/aggregations.rb +0 -54
- data/lib/fontisan/audit/extractors/base.rb +0 -26
- data/lib/fontisan/audit/extractors/color_capabilities.rb +0 -141
- data/lib/fontisan/audit/extractors/coverage.rb +0 -48
- data/lib/fontisan/audit/extractors/hinting.rb +0 -197
- data/lib/fontisan/audit/extractors/identity.rb +0 -52
- data/lib/fontisan/audit/extractors/language_coverage.rb +0 -37
- data/lib/fontisan/audit/extractors/licensing.rb +0 -79
- data/lib/fontisan/audit/extractors/metrics.rb +0 -103
- data/lib/fontisan/audit/extractors/opentype_layout.rb +0 -69
- data/lib/fontisan/audit/extractors/provenance.rb +0 -29
- data/lib/fontisan/audit/extractors/style.rb +0 -32
- data/lib/fontisan/audit/extractors/variation_detail.rb +0 -99
- data/lib/fontisan/audit/extractors.rb +0 -27
- data/lib/fontisan/audit/library_aggregator.rb +0 -83
- data/lib/fontisan/audit/library_auditor.rb +0 -90
- data/lib/fontisan/audit/registry.rb +0 -60
- data/lib/fontisan/audit/style_extractor.rb +0 -80
- data/lib/fontisan/audit.rb +0 -20
- data/lib/fontisan/cli/ucd_cli.rb +0 -97
- data/lib/fontisan/commands/audit_command.rb +0 -123
- data/lib/fontisan/commands/audit_compare_command.rb +0 -66
- data/lib/fontisan/commands/audit_library_command.rb +0 -46
- data/lib/fontisan/config/ucd.yml +0 -23
- data/lib/fontisan/formatters/audit_diff_text_renderer.rb +0 -122
- data/lib/fontisan/formatters/audit_text_renderer.rb +0 -324
- data/lib/fontisan/formatters/library_summary_text_renderer.rb +0 -99
- data/lib/fontisan/models/audit/audit_axis.rb +0 -30
- data/lib/fontisan/models/audit/audit_block.rb +0 -32
- data/lib/fontisan/models/audit/audit_diff.rb +0 -77
- data/lib/fontisan/models/audit/audit_report.rb +0 -153
- data/lib/fontisan/models/audit/codepoint_range.rb +0 -40
- data/lib/fontisan/models/audit/codepoint_set_diff.rb +0 -34
- data/lib/fontisan/models/audit/color_capabilities.rb +0 -93
- data/lib/fontisan/models/audit/duplicate_group.rb +0 -23
- data/lib/fontisan/models/audit/embedding_type.rb +0 -76
- data/lib/fontisan/models/audit/field_change.rb +0 -28
- data/lib/fontisan/models/audit/fs_selection_flags.rb +0 -61
- data/lib/fontisan/models/audit/gasp_range.rb +0 -63
- data/lib/fontisan/models/audit/hinting.rb +0 -93
- data/lib/fontisan/models/audit/library_summary.rb +0 -40
- data/lib/fontisan/models/audit/licensing.rb +0 -48
- data/lib/fontisan/models/audit/metrics.rb +0 -111
- data/lib/fontisan/models/audit/named_instance.rb +0 -41
- data/lib/fontisan/models/audit/opentype_layout.rb +0 -40
- data/lib/fontisan/models/audit/script_coverage_row.rb +0 -26
- data/lib/fontisan/models/audit/script_features.rb +0 -28
- data/lib/fontisan/models/audit/variation_detail.rb +0 -44
- data/lib/fontisan/models/audit.rb +0 -33
- data/lib/fontisan/models/ucd/ucd.rb +0 -38
- data/lib/fontisan/models/ucd/ucd_char.rb +0 -67
- data/lib/fontisan/models/ucd.rb +0 -19
- data/lib/fontisan/ucd/aggregator.rb +0 -73
- data/lib/fontisan/ucd/cache_manager.rb +0 -111
- data/lib/fontisan/ucd/config.rb +0 -59
- data/lib/fontisan/ucd/download_error.rb +0 -9
- data/lib/fontisan/ucd/downloader.rb +0 -88
- data/lib/fontisan/ucd/error.rb +0 -8
- data/lib/fontisan/ucd/index.rb +0 -103
- data/lib/fontisan/ucd/index_builder.rb +0 -107
- data/lib/fontisan/ucd/range_entry.rb +0 -56
- data/lib/fontisan/ucd/unknown_version_error.rb +0 -9
- data/lib/fontisan/ucd/version_resolver.rb +0 -79
- 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
|
data/lib/fontisan/audit.rb
DELETED
|
@@ -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
|
data/lib/fontisan/cli/ucd_cli.rb
DELETED
|
@@ -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
|