fontisan 0.2.17 → 0.2.22
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_todo.yml +14 -90
- data/README.adoc +257 -1
- data/docs/.vitepress/config.ts +68 -8
- data/docs/.vitepress/theme/style.css +570 -272
- data/docs/CONVERSION_GUIDE.adoc +31 -8
- data/docs/EXTRACT_TTC_MIGRATION.md +1 -1
- data/docs/WOFF_WOFF2_FORMATS.adoc +53 -0
- data/docs/api/conversion-options.md +37 -14
- data/docs/cli/audit.md +337 -0
- data/docs/cli/convert.md +20 -1
- data/docs/cli/index.md +31 -0
- data/docs/guide/color.md +1 -1
- data/docs/guide/conversion/options.md +32 -3
- data/docs/guide/conversion/ttf-otf.md +1 -1
- data/docs/guide/conversion/type1.md +1 -1
- data/docs/guide/conversion/web.md +91 -32
- data/docs/guide/conversion.md +6 -5
- data/docs/guide/formats/woff.md +35 -11
- data/docs/guide/index.md +2 -2
- data/docs/guide/migrations/extract-ttc.md +1 -1
- data/docs/guide/quick-start.md +4 -4
- data/docs/guide/type1.md +4 -4
- data/docs/guide/woff.md +19 -17
- data/docs/index.md +2 -0
- data/docs/lychee.toml +5 -1
- data/docs/package.json +1 -1
- data/docs/public/robots.txt +4 -0
- data/docs/scripts/post-build.mjs +81 -0
- data/lib/fontisan/audit/codepoint_range_coalescer.rb +41 -0
- data/lib/fontisan/audit/context.rb +122 -0
- data/lib/fontisan/audit/differ.rb +124 -0
- data/lib/fontisan/audit/extractors/aggregations.rb +54 -0
- data/lib/fontisan/audit/extractors/base.rb +26 -0
- data/lib/fontisan/audit/extractors/color_capabilities.rb +141 -0
- data/lib/fontisan/audit/extractors/coverage.rb +48 -0
- data/lib/fontisan/audit/extractors/hinting.rb +197 -0
- data/lib/fontisan/audit/extractors/identity.rb +52 -0
- data/lib/fontisan/audit/extractors/language_coverage.rb +37 -0
- data/lib/fontisan/audit/extractors/licensing.rb +79 -0
- data/lib/fontisan/audit/extractors/metrics.rb +103 -0
- data/lib/fontisan/audit/extractors/opentype_layout.rb +69 -0
- data/lib/fontisan/audit/extractors/provenance.rb +29 -0
- data/lib/fontisan/audit/extractors/style.rb +32 -0
- data/lib/fontisan/audit/extractors/variation_detail.rb +99 -0
- data/lib/fontisan/audit/extractors.rb +27 -0
- data/lib/fontisan/audit/library_aggregator.rb +83 -0
- data/lib/fontisan/audit/library_auditor.rb +90 -0
- data/lib/fontisan/audit/registry.rb +60 -0
- data/lib/fontisan/audit/style_extractor.rb +80 -0
- data/lib/fontisan/audit.rb +20 -0
- data/lib/fontisan/base_collection.rb +23 -9
- data/lib/fontisan/binary/structures.rb +0 -2
- data/lib/fontisan/binary.rb +11 -0
- data/lib/fontisan/cldr/aggregator.rb +33 -0
- data/lib/fontisan/cldr/cache_manager.rb +110 -0
- data/lib/fontisan/cldr/config.rb +59 -0
- data/lib/fontisan/cldr/download_error.rb +9 -0
- data/lib/fontisan/cldr/downloader.rb +79 -0
- data/lib/fontisan/cldr/error.rb +8 -0
- data/lib/fontisan/cldr/index.rb +64 -0
- data/lib/fontisan/cldr/index_builder.rb +72 -0
- data/lib/fontisan/cldr/unicode_set_parser.rb +172 -0
- data/lib/fontisan/cldr/unknown_version_error.rb +9 -0
- data/lib/fontisan/cldr/version_resolver.rb +91 -0
- data/lib/fontisan/cldr.rb +23 -0
- data/lib/fontisan/cli/cldr_cli.rb +85 -0
- data/lib/fontisan/cli/ucd_cli.rb +97 -0
- data/lib/fontisan/cli.rb +201 -2
- data/lib/fontisan/collection/builder.rb +0 -4
- data/lib/fontisan/collection/dfont_builder.rb +0 -4
- data/lib/fontisan/collection/shared_logic.rb +0 -2
- data/lib/fontisan/collection/writer.rb +0 -3
- data/lib/fontisan/collection.rb +15 -0
- data/lib/fontisan/commands/audit_command.rb +123 -0
- data/lib/fontisan/commands/audit_compare_command.rb +66 -0
- data/lib/fontisan/commands/audit_library_command.rb +46 -0
- data/lib/fontisan/commands/base_command.rb +0 -3
- data/lib/fontisan/commands/convert_command.rb +25 -20
- data/lib/fontisan/commands/dump_table_command.rb +0 -3
- data/lib/fontisan/commands/export_command.rb +0 -4
- data/lib/fontisan/commands/features_command.rb +0 -3
- data/lib/fontisan/commands/instance_command.rb +0 -5
- data/lib/fontisan/commands/ls_command.rb +0 -6
- data/lib/fontisan/commands/optical_size_command.rb +0 -3
- data/lib/fontisan/commands/pack_command.rb +0 -5
- data/lib/fontisan/commands/scripts_command.rb +0 -2
- data/lib/fontisan/commands/subset_command.rb +0 -3
- data/lib/fontisan/commands/unicode_command.rb +0 -3
- data/lib/fontisan/commands/unpack_command.rb +0 -7
- data/lib/fontisan/commands/validate_command.rb +0 -8
- data/lib/fontisan/commands/variable_command.rb +0 -3
- data/lib/fontisan/commands.rb +29 -0
- data/lib/fontisan/config/cldr.yml +22 -0
- data/lib/fontisan/config/conversion_matrix.yml +38 -0
- data/lib/fontisan/config/ucd.yml +23 -0
- data/lib/fontisan/constants.rb +19 -0
- data/lib/fontisan/conversion_options.rb +30 -19
- data/lib/fontisan/converters/cff_table_builder.rb +0 -3
- data/lib/fontisan/converters/collection_converter.rb +0 -8
- data/lib/fontisan/converters/conversion_strategy.rb +161 -46
- data/lib/fontisan/converters/format_converter.rb +143 -32
- data/lib/fontisan/converters/glyf_table_builder.rb +0 -2
- data/lib/fontisan/converters/outline_converter.rb +0 -19
- data/lib/fontisan/converters/outline_extraction.rb +0 -5
- data/lib/fontisan/converters/outline_optimizer.rb +0 -5
- data/lib/fontisan/converters/svg_generator.rb +0 -4
- data/lib/fontisan/converters/table_copier.rb +0 -2
- data/lib/fontisan/converters/type1_converter.rb +0 -11
- data/lib/fontisan/converters/woff2_encoder.rb +49 -20
- data/lib/fontisan/converters/woff_writer.rb +211 -282
- data/lib/fontisan/converters.rb +21 -0
- data/lib/fontisan/dfont_collection.rb +29 -10
- data/lib/fontisan/export/exporter.rb +0 -6
- data/lib/fontisan/export/transformers/font_to_ttx.rb +0 -9
- data/lib/fontisan/export/transformers/head_transformer.rb +0 -2
- data/lib/fontisan/export/transformers/hhea_transformer.rb +0 -2
- data/lib/fontisan/export/transformers/maxp_transformer.rb +0 -2
- data/lib/fontisan/export/transformers/name_transformer.rb +0 -2
- data/lib/fontisan/export/transformers/os2_transformer.rb +0 -2
- data/lib/fontisan/export/transformers/post_transformer.rb +0 -2
- data/lib/fontisan/export/transformers.rb +17 -0
- data/lib/fontisan/export.rb +13 -0
- data/lib/fontisan/font_loader.rb +14 -19
- data/lib/fontisan/font_writer.rb +0 -1
- data/lib/fontisan/formatters/audit_diff_text_renderer.rb +122 -0
- data/lib/fontisan/formatters/audit_text_renderer.rb +324 -0
- data/lib/fontisan/formatters/library_summary_text_renderer.rb +99 -0
- data/lib/fontisan/formatters/text_formatter.rb +6 -0
- data/lib/fontisan/formatters.rb +12 -0
- data/lib/fontisan/hints/hint_converter.rb +0 -1
- data/lib/fontisan/hints/postscript_hint_applier.rb +0 -9
- data/lib/fontisan/hints/postscript_hint_extractor.rb +0 -2
- data/lib/fontisan/hints/truetype_hint_extractor.rb +0 -2
- data/lib/fontisan/hints.rb +16 -0
- data/lib/fontisan/metrics_calculator.rb +0 -2
- data/lib/fontisan/models/all_scripts_features_info.rb +0 -1
- data/lib/fontisan/models/audit/audit_axis.rb +30 -0
- data/lib/fontisan/models/audit/audit_block.rb +32 -0
- data/lib/fontisan/models/audit/audit_diff.rb +77 -0
- data/lib/fontisan/models/audit/audit_report.rb +153 -0
- data/lib/fontisan/models/audit/codepoint_range.rb +40 -0
- data/lib/fontisan/models/audit/codepoint_set_diff.rb +34 -0
- data/lib/fontisan/models/audit/color_capabilities.rb +93 -0
- data/lib/fontisan/models/audit/duplicate_group.rb +23 -0
- data/lib/fontisan/models/audit/embedding_type.rb +76 -0
- data/lib/fontisan/models/audit/field_change.rb +28 -0
- data/lib/fontisan/models/audit/fs_selection_flags.rb +61 -0
- data/lib/fontisan/models/audit/gasp_range.rb +63 -0
- data/lib/fontisan/models/audit/hinting.rb +93 -0
- data/lib/fontisan/models/audit/library_summary.rb +40 -0
- data/lib/fontisan/models/audit/licensing.rb +48 -0
- data/lib/fontisan/models/audit/metrics.rb +111 -0
- data/lib/fontisan/models/audit/named_instance.rb +41 -0
- data/lib/fontisan/models/audit/opentype_layout.rb +40 -0
- data/lib/fontisan/models/audit/script_coverage_row.rb +26 -0
- data/lib/fontisan/models/audit/script_features.rb +28 -0
- data/lib/fontisan/models/audit/variation_detail.rb +44 -0
- data/lib/fontisan/models/audit.rb +33 -0
- data/lib/fontisan/models/cldr/language_coverage.rb +31 -0
- data/lib/fontisan/models/cldr.rb +12 -0
- data/lib/fontisan/models/collection_brief_info.rb +0 -1
- data/lib/fontisan/models/collection_info.rb +0 -2
- data/lib/fontisan/models/collection_list_info.rb +0 -1
- data/lib/fontisan/models/collection_validation_report.rb +0 -2
- data/lib/fontisan/models/color_glyph.rb +0 -1
- data/lib/fontisan/models/font_report.rb +0 -1
- data/lib/fontisan/models/ttx/tables.rb +21 -0
- data/lib/fontisan/models/ttx/ttfont.rb +0 -8
- data/lib/fontisan/models/ttx.rb +14 -0
- data/lib/fontisan/models/ucd/ucd.rb +38 -0
- data/lib/fontisan/models/ucd/ucd_char.rb +67 -0
- data/lib/fontisan/models/ucd.rb +19 -0
- data/lib/fontisan/models.rb +47 -0
- data/lib/fontisan/open_type_collection.rb +6 -5
- data/lib/fontisan/open_type_font.rb +8 -2
- data/lib/fontisan/open_type_font_extensions.rb +9 -9
- data/lib/fontisan/optimizers/pattern_analyzer.rb +0 -1
- data/lib/fontisan/optimizers.rb +14 -0
- data/lib/fontisan/outline_extractor.rb +0 -2
- data/lib/fontisan/parsers/dfont_parser.rb +0 -1
- data/lib/fontisan/parsers.rb +10 -0
- data/lib/fontisan/pipeline/format_detector.rb +29 -102
- data/lib/fontisan/pipeline/output_writer.rb +11 -9
- data/lib/fontisan/pipeline/strategies/instance_strategy.rb +0 -4
- data/lib/fontisan/pipeline/strategies/named_strategy.rb +0 -4
- data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +0 -2
- data/lib/fontisan/pipeline/strategies.rb +14 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +0 -7
- data/lib/fontisan/pipeline/variation_resolver.rb +0 -7
- data/lib/fontisan/pipeline.rb +13 -0
- data/lib/fontisan/sfnt_font.rb +29 -14
- data/lib/fontisan/sfnt_table.rb +0 -4
- data/lib/fontisan/subset/builder.rb +0 -6
- data/lib/fontisan/subset.rb +13 -0
- data/lib/fontisan/svg/font_generator.rb +0 -4
- data/lib/fontisan/svg/glyph_generator.rb +0 -2
- data/lib/fontisan/svg.rb +12 -0
- data/lib/fontisan/tables/cbdt.rb +0 -1
- data/lib/fontisan/tables/cblc.rb +0 -1
- data/lib/fontisan/tables/cff/charset.rb +0 -1
- data/lib/fontisan/tables/cff/charstring.rb +0 -1
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +0 -4
- data/lib/fontisan/tables/cff/charstrings_index.rb +0 -3
- data/lib/fontisan/tables/cff/dict.rb +0 -1
- data/lib/fontisan/tables/cff/encoding.rb +0 -1
- data/lib/fontisan/tables/cff/header.rb +0 -2
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +0 -2
- data/lib/fontisan/tables/cff/index.rb +0 -1
- data/lib/fontisan/tables/cff/private_dict.rb +0 -2
- data/lib/fontisan/tables/cff/private_dict_writer.rb +0 -2
- data/lib/fontisan/tables/cff/table_builder.rb +0 -6
- data/lib/fontisan/tables/cff/top_dict.rb +0 -2
- data/lib/fontisan/tables/cff.rb +22 -15
- data/lib/fontisan/tables/cff2/charstring_parser.rb +0 -2
- data/lib/fontisan/tables/cff2/table_builder.rb +0 -11
- data/lib/fontisan/tables/cff2/table_reader.rb +0 -2
- data/lib/fontisan/tables/cff2.rb +13 -14
- data/lib/fontisan/tables/cmap.rb +24 -2
- data/lib/fontisan/tables/cmap_table.rb +0 -3
- data/lib/fontisan/tables/colr.rb +0 -1
- data/lib/fontisan/tables/cpal.rb +0 -1
- data/lib/fontisan/tables/cvar.rb +0 -2
- data/lib/fontisan/tables/fvar.rb +0 -1
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +0 -2
- data/lib/fontisan/tables/glyf/glyph_builder.rb +0 -3
- data/lib/fontisan/tables/glyf.rb +0 -6
- data/lib/fontisan/tables/glyf_table.rb +0 -3
- data/lib/fontisan/tables/gpos.rb +0 -2
- data/lib/fontisan/tables/gsub.rb +0 -2
- data/lib/fontisan/tables/gvar.rb +0 -2
- data/lib/fontisan/tables/head.rb +0 -2
- data/lib/fontisan/tables/head_table.rb +0 -3
- data/lib/fontisan/tables/hhea.rb +0 -2
- data/lib/fontisan/tables/hhea_table.rb +0 -3
- data/lib/fontisan/tables/hmtx.rb +0 -2
- data/lib/fontisan/tables/hmtx_table.rb +0 -3
- data/lib/fontisan/tables/hvar.rb +0 -3
- data/lib/fontisan/tables/loca.rb +0 -2
- data/lib/fontisan/tables/loca_table.rb +0 -3
- data/lib/fontisan/tables/maxp.rb +0 -2
- data/lib/fontisan/tables/maxp_table.rb +0 -3
- data/lib/fontisan/tables/mvar.rb +0 -3
- data/lib/fontisan/tables/name.rb +0 -2
- data/lib/fontisan/tables/name_table.rb +0 -3
- data/lib/fontisan/tables/os2_table.rb +0 -3
- data/lib/fontisan/tables/post_table.rb +0 -3
- data/lib/fontisan/tables/sbix.rb +0 -1
- data/lib/fontisan/tables/svg.rb +0 -1
- data/lib/fontisan/tables/variation_common.rb +0 -1
- data/lib/fontisan/tables/vvar.rb +0 -3
- data/lib/fontisan/tables.rb +54 -0
- data/lib/fontisan/true_type_collection.rb +6 -14
- data/lib/fontisan/true_type_font.rb +8 -2
- data/lib/fontisan/true_type_font_extensions.rb +9 -9
- data/lib/fontisan/type1/afm_generator.rb +0 -4
- data/lib/fontisan/type1/conversion_options.rb +0 -2
- data/lib/fontisan/type1/encodings.rb +0 -2
- data/lib/fontisan/type1/generator.rb +0 -8
- data/lib/fontisan/type1/pfa_generator.rb +0 -3
- data/lib/fontisan/type1/pfb_generator.rb +0 -5
- data/lib/fontisan/type1/pfm_generator.rb +0 -4
- data/lib/fontisan/type1.rb +42 -69
- data/lib/fontisan/type1_font.rb +40 -11
- data/lib/fontisan/ucd/aggregator.rb +73 -0
- data/lib/fontisan/ucd/cache_manager.rb +111 -0
- data/lib/fontisan/ucd/config.rb +59 -0
- data/lib/fontisan/ucd/download_error.rb +9 -0
- data/lib/fontisan/ucd/downloader.rb +88 -0
- data/lib/fontisan/ucd/error.rb +8 -0
- data/lib/fontisan/ucd/index.rb +103 -0
- data/lib/fontisan/ucd/index_builder.rb +107 -0
- data/lib/fontisan/ucd/range_entry.rb +56 -0
- data/lib/fontisan/ucd/unknown_version_error.rb +9 -0
- data/lib/fontisan/ucd/version_resolver.rb +79 -0
- data/lib/fontisan/ucd.rb +23 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +0 -1
- data/lib/fontisan/utilities.rb +10 -0
- data/lib/fontisan/utils.rb +10 -0
- data/lib/fontisan/validation/collection_validator.rb +0 -2
- data/lib/fontisan/validation.rb +9 -0
- data/lib/fontisan/validators/basic_validator.rb +0 -2
- data/lib/fontisan/validators/font_book_validator.rb +0 -2
- data/lib/fontisan/validators/opentype_validator.rb +0 -2
- data/lib/fontisan/validators/profile_loader.rb +0 -5
- data/lib/fontisan/validators/validator.rb +0 -2
- data/lib/fontisan/validators/web_font_validator.rb +0 -2
- data/lib/fontisan/validators.rb +14 -0
- data/lib/fontisan/variable/delta_applicator.rb +0 -4
- data/lib/fontisan/variable/instancer.rb +0 -3
- data/lib/fontisan/variable/static_font_builder.rb +0 -3
- data/lib/fontisan/variable.rb +16 -0
- data/lib/fontisan/variation/blend_applier.rb +0 -2
- data/lib/fontisan/variation/cache.rb +0 -2
- data/lib/fontisan/variation/converter.rb +0 -3
- data/lib/fontisan/variation/data_extractor.rb +0 -2
- data/lib/fontisan/variation/delta_applier.rb +0 -5
- data/lib/fontisan/variation/inspector.rb +0 -1
- data/lib/fontisan/variation/instance_generator.rb +0 -6
- data/lib/fontisan/variation/instance_writer.rb +0 -5
- data/lib/fontisan/variation/metrics_adjuster.rb +0 -4
- data/lib/fontisan/variation/optimizer.rb +0 -3
- data/lib/fontisan/variation/parallel_generator.rb +0 -3
- data/lib/fontisan/variation/subsetter.rb +0 -4
- data/lib/fontisan/variation/tuple_variation_header.rb +0 -2
- data/lib/fontisan/variation/variable_svg_generator.rb +0 -3
- data/lib/fontisan/variation/variation_context.rb +0 -3
- data/lib/fontisan/variation/variation_preserver.rb +0 -3
- data/lib/fontisan/variation.rb +31 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2.rb +13 -0
- data/lib/fontisan/woff2_font.rb +31 -9
- data/lib/fontisan/woff_font.rb +31 -2
- data/lib/fontisan.rb +124 -196
- metadata +114 -7
- data/fontisan.gemspec +0 -48
|
@@ -0,0 +1,197 @@
|
|
|
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
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Audit
|
|
5
|
+
module Extractors
|
|
6
|
+
# Licensing + embedding permissions + vendor provenance.
|
|
7
|
+
#
|
|
8
|
+
# Returned fields:
|
|
9
|
+
# licensing: Models::Audit::Licensing instance, or nil for Type 1
|
|
10
|
+
#
|
|
11
|
+
# Type 1 fonts have no OS/2 table; their licensing is nil. WOFF/
|
|
12
|
+
# WOFF2 carry the same OS/2 + name tables as TTF/OTF and need no
|
|
13
|
+
# special handling.
|
|
14
|
+
class Licensing < Base
|
|
15
|
+
# nameID → AuditReport field name, per OpenType name table spec.
|
|
16
|
+
NAME_IDS = {
|
|
17
|
+
copyright: 0,
|
|
18
|
+
trademark: 7,
|
|
19
|
+
manufacturer: 8,
|
|
20
|
+
designer: 9,
|
|
21
|
+
description: 10,
|
|
22
|
+
vendor_url: 11,
|
|
23
|
+
designer_url: 12,
|
|
24
|
+
license_description: 13,
|
|
25
|
+
license_url: 14,
|
|
26
|
+
}.freeze
|
|
27
|
+
private_constant :NAME_IDS
|
|
28
|
+
|
|
29
|
+
def extract(context)
|
|
30
|
+
font = context.font
|
|
31
|
+
return { licensing: nil } unless sfnt?(font)
|
|
32
|
+
|
|
33
|
+
os2 = os2_for(font)
|
|
34
|
+
name = name_table_for(font)
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
licensing: Models::Audit::Licensing.new(
|
|
38
|
+
**name_fields(name),
|
|
39
|
+
vendor_id: sanitized_vendor_id(os2),
|
|
40
|
+
embedding_type: Models::Audit::EmbeddingType.decode(os2&.fs_type&.to_i),
|
|
41
|
+
fs_selection_flags: Models::Audit::FsSelectionFlags.decode(os2&.fs_selection&.to_i),
|
|
42
|
+
),
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def sfnt?(font)
|
|
49
|
+
font.is_a?(SfntFont)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def os2_for(font)
|
|
53
|
+
return nil unless font.has_table?(Constants::OS2_TAG)
|
|
54
|
+
|
|
55
|
+
font.table(Constants::OS2_TAG)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def name_table_for(font)
|
|
59
|
+
return nil unless font.has_table?(Constants::NAME_TAG)
|
|
60
|
+
|
|
61
|
+
font.table(Constants::NAME_TAG)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def name_fields(name)
|
|
65
|
+
return {} unless name
|
|
66
|
+
|
|
67
|
+
NAME_IDS.transform_values { |id| name.english_name(id) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def sanitized_vendor_id(os2)
|
|
71
|
+
raw = os2&.ach_vend_id
|
|
72
|
+
return nil if raw.nil?
|
|
73
|
+
|
|
74
|
+
raw.gsub(/[\x00\s]+$/, "")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Audit
|
|
5
|
+
module Extractors
|
|
6
|
+
# Layout-critical metrics consolidated from head, hhea, OS/2, post.
|
|
7
|
+
#
|
|
8
|
+
# Returned fields:
|
|
9
|
+
# metrics: Models::Audit::Metrics instance, or nil for Type 1
|
|
10
|
+
#
|
|
11
|
+
# All table reads are nil-safe; tables may be absent in stripped
|
|
12
|
+
# WOFF builds or legacy formats.
|
|
13
|
+
class Metrics < Base
|
|
14
|
+
def extract(context)
|
|
15
|
+
font = context.font
|
|
16
|
+
return { metrics: nil } unless sfnt?(font)
|
|
17
|
+
|
|
18
|
+
{ metrics: Models::Audit::Metrics.new(**gather(font)) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def sfnt?(font)
|
|
24
|
+
font.is_a?(SfntFont)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def gather(font)
|
|
28
|
+
{}.tap do |h|
|
|
29
|
+
h.merge!(head_fields(font))
|
|
30
|
+
h.merge!(hhea_fields(font))
|
|
31
|
+
h.merge!(os2_fields(font))
|
|
32
|
+
h.merge!(post_fields(font))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def head_fields(font)
|
|
37
|
+
head = table(font, Constants::HEAD_TAG)
|
|
38
|
+
return {} unless head
|
|
39
|
+
|
|
40
|
+
{
|
|
41
|
+
units_per_em: head.units_per_em&.to_i,
|
|
42
|
+
bbox_x_min: head.x_min&.to_i,
|
|
43
|
+
bbox_y_min: head.y_min&.to_i,
|
|
44
|
+
bbox_x_max: head.x_max&.to_i,
|
|
45
|
+
bbox_y_max: head.y_max&.to_i,
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def hhea_fields(font)
|
|
50
|
+
hhea = table(font, Constants::HHEA_TAG)
|
|
51
|
+
return {} unless hhea
|
|
52
|
+
|
|
53
|
+
{
|
|
54
|
+
hhea_ascent: hhea.ascent&.to_i,
|
|
55
|
+
hhea_descent: hhea.descent&.to_i,
|
|
56
|
+
hhea_line_gap: hhea.line_gap&.to_i,
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def os2_fields(font)
|
|
61
|
+
os2 = table(font, Constants::OS2_TAG)
|
|
62
|
+
return {} unless os2
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
typo_ascender: os2.s_typo_ascender&.to_i,
|
|
66
|
+
typo_descender: os2.s_typo_descender&.to_i,
|
|
67
|
+
typo_line_gap: os2.s_typo_line_gap&.to_i,
|
|
68
|
+
win_ascent: os2.us_win_ascent&.to_i,
|
|
69
|
+
win_descent: os2.us_win_descent&.to_i,
|
|
70
|
+
x_height: os2.sx_height&.to_i,
|
|
71
|
+
cap_height: os2.s_cap_height&.to_i,
|
|
72
|
+
subscript_x_size: os2.y_subscript_x_size&.to_i,
|
|
73
|
+
subscript_y_size: os2.y_subscript_y_size&.to_i,
|
|
74
|
+
subscript_x_offset: os2.y_subscript_x_offset&.to_i,
|
|
75
|
+
subscript_y_offset: os2.y_subscript_y_offset&.to_i,
|
|
76
|
+
superscript_x_size: os2.y_superscript_x_size&.to_i,
|
|
77
|
+
superscript_y_size: os2.y_superscript_y_size&.to_i,
|
|
78
|
+
superscript_x_offset: os2.y_superscript_x_offset&.to_i,
|
|
79
|
+
superscript_y_offset: os2.y_superscript_y_offset&.to_i,
|
|
80
|
+
strikeout_size: os2.y_strikeout_size&.to_i,
|
|
81
|
+
strikeout_position: os2.y_strikeout_position&.to_i,
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def post_fields(font)
|
|
86
|
+
post = table(font, Constants::POST_TAG)
|
|
87
|
+
return {} unless post
|
|
88
|
+
|
|
89
|
+
{
|
|
90
|
+
underline_position: post.underline_position&.to_f,
|
|
91
|
+
underline_thickness: post.underline_thickness&.to_f,
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def table(font, tag)
|
|
96
|
+
return nil unless font.has_table?(tag)
|
|
97
|
+
|
|
98
|
+
font.table(tag)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|