fontisan 0.2.16 → 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.
Files changed (318) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +14 -90
  3. data/Gemfile +6 -3
  4. data/README.adoc +257 -1
  5. data/docs/.vitepress/config.ts +68 -8
  6. data/docs/.vitepress/theme/style.css +570 -272
  7. data/docs/CONVERSION_GUIDE.adoc +31 -8
  8. data/docs/EXTRACT_TTC_MIGRATION.md +1 -1
  9. data/docs/WOFF_WOFF2_FORMATS.adoc +53 -0
  10. data/docs/api/conversion-options.md +37 -14
  11. data/docs/api/font-loader.md +21 -15
  12. data/docs/cli/audit.md +337 -0
  13. data/docs/cli/convert.md +20 -1
  14. data/docs/cli/index.md +31 -0
  15. data/docs/guide/color.md +1 -1
  16. data/docs/guide/conversion/options.md +32 -3
  17. data/docs/guide/conversion/ttf-otf.md +1 -1
  18. data/docs/guide/conversion/type1.md +1 -1
  19. data/docs/guide/conversion/web.md +91 -32
  20. data/docs/guide/conversion.md +6 -5
  21. data/docs/guide/formats/woff.md +35 -11
  22. data/docs/guide/index.md +2 -2
  23. data/docs/guide/migrations/extract-ttc.md +1 -1
  24. data/docs/guide/quick-start.md +4 -4
  25. data/docs/guide/type1.md +4 -4
  26. data/docs/guide/woff.md +19 -17
  27. data/docs/index.md +2 -0
  28. data/docs/lychee.toml +5 -1
  29. data/docs/package.json +1 -1
  30. data/docs/public/robots.txt +4 -0
  31. data/docs/scripts/post-build.mjs +81 -0
  32. data/lib/fontisan/audit/codepoint_range_coalescer.rb +41 -0
  33. data/lib/fontisan/audit/context.rb +122 -0
  34. data/lib/fontisan/audit/differ.rb +124 -0
  35. data/lib/fontisan/audit/extractors/aggregations.rb +54 -0
  36. data/lib/fontisan/audit/extractors/base.rb +26 -0
  37. data/lib/fontisan/audit/extractors/color_capabilities.rb +141 -0
  38. data/lib/fontisan/audit/extractors/coverage.rb +48 -0
  39. data/lib/fontisan/audit/extractors/hinting.rb +197 -0
  40. data/lib/fontisan/audit/extractors/identity.rb +52 -0
  41. data/lib/fontisan/audit/extractors/language_coverage.rb +37 -0
  42. data/lib/fontisan/audit/extractors/licensing.rb +79 -0
  43. data/lib/fontisan/audit/extractors/metrics.rb +103 -0
  44. data/lib/fontisan/audit/extractors/opentype_layout.rb +69 -0
  45. data/lib/fontisan/audit/extractors/provenance.rb +29 -0
  46. data/lib/fontisan/audit/extractors/style.rb +32 -0
  47. data/lib/fontisan/audit/extractors/variation_detail.rb +99 -0
  48. data/lib/fontisan/audit/extractors.rb +27 -0
  49. data/lib/fontisan/audit/library_aggregator.rb +83 -0
  50. data/lib/fontisan/audit/library_auditor.rb +90 -0
  51. data/lib/fontisan/audit/registry.rb +60 -0
  52. data/lib/fontisan/audit/style_extractor.rb +80 -0
  53. data/lib/fontisan/audit.rb +20 -0
  54. data/lib/fontisan/base_collection.rb +23 -9
  55. data/lib/fontisan/binary/structures.rb +0 -2
  56. data/lib/fontisan/binary.rb +11 -0
  57. data/lib/fontisan/cldr/aggregator.rb +33 -0
  58. data/lib/fontisan/cldr/cache_manager.rb +110 -0
  59. data/lib/fontisan/cldr/config.rb +59 -0
  60. data/lib/fontisan/cldr/download_error.rb +9 -0
  61. data/lib/fontisan/cldr/downloader.rb +79 -0
  62. data/lib/fontisan/cldr/error.rb +8 -0
  63. data/lib/fontisan/cldr/index.rb +64 -0
  64. data/lib/fontisan/cldr/index_builder.rb +72 -0
  65. data/lib/fontisan/cldr/unicode_set_parser.rb +172 -0
  66. data/lib/fontisan/cldr/unknown_version_error.rb +9 -0
  67. data/lib/fontisan/cldr/version_resolver.rb +91 -0
  68. data/lib/fontisan/cldr.rb +23 -0
  69. data/lib/fontisan/cli/cldr_cli.rb +85 -0
  70. data/lib/fontisan/cli/ucd_cli.rb +97 -0
  71. data/lib/fontisan/cli.rb +201 -2
  72. data/lib/fontisan/collection/builder.rb +0 -4
  73. data/lib/fontisan/collection/dfont_builder.rb +0 -4
  74. data/lib/fontisan/collection/shared_logic.rb +0 -2
  75. data/lib/fontisan/collection/writer.rb +0 -3
  76. data/lib/fontisan/collection.rb +15 -0
  77. data/lib/fontisan/commands/audit_command.rb +123 -0
  78. data/lib/fontisan/commands/audit_compare_command.rb +66 -0
  79. data/lib/fontisan/commands/audit_library_command.rb +46 -0
  80. data/lib/fontisan/commands/base_command.rb +0 -3
  81. data/lib/fontisan/commands/convert_command.rb +25 -20
  82. data/lib/fontisan/commands/dump_table_command.rb +0 -3
  83. data/lib/fontisan/commands/export_command.rb +0 -4
  84. data/lib/fontisan/commands/features_command.rb +0 -3
  85. data/lib/fontisan/commands/instance_command.rb +0 -5
  86. data/lib/fontisan/commands/ls_command.rb +0 -6
  87. data/lib/fontisan/commands/optical_size_command.rb +0 -3
  88. data/lib/fontisan/commands/pack_command.rb +0 -5
  89. data/lib/fontisan/commands/scripts_command.rb +0 -2
  90. data/lib/fontisan/commands/subset_command.rb +0 -3
  91. data/lib/fontisan/commands/unicode_command.rb +0 -3
  92. data/lib/fontisan/commands/unpack_command.rb +0 -7
  93. data/lib/fontisan/commands/validate_command.rb +0 -8
  94. data/lib/fontisan/commands/variable_command.rb +0 -3
  95. data/lib/fontisan/commands.rb +29 -0
  96. data/lib/fontisan/config/cldr.yml +22 -0
  97. data/lib/fontisan/config/conversion_matrix.yml +38 -0
  98. data/lib/fontisan/config/ucd.yml +23 -0
  99. data/lib/fontisan/constants.rb +48 -6
  100. data/lib/fontisan/conversion_options.rb +30 -19
  101. data/lib/fontisan/converters/cff_table_builder.rb +0 -3
  102. data/lib/fontisan/converters/collection_converter.rb +0 -8
  103. data/lib/fontisan/converters/conversion_strategy.rb +161 -46
  104. data/lib/fontisan/converters/format_converter.rb +143 -32
  105. data/lib/fontisan/converters/glyf_table_builder.rb +0 -2
  106. data/lib/fontisan/converters/outline_converter.rb +0 -19
  107. data/lib/fontisan/converters/outline_extraction.rb +0 -5
  108. data/lib/fontisan/converters/outline_optimizer.rb +0 -5
  109. data/lib/fontisan/converters/svg_generator.rb +0 -4
  110. data/lib/fontisan/converters/table_copier.rb +0 -2
  111. data/lib/fontisan/converters/type1_converter.rb +0 -11
  112. data/lib/fontisan/converters/woff2_encoder.rb +49 -20
  113. data/lib/fontisan/converters/woff_writer.rb +211 -282
  114. data/lib/fontisan/converters.rb +21 -0
  115. data/lib/fontisan/dfont_collection.rb +29 -10
  116. data/lib/fontisan/export/exporter.rb +0 -6
  117. data/lib/fontisan/export/transformers/font_to_ttx.rb +0 -9
  118. data/lib/fontisan/export/transformers/head_transformer.rb +0 -2
  119. data/lib/fontisan/export/transformers/hhea_transformer.rb +0 -2
  120. data/lib/fontisan/export/transformers/maxp_transformer.rb +0 -2
  121. data/lib/fontisan/export/transformers/name_transformer.rb +0 -2
  122. data/lib/fontisan/export/transformers/os2_transformer.rb +0 -2
  123. data/lib/fontisan/export/transformers/post_transformer.rb +0 -2
  124. data/lib/fontisan/export/transformers.rb +17 -0
  125. data/lib/fontisan/export.rb +13 -0
  126. data/lib/fontisan/font_loader.rb +189 -328
  127. data/lib/fontisan/font_writer.rb +0 -1
  128. data/lib/fontisan/formatters/audit_diff_text_renderer.rb +122 -0
  129. data/lib/fontisan/formatters/audit_text_renderer.rb +324 -0
  130. data/lib/fontisan/formatters/library_summary_text_renderer.rb +99 -0
  131. data/lib/fontisan/formatters/text_formatter.rb +6 -0
  132. data/lib/fontisan/formatters.rb +12 -0
  133. data/lib/fontisan/hints/hint_converter.rb +0 -1
  134. data/lib/fontisan/hints/postscript_hint_applier.rb +0 -9
  135. data/lib/fontisan/hints/postscript_hint_extractor.rb +0 -2
  136. data/lib/fontisan/hints/truetype_hint_extractor.rb +0 -2
  137. data/lib/fontisan/hints.rb +16 -0
  138. data/lib/fontisan/metrics_calculator.rb +0 -2
  139. data/lib/fontisan/models/all_scripts_features_info.rb +0 -1
  140. data/lib/fontisan/models/audit/audit_axis.rb +30 -0
  141. data/lib/fontisan/models/audit/audit_block.rb +32 -0
  142. data/lib/fontisan/models/audit/audit_diff.rb +77 -0
  143. data/lib/fontisan/models/audit/audit_report.rb +153 -0
  144. data/lib/fontisan/models/audit/codepoint_range.rb +40 -0
  145. data/lib/fontisan/models/audit/codepoint_set_diff.rb +34 -0
  146. data/lib/fontisan/models/audit/color_capabilities.rb +93 -0
  147. data/lib/fontisan/models/audit/duplicate_group.rb +23 -0
  148. data/lib/fontisan/models/audit/embedding_type.rb +76 -0
  149. data/lib/fontisan/models/audit/field_change.rb +28 -0
  150. data/lib/fontisan/models/audit/fs_selection_flags.rb +61 -0
  151. data/lib/fontisan/models/audit/gasp_range.rb +63 -0
  152. data/lib/fontisan/models/audit/hinting.rb +93 -0
  153. data/lib/fontisan/models/audit/library_summary.rb +40 -0
  154. data/lib/fontisan/models/audit/licensing.rb +48 -0
  155. data/lib/fontisan/models/audit/metrics.rb +111 -0
  156. data/lib/fontisan/models/audit/named_instance.rb +41 -0
  157. data/lib/fontisan/models/audit/opentype_layout.rb +40 -0
  158. data/lib/fontisan/models/audit/script_coverage_row.rb +26 -0
  159. data/lib/fontisan/models/audit/script_features.rb +28 -0
  160. data/lib/fontisan/models/audit/variation_detail.rb +44 -0
  161. data/lib/fontisan/models/audit.rb +33 -0
  162. data/lib/fontisan/models/cldr/language_coverage.rb +31 -0
  163. data/lib/fontisan/models/cldr.rb +12 -0
  164. data/lib/fontisan/models/collection_brief_info.rb +0 -1
  165. data/lib/fontisan/models/collection_info.rb +0 -2
  166. data/lib/fontisan/models/collection_list_info.rb +0 -1
  167. data/lib/fontisan/models/collection_validation_report.rb +0 -2
  168. data/lib/fontisan/models/color_glyph.rb +0 -1
  169. data/lib/fontisan/models/font_report.rb +0 -1
  170. data/lib/fontisan/models/ttx/tables.rb +21 -0
  171. data/lib/fontisan/models/ttx/ttfont.rb +0 -8
  172. data/lib/fontisan/models/ttx.rb +14 -0
  173. data/lib/fontisan/models/ucd/ucd.rb +38 -0
  174. data/lib/fontisan/models/ucd/ucd_char.rb +67 -0
  175. data/lib/fontisan/models/ucd.rb +19 -0
  176. data/lib/fontisan/models.rb +47 -0
  177. data/lib/fontisan/open_type_collection.rb +6 -5
  178. data/lib/fontisan/open_type_font.rb +8 -2
  179. data/lib/fontisan/open_type_font_extensions.rb +9 -9
  180. data/lib/fontisan/optimizers/pattern_analyzer.rb +0 -1
  181. data/lib/fontisan/optimizers.rb +14 -0
  182. data/lib/fontisan/outline_extractor.rb +0 -2
  183. data/lib/fontisan/parsers/dfont_parser.rb +0 -1
  184. data/lib/fontisan/parsers.rb +10 -0
  185. data/lib/fontisan/pipeline/format_detector.rb +29 -102
  186. data/lib/fontisan/pipeline/output_writer.rb +11 -9
  187. data/lib/fontisan/pipeline/strategies/instance_strategy.rb +0 -4
  188. data/lib/fontisan/pipeline/strategies/named_strategy.rb +0 -4
  189. data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +0 -2
  190. data/lib/fontisan/pipeline/strategies.rb +14 -0
  191. data/lib/fontisan/pipeline/transformation_pipeline.rb +0 -7
  192. data/lib/fontisan/pipeline/variation_resolver.rb +0 -7
  193. data/lib/fontisan/pipeline.rb +13 -0
  194. data/lib/fontisan/sfnt_font.rb +29 -14
  195. data/lib/fontisan/sfnt_table.rb +0 -4
  196. data/lib/fontisan/subset/builder.rb +0 -6
  197. data/lib/fontisan/subset.rb +13 -0
  198. data/lib/fontisan/svg/font_generator.rb +0 -4
  199. data/lib/fontisan/svg/glyph_generator.rb +0 -2
  200. data/lib/fontisan/svg.rb +12 -0
  201. data/lib/fontisan/tables/cbdt.rb +0 -1
  202. data/lib/fontisan/tables/cblc.rb +0 -1
  203. data/lib/fontisan/tables/cff/charset.rb +0 -1
  204. data/lib/fontisan/tables/cff/charstring.rb +0 -1
  205. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +0 -4
  206. data/lib/fontisan/tables/cff/charstrings_index.rb +0 -3
  207. data/lib/fontisan/tables/cff/dict.rb +0 -1
  208. data/lib/fontisan/tables/cff/encoding.rb +0 -1
  209. data/lib/fontisan/tables/cff/header.rb +0 -2
  210. data/lib/fontisan/tables/cff/hint_operation_injector.rb +0 -2
  211. data/lib/fontisan/tables/cff/index.rb +0 -1
  212. data/lib/fontisan/tables/cff/private_dict.rb +0 -2
  213. data/lib/fontisan/tables/cff/private_dict_writer.rb +0 -2
  214. data/lib/fontisan/tables/cff/table_builder.rb +0 -6
  215. data/lib/fontisan/tables/cff/top_dict.rb +0 -2
  216. data/lib/fontisan/tables/cff.rb +22 -15
  217. data/lib/fontisan/tables/cff2/charstring_parser.rb +0 -2
  218. data/lib/fontisan/tables/cff2/table_builder.rb +0 -11
  219. data/lib/fontisan/tables/cff2/table_reader.rb +0 -2
  220. data/lib/fontisan/tables/cff2.rb +13 -14
  221. data/lib/fontisan/tables/cmap.rb +24 -2
  222. data/lib/fontisan/tables/cmap_table.rb +0 -3
  223. data/lib/fontisan/tables/colr.rb +0 -1
  224. data/lib/fontisan/tables/cpal.rb +0 -1
  225. data/lib/fontisan/tables/cvar.rb +0 -2
  226. data/lib/fontisan/tables/fvar.rb +0 -1
  227. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +0 -2
  228. data/lib/fontisan/tables/glyf/glyph_builder.rb +0 -3
  229. data/lib/fontisan/tables/glyf.rb +0 -6
  230. data/lib/fontisan/tables/glyf_table.rb +0 -3
  231. data/lib/fontisan/tables/gpos.rb +0 -2
  232. data/lib/fontisan/tables/gsub.rb +0 -2
  233. data/lib/fontisan/tables/gvar.rb +0 -2
  234. data/lib/fontisan/tables/head.rb +0 -2
  235. data/lib/fontisan/tables/head_table.rb +0 -3
  236. data/lib/fontisan/tables/hhea.rb +0 -2
  237. data/lib/fontisan/tables/hhea_table.rb +0 -3
  238. data/lib/fontisan/tables/hmtx.rb +0 -2
  239. data/lib/fontisan/tables/hmtx_table.rb +0 -3
  240. data/lib/fontisan/tables/hvar.rb +0 -3
  241. data/lib/fontisan/tables/loca.rb +0 -2
  242. data/lib/fontisan/tables/loca_table.rb +0 -3
  243. data/lib/fontisan/tables/maxp.rb +0 -2
  244. data/lib/fontisan/tables/maxp_table.rb +0 -3
  245. data/lib/fontisan/tables/mvar.rb +0 -3
  246. data/lib/fontisan/tables/name.rb +0 -2
  247. data/lib/fontisan/tables/name_table.rb +0 -3
  248. data/lib/fontisan/tables/os2_table.rb +0 -3
  249. data/lib/fontisan/tables/post_table.rb +0 -3
  250. data/lib/fontisan/tables/sbix.rb +0 -1
  251. data/lib/fontisan/tables/svg.rb +0 -1
  252. data/lib/fontisan/tables/variation_common.rb +0 -1
  253. data/lib/fontisan/tables/vvar.rb +0 -3
  254. data/lib/fontisan/tables.rb +54 -0
  255. data/lib/fontisan/true_type_collection.rb +6 -14
  256. data/lib/fontisan/true_type_font.rb +8 -2
  257. data/lib/fontisan/true_type_font_extensions.rb +9 -9
  258. data/lib/fontisan/type1/afm_generator.rb +0 -4
  259. data/lib/fontisan/type1/conversion_options.rb +0 -2
  260. data/lib/fontisan/type1/encodings.rb +0 -2
  261. data/lib/fontisan/type1/generator.rb +0 -8
  262. data/lib/fontisan/type1/pfa_generator.rb +0 -3
  263. data/lib/fontisan/type1/pfb_generator.rb +0 -5
  264. data/lib/fontisan/type1/pfm_generator.rb +0 -4
  265. data/lib/fontisan/type1.rb +42 -69
  266. data/lib/fontisan/type1_font.rb +40 -11
  267. data/lib/fontisan/ucd/aggregator.rb +73 -0
  268. data/lib/fontisan/ucd/cache_manager.rb +111 -0
  269. data/lib/fontisan/ucd/config.rb +59 -0
  270. data/lib/fontisan/ucd/download_error.rb +9 -0
  271. data/lib/fontisan/ucd/downloader.rb +88 -0
  272. data/lib/fontisan/ucd/error.rb +8 -0
  273. data/lib/fontisan/ucd/index.rb +103 -0
  274. data/lib/fontisan/ucd/index_builder.rb +107 -0
  275. data/lib/fontisan/ucd/range_entry.rb +56 -0
  276. data/lib/fontisan/ucd/unknown_version_error.rb +9 -0
  277. data/lib/fontisan/ucd/version_resolver.rb +79 -0
  278. data/lib/fontisan/ucd.rb +23 -0
  279. data/lib/fontisan/utilities/checksum_calculator.rb +0 -1
  280. data/lib/fontisan/utilities.rb +10 -0
  281. data/lib/fontisan/utils.rb +10 -0
  282. data/lib/fontisan/validation/collection_validator.rb +0 -2
  283. data/lib/fontisan/validation.rb +9 -0
  284. data/lib/fontisan/validators/basic_validator.rb +0 -2
  285. data/lib/fontisan/validators/font_book_validator.rb +0 -2
  286. data/lib/fontisan/validators/opentype_validator.rb +0 -2
  287. data/lib/fontisan/validators/profile_loader.rb +0 -5
  288. data/lib/fontisan/validators/validator.rb +0 -2
  289. data/lib/fontisan/validators/web_font_validator.rb +0 -2
  290. data/lib/fontisan/validators.rb +14 -0
  291. data/lib/fontisan/variable/delta_applicator.rb +0 -4
  292. data/lib/fontisan/variable/instancer.rb +0 -3
  293. data/lib/fontisan/variable/static_font_builder.rb +0 -3
  294. data/lib/fontisan/variable.rb +16 -0
  295. data/lib/fontisan/variation/blend_applier.rb +0 -2
  296. data/lib/fontisan/variation/cache.rb +0 -2
  297. data/lib/fontisan/variation/converter.rb +0 -3
  298. data/lib/fontisan/variation/data_extractor.rb +0 -2
  299. data/lib/fontisan/variation/delta_applier.rb +0 -5
  300. data/lib/fontisan/variation/inspector.rb +0 -1
  301. data/lib/fontisan/variation/instance_generator.rb +0 -6
  302. data/lib/fontisan/variation/instance_writer.rb +0 -5
  303. data/lib/fontisan/variation/metrics_adjuster.rb +0 -4
  304. data/lib/fontisan/variation/optimizer.rb +0 -3
  305. data/lib/fontisan/variation/parallel_generator.rb +0 -3
  306. data/lib/fontisan/variation/subsetter.rb +0 -4
  307. data/lib/fontisan/variation/tuple_variation_header.rb +0 -2
  308. data/lib/fontisan/variation/variable_svg_generator.rb +0 -3
  309. data/lib/fontisan/variation/variation_context.rb +0 -3
  310. data/lib/fontisan/variation/variation_preserver.rb +0 -3
  311. data/lib/fontisan/variation.rb +31 -0
  312. data/lib/fontisan/version.rb +1 -1
  313. data/lib/fontisan/woff2.rb +13 -0
  314. data/lib/fontisan/woff2_font.rb +31 -9
  315. data/lib/fontisan/woff_font.rb +31 -2
  316. data/lib/fontisan.rb +124 -196
  317. metadata +128 -7
  318. data/fontisan.gemspec +0 -47
@@ -0,0 +1,27 @@
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
@@ -0,0 +1,83 @@
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
@@ -0,0 +1,90 @@
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
@@ -0,0 +1,60 @@
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
@@ -0,0 +1,80 @@
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
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Autoload hub for the Fontisan::Audit namespace.
4
+ #
5
+ # AuditCommand (under Commands::AuditCommand) builds a Context and
6
+ # runs every extractor in Audit::Registry, merging their outputs
7
+ # into a single AuditReport.
8
+
9
+ module Fontisan
10
+ module Audit
11
+ autoload :Context, "fontisan/audit/context"
12
+ autoload :CodepointRangeCoalescer, "fontisan/audit/codepoint_range_coalescer"
13
+ autoload :Differ, "fontisan/audit/differ"
14
+ autoload :LibraryAggregator, "fontisan/audit/library_aggregator"
15
+ autoload :LibraryAuditor, "fontisan/audit/library_auditor"
16
+ autoload :Registry, "fontisan/audit/registry"
17
+ autoload :Extractors, "fontisan/audit/extractors"
18
+ autoload :StyleExtractor, "fontisan/audit/style_extractor"
19
+ end
20
+ end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bindata"
4
- require_relative "constants"
5
- require_relative "collection/shared_logic"
6
4
 
7
5
  module Fontisan
8
6
  # Abstract base class for font collections (TTC/OTC)
@@ -118,6 +116,29 @@ module Fontisan
118
116
  num_fonts
119
117
  end
120
118
 
119
+ # Whether this object represents a font collection rather than a single
120
+ # font. Each font class is the authority on this question.
121
+ #
122
+ # @return [Boolean]
123
+ def collection? = true
124
+
125
+ # Variation profile. Collections are containers; per-font variation is
126
+ # not exposed at the collection level.
127
+ #
128
+ # @return [Symbol] :static
129
+ def variation_type = :static
130
+
131
+ # Outline representation. Collections may contain mixed-flavor fonts;
132
+ # per-font outline type requires loading an individual font.
133
+ #
134
+ # @return [Symbol] :unknown
135
+ def outline_type = :unknown
136
+
137
+ # Collections have no single SFNT table directory.
138
+ #
139
+ # @return [Array<String>] empty
140
+ def table_names = []
141
+
121
142
  # Validate format correctness
122
143
  #
123
144
  # @return [Boolean] true if the format is valid, false otherwise
@@ -156,10 +177,6 @@ module Fontisan
156
177
  # list.fonts.each { |f| puts "#{f.index}: #{f.family_name}" }
157
178
  # end
158
179
  def list_fonts(io)
159
- require_relative "models/collection_list_info"
160
- require_relative "models/collection_font_summary"
161
- require_relative "tables/name"
162
-
163
180
  font_class = self.class.font_class
164
181
 
165
182
  fonts = font_offsets.map.with_index do |offset, index|
@@ -222,9 +239,6 @@ module Fontisan
222
239
  # puts "Version: #{info.version_string}"
223
240
  # end
224
241
  def collection_info(io, path)
225
- require_relative "models/collection_info"
226
- require_relative "models/table_sharing_info"
227
-
228
242
  # Calculate table sharing statistics
229
243
  table_sharing = calculate_table_sharing(io)
230
244
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base_record"
4
-
5
3
  module Fontisan
6
4
  module Binary
7
5
  # OpenType Offset Table (Font Header)
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Autoload hub for the Fontisan::Binary namespace.
4
+
5
+ module Fontisan
6
+ module Binary
7
+ autoload :BaseRecord, "fontisan/binary/base_record"
8
+ autoload :OffsetTable, "fontisan/binary/structures"
9
+ autoload :TableDirectoryEntry, "fontisan/binary/structures"
10
+ end
11
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Cldr
5
+ # Produces audit-ready per-language coverage from a codepoint list
6
+ # and a Cldr::Index of per-language exemplar sets.
7
+ #
8
+ # Pure: no I/O, no side effects.
9
+ module Aggregator
10
+ module_function
11
+
12
+ # @param codepoints [Enumerable<Integer>] font's codepoints
13
+ # @param languages_index [Cldr::Index]
14
+ # @return [Array<Models::Cldr::LanguageCoverage>] sorted by
15
+ # descending coverage_ratio, then by language name
16
+ def aggregate(codepoints, languages_index)
17
+ font_set = Set.new(codepoints)
18
+
19
+ languages_index.entries.map do |lang, required_set|
20
+ covered = (font_set & required_set).size
21
+ total = required_set.size
22
+ Models::Cldr::LanguageCoverage.new(
23
+ language: lang,
24
+ covered: covered,
25
+ total: total,
26
+ coverage_ratio: total.zero? ? 0.0 : covered.fdiv(total).round(4),
27
+ fully_supported: total.positive? && covered == total,
28
+ )
29
+ end.sort_by { |lc| [lc.coverage_ratio * -1, lc.language] }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module Fontisan
6
+ module Cldr
7
+ # Manages the on-disk CLDR cache layout.
8
+ #
9
+ # Cache root resolution honors `XDG_CONFIG_HOME` per the XDG Base
10
+ # Directory Specification. Falls back to `~/.config` on Unix.
11
+ #
12
+ # Layout:
13
+ #
14
+ # <root>/
15
+ # <version>/
16
+ # json/ # extracted CLDR JSON archive
17
+ # cldr-json/
18
+ # cldr-characters-full/
19
+ # main/<lang>/characters.json
20
+ # index/
21
+ # languages.yml # built index of per-language codepoint sets
22
+ #
23
+ # No network access — all methods are pure filesystem operations.
24
+ module CacheManager
25
+ LANGUAGES_INDEX_FILENAME = "languages.yml"
26
+ private_constant :LANGUAGES_INDEX_FILENAME
27
+
28
+ class << self
29
+ # Root path of the CLDR cache.
30
+ # @return [Pathname]
31
+ def root
32
+ base = xdg_config_home || File.join(Dir.home, ".config")
33
+ Pathname.new(base).join("fontisan", "cldr")
34
+ end
35
+
36
+ # Per-version directory.
37
+ # @param version [String] e.g. "46.0.0"
38
+ # @return [Pathname]
39
+ def version_dir(version)
40
+ root.join(version)
41
+ end
42
+
43
+ # Directory where the raw CLDR JSON archive is extracted.
44
+ # @param version [String]
45
+ # @return [Pathname]
46
+ def json_dir(version)
47
+ version_dir(version).join("json")
48
+ end
49
+
50
+ # Directory containing the per-language characters.json files
51
+ # inside the extracted archive.
52
+ # @param version [String]
53
+ # @return [Pathname]
54
+ def characters_main_dir(version)
55
+ json_dir(version).join("cldr-json", "cldr-characters-full", "main")
56
+ end
57
+
58
+ # Directory holding the derived language index for a version.
59
+ # @param version [String]
60
+ # @return [Pathname]
61
+ def index_dir(version)
62
+ version_dir(version).join("index")
63
+ end
64
+
65
+ def languages_index_path(version)
66
+ index_dir(version).join(LANGUAGES_INDEX_FILENAME)
67
+ end
68
+
69
+ # True if the extracted JSON archive is present for this version.
70
+ # @param version [String]
71
+ # @return [Boolean]
72
+ def cached?(version)
73
+ characters_main_dir(version).exist?
74
+ end
75
+
76
+ # All versions currently in the cache (sorted ascending).
77
+ # @return [Array<String>]
78
+ def cached_versions
79
+ return [] unless root.exist?
80
+
81
+ root.children.select(&:directory?).map { |p| p.basename.to_s }.sort
82
+ end
83
+
84
+ # Create the version directory and json/index subdirs.
85
+ # Idempotent.
86
+ # @param version [String]
87
+ def ensure_version_dir!(version)
88
+ json_dir(version).mkpath
89
+ index_dir(version).mkpath
90
+ end
91
+
92
+ # Remove a version from the cache. No-op if absent.
93
+ # @param version [String]
94
+ def remove_version(version)
95
+ dir = version_dir(version)
96
+ dir.rmtree if dir.exist?
97
+ end
98
+
99
+ private
100
+
101
+ def xdg_config_home
102
+ env = ENV["XDG_CONFIG_HOME"]
103
+ return nil if env.nil? || env.empty?
104
+
105
+ env
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Fontisan
6
+ module Cldr
7
+ # Single source of truth for CLDR version selection.
8
+ #
9
+ # Wraps `lib/fontisan/config/cldr.yml`. Loads the YAML once at first
10
+ # access and memoizes. All other Cldr::* classes resolve versions,
11
+ # URLs, and known-version validation through this module.
12
+ module Config
13
+ CONFIG_PATH = File.expand_path("../config/cldr.yml", __dir__)
14
+ private_constant :CONFIG_PATH
15
+
16
+ class << self
17
+ # The CLDR version Fontisan uses by default for auto-download and
18
+ # `fontisan cldr download` (no args). String like "46.0.0".
19
+ def default_version
20
+ data[:default_version]
21
+ end
22
+
23
+ # Array of CLDR version strings this Fontisan release recognizes.
24
+ # Used by VersionResolver to reject unknown versions early.
25
+ def known_versions
26
+ data[:known_versions]
27
+ end
28
+
29
+ # Base URL for fetching CLDR JSON artifacts.
30
+ def base_url
31
+ data[:base_url]
32
+ end
33
+
34
+ # Listing URL for `--latest` probing.
35
+ def listing_url
36
+ data[:listing_url]
37
+ end
38
+
39
+ # Full URL to the CLDR JSON full archive for a version.
40
+ # @param version [String] e.g. "46.0.0"
41
+ # @return [String]
42
+ def archive_url_for(version)
43
+ "#{base_url}/#{version}/cldr-#{version}-json-full.zip"
44
+ end
45
+
46
+ # True if the version appears in `known_versions`.
47
+ def known?(version)
48
+ known_versions.include?(version)
49
+ end
50
+
51
+ private
52
+
53
+ def data
54
+ @data ||= YAML.load_file(CONFIG_PATH).transform_keys(&:to_sym)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Cldr
5
+ # Raised by Cldr::Downloader when the upstream HTTP fetch or the zip
6
+ # extraction fails. Caught by AuditCommand to degrade-with-warning.
7
+ class DownloadError < Cldr::Error; end
8
+ end
9
+ end