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.
Files changed (316) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +14 -90
  3. data/README.adoc +257 -1
  4. data/docs/.vitepress/config.ts +68 -8
  5. data/docs/.vitepress/theme/style.css +570 -272
  6. data/docs/CONVERSION_GUIDE.adoc +31 -8
  7. data/docs/EXTRACT_TTC_MIGRATION.md +1 -1
  8. data/docs/WOFF_WOFF2_FORMATS.adoc +53 -0
  9. data/docs/api/conversion-options.md +37 -14
  10. data/docs/cli/audit.md +337 -0
  11. data/docs/cli/convert.md +20 -1
  12. data/docs/cli/index.md +31 -0
  13. data/docs/guide/color.md +1 -1
  14. data/docs/guide/conversion/options.md +32 -3
  15. data/docs/guide/conversion/ttf-otf.md +1 -1
  16. data/docs/guide/conversion/type1.md +1 -1
  17. data/docs/guide/conversion/web.md +91 -32
  18. data/docs/guide/conversion.md +6 -5
  19. data/docs/guide/formats/woff.md +35 -11
  20. data/docs/guide/index.md +2 -2
  21. data/docs/guide/migrations/extract-ttc.md +1 -1
  22. data/docs/guide/quick-start.md +4 -4
  23. data/docs/guide/type1.md +4 -4
  24. data/docs/guide/woff.md +19 -17
  25. data/docs/index.md +2 -0
  26. data/docs/lychee.toml +5 -1
  27. data/docs/package.json +1 -1
  28. data/docs/public/robots.txt +4 -0
  29. data/docs/scripts/post-build.mjs +81 -0
  30. data/lib/fontisan/audit/codepoint_range_coalescer.rb +41 -0
  31. data/lib/fontisan/audit/context.rb +122 -0
  32. data/lib/fontisan/audit/differ.rb +124 -0
  33. data/lib/fontisan/audit/extractors/aggregations.rb +54 -0
  34. data/lib/fontisan/audit/extractors/base.rb +26 -0
  35. data/lib/fontisan/audit/extractors/color_capabilities.rb +141 -0
  36. data/lib/fontisan/audit/extractors/coverage.rb +48 -0
  37. data/lib/fontisan/audit/extractors/hinting.rb +197 -0
  38. data/lib/fontisan/audit/extractors/identity.rb +52 -0
  39. data/lib/fontisan/audit/extractors/language_coverage.rb +37 -0
  40. data/lib/fontisan/audit/extractors/licensing.rb +79 -0
  41. data/lib/fontisan/audit/extractors/metrics.rb +103 -0
  42. data/lib/fontisan/audit/extractors/opentype_layout.rb +69 -0
  43. data/lib/fontisan/audit/extractors/provenance.rb +29 -0
  44. data/lib/fontisan/audit/extractors/style.rb +32 -0
  45. data/lib/fontisan/audit/extractors/variation_detail.rb +99 -0
  46. data/lib/fontisan/audit/extractors.rb +27 -0
  47. data/lib/fontisan/audit/library_aggregator.rb +83 -0
  48. data/lib/fontisan/audit/library_auditor.rb +90 -0
  49. data/lib/fontisan/audit/registry.rb +60 -0
  50. data/lib/fontisan/audit/style_extractor.rb +80 -0
  51. data/lib/fontisan/audit.rb +20 -0
  52. data/lib/fontisan/base_collection.rb +23 -9
  53. data/lib/fontisan/binary/structures.rb +0 -2
  54. data/lib/fontisan/binary.rb +11 -0
  55. data/lib/fontisan/cldr/aggregator.rb +33 -0
  56. data/lib/fontisan/cldr/cache_manager.rb +110 -0
  57. data/lib/fontisan/cldr/config.rb +59 -0
  58. data/lib/fontisan/cldr/download_error.rb +9 -0
  59. data/lib/fontisan/cldr/downloader.rb +79 -0
  60. data/lib/fontisan/cldr/error.rb +8 -0
  61. data/lib/fontisan/cldr/index.rb +64 -0
  62. data/lib/fontisan/cldr/index_builder.rb +72 -0
  63. data/lib/fontisan/cldr/unicode_set_parser.rb +172 -0
  64. data/lib/fontisan/cldr/unknown_version_error.rb +9 -0
  65. data/lib/fontisan/cldr/version_resolver.rb +91 -0
  66. data/lib/fontisan/cldr.rb +23 -0
  67. data/lib/fontisan/cli/cldr_cli.rb +85 -0
  68. data/lib/fontisan/cli/ucd_cli.rb +97 -0
  69. data/lib/fontisan/cli.rb +201 -2
  70. data/lib/fontisan/collection/builder.rb +0 -4
  71. data/lib/fontisan/collection/dfont_builder.rb +0 -4
  72. data/lib/fontisan/collection/shared_logic.rb +0 -2
  73. data/lib/fontisan/collection/writer.rb +0 -3
  74. data/lib/fontisan/collection.rb +15 -0
  75. data/lib/fontisan/commands/audit_command.rb +123 -0
  76. data/lib/fontisan/commands/audit_compare_command.rb +66 -0
  77. data/lib/fontisan/commands/audit_library_command.rb +46 -0
  78. data/lib/fontisan/commands/base_command.rb +0 -3
  79. data/lib/fontisan/commands/convert_command.rb +25 -20
  80. data/lib/fontisan/commands/dump_table_command.rb +0 -3
  81. data/lib/fontisan/commands/export_command.rb +0 -4
  82. data/lib/fontisan/commands/features_command.rb +0 -3
  83. data/lib/fontisan/commands/instance_command.rb +0 -5
  84. data/lib/fontisan/commands/ls_command.rb +0 -6
  85. data/lib/fontisan/commands/optical_size_command.rb +0 -3
  86. data/lib/fontisan/commands/pack_command.rb +0 -5
  87. data/lib/fontisan/commands/scripts_command.rb +0 -2
  88. data/lib/fontisan/commands/subset_command.rb +0 -3
  89. data/lib/fontisan/commands/unicode_command.rb +0 -3
  90. data/lib/fontisan/commands/unpack_command.rb +0 -7
  91. data/lib/fontisan/commands/validate_command.rb +0 -8
  92. data/lib/fontisan/commands/variable_command.rb +0 -3
  93. data/lib/fontisan/commands.rb +29 -0
  94. data/lib/fontisan/config/cldr.yml +22 -0
  95. data/lib/fontisan/config/conversion_matrix.yml +38 -0
  96. data/lib/fontisan/config/ucd.yml +23 -0
  97. data/lib/fontisan/constants.rb +19 -0
  98. data/lib/fontisan/conversion_options.rb +30 -19
  99. data/lib/fontisan/converters/cff_table_builder.rb +0 -3
  100. data/lib/fontisan/converters/collection_converter.rb +0 -8
  101. data/lib/fontisan/converters/conversion_strategy.rb +161 -46
  102. data/lib/fontisan/converters/format_converter.rb +143 -32
  103. data/lib/fontisan/converters/glyf_table_builder.rb +0 -2
  104. data/lib/fontisan/converters/outline_converter.rb +0 -19
  105. data/lib/fontisan/converters/outline_extraction.rb +0 -5
  106. data/lib/fontisan/converters/outline_optimizer.rb +0 -5
  107. data/lib/fontisan/converters/svg_generator.rb +0 -4
  108. data/lib/fontisan/converters/table_copier.rb +0 -2
  109. data/lib/fontisan/converters/type1_converter.rb +0 -11
  110. data/lib/fontisan/converters/woff2_encoder.rb +49 -20
  111. data/lib/fontisan/converters/woff_writer.rb +211 -282
  112. data/lib/fontisan/converters.rb +21 -0
  113. data/lib/fontisan/dfont_collection.rb +29 -10
  114. data/lib/fontisan/export/exporter.rb +0 -6
  115. data/lib/fontisan/export/transformers/font_to_ttx.rb +0 -9
  116. data/lib/fontisan/export/transformers/head_transformer.rb +0 -2
  117. data/lib/fontisan/export/transformers/hhea_transformer.rb +0 -2
  118. data/lib/fontisan/export/transformers/maxp_transformer.rb +0 -2
  119. data/lib/fontisan/export/transformers/name_transformer.rb +0 -2
  120. data/lib/fontisan/export/transformers/os2_transformer.rb +0 -2
  121. data/lib/fontisan/export/transformers/post_transformer.rb +0 -2
  122. data/lib/fontisan/export/transformers.rb +17 -0
  123. data/lib/fontisan/export.rb +13 -0
  124. data/lib/fontisan/font_loader.rb +14 -19
  125. data/lib/fontisan/font_writer.rb +0 -1
  126. data/lib/fontisan/formatters/audit_diff_text_renderer.rb +122 -0
  127. data/lib/fontisan/formatters/audit_text_renderer.rb +324 -0
  128. data/lib/fontisan/formatters/library_summary_text_renderer.rb +99 -0
  129. data/lib/fontisan/formatters/text_formatter.rb +6 -0
  130. data/lib/fontisan/formatters.rb +12 -0
  131. data/lib/fontisan/hints/hint_converter.rb +0 -1
  132. data/lib/fontisan/hints/postscript_hint_applier.rb +0 -9
  133. data/lib/fontisan/hints/postscript_hint_extractor.rb +0 -2
  134. data/lib/fontisan/hints/truetype_hint_extractor.rb +0 -2
  135. data/lib/fontisan/hints.rb +16 -0
  136. data/lib/fontisan/metrics_calculator.rb +0 -2
  137. data/lib/fontisan/models/all_scripts_features_info.rb +0 -1
  138. data/lib/fontisan/models/audit/audit_axis.rb +30 -0
  139. data/lib/fontisan/models/audit/audit_block.rb +32 -0
  140. data/lib/fontisan/models/audit/audit_diff.rb +77 -0
  141. data/lib/fontisan/models/audit/audit_report.rb +153 -0
  142. data/lib/fontisan/models/audit/codepoint_range.rb +40 -0
  143. data/lib/fontisan/models/audit/codepoint_set_diff.rb +34 -0
  144. data/lib/fontisan/models/audit/color_capabilities.rb +93 -0
  145. data/lib/fontisan/models/audit/duplicate_group.rb +23 -0
  146. data/lib/fontisan/models/audit/embedding_type.rb +76 -0
  147. data/lib/fontisan/models/audit/field_change.rb +28 -0
  148. data/lib/fontisan/models/audit/fs_selection_flags.rb +61 -0
  149. data/lib/fontisan/models/audit/gasp_range.rb +63 -0
  150. data/lib/fontisan/models/audit/hinting.rb +93 -0
  151. data/lib/fontisan/models/audit/library_summary.rb +40 -0
  152. data/lib/fontisan/models/audit/licensing.rb +48 -0
  153. data/lib/fontisan/models/audit/metrics.rb +111 -0
  154. data/lib/fontisan/models/audit/named_instance.rb +41 -0
  155. data/lib/fontisan/models/audit/opentype_layout.rb +40 -0
  156. data/lib/fontisan/models/audit/script_coverage_row.rb +26 -0
  157. data/lib/fontisan/models/audit/script_features.rb +28 -0
  158. data/lib/fontisan/models/audit/variation_detail.rb +44 -0
  159. data/lib/fontisan/models/audit.rb +33 -0
  160. data/lib/fontisan/models/cldr/language_coverage.rb +31 -0
  161. data/lib/fontisan/models/cldr.rb +12 -0
  162. data/lib/fontisan/models/collection_brief_info.rb +0 -1
  163. data/lib/fontisan/models/collection_info.rb +0 -2
  164. data/lib/fontisan/models/collection_list_info.rb +0 -1
  165. data/lib/fontisan/models/collection_validation_report.rb +0 -2
  166. data/lib/fontisan/models/color_glyph.rb +0 -1
  167. data/lib/fontisan/models/font_report.rb +0 -1
  168. data/lib/fontisan/models/ttx/tables.rb +21 -0
  169. data/lib/fontisan/models/ttx/ttfont.rb +0 -8
  170. data/lib/fontisan/models/ttx.rb +14 -0
  171. data/lib/fontisan/models/ucd/ucd.rb +38 -0
  172. data/lib/fontisan/models/ucd/ucd_char.rb +67 -0
  173. data/lib/fontisan/models/ucd.rb +19 -0
  174. data/lib/fontisan/models.rb +47 -0
  175. data/lib/fontisan/open_type_collection.rb +6 -5
  176. data/lib/fontisan/open_type_font.rb +8 -2
  177. data/lib/fontisan/open_type_font_extensions.rb +9 -9
  178. data/lib/fontisan/optimizers/pattern_analyzer.rb +0 -1
  179. data/lib/fontisan/optimizers.rb +14 -0
  180. data/lib/fontisan/outline_extractor.rb +0 -2
  181. data/lib/fontisan/parsers/dfont_parser.rb +0 -1
  182. data/lib/fontisan/parsers.rb +10 -0
  183. data/lib/fontisan/pipeline/format_detector.rb +29 -102
  184. data/lib/fontisan/pipeline/output_writer.rb +11 -9
  185. data/lib/fontisan/pipeline/strategies/instance_strategy.rb +0 -4
  186. data/lib/fontisan/pipeline/strategies/named_strategy.rb +0 -4
  187. data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +0 -2
  188. data/lib/fontisan/pipeline/strategies.rb +14 -0
  189. data/lib/fontisan/pipeline/transformation_pipeline.rb +0 -7
  190. data/lib/fontisan/pipeline/variation_resolver.rb +0 -7
  191. data/lib/fontisan/pipeline.rb +13 -0
  192. data/lib/fontisan/sfnt_font.rb +29 -14
  193. data/lib/fontisan/sfnt_table.rb +0 -4
  194. data/lib/fontisan/subset/builder.rb +0 -6
  195. data/lib/fontisan/subset.rb +13 -0
  196. data/lib/fontisan/svg/font_generator.rb +0 -4
  197. data/lib/fontisan/svg/glyph_generator.rb +0 -2
  198. data/lib/fontisan/svg.rb +12 -0
  199. data/lib/fontisan/tables/cbdt.rb +0 -1
  200. data/lib/fontisan/tables/cblc.rb +0 -1
  201. data/lib/fontisan/tables/cff/charset.rb +0 -1
  202. data/lib/fontisan/tables/cff/charstring.rb +0 -1
  203. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +0 -4
  204. data/lib/fontisan/tables/cff/charstrings_index.rb +0 -3
  205. data/lib/fontisan/tables/cff/dict.rb +0 -1
  206. data/lib/fontisan/tables/cff/encoding.rb +0 -1
  207. data/lib/fontisan/tables/cff/header.rb +0 -2
  208. data/lib/fontisan/tables/cff/hint_operation_injector.rb +0 -2
  209. data/lib/fontisan/tables/cff/index.rb +0 -1
  210. data/lib/fontisan/tables/cff/private_dict.rb +0 -2
  211. data/lib/fontisan/tables/cff/private_dict_writer.rb +0 -2
  212. data/lib/fontisan/tables/cff/table_builder.rb +0 -6
  213. data/lib/fontisan/tables/cff/top_dict.rb +0 -2
  214. data/lib/fontisan/tables/cff.rb +22 -15
  215. data/lib/fontisan/tables/cff2/charstring_parser.rb +0 -2
  216. data/lib/fontisan/tables/cff2/table_builder.rb +0 -11
  217. data/lib/fontisan/tables/cff2/table_reader.rb +0 -2
  218. data/lib/fontisan/tables/cff2.rb +13 -14
  219. data/lib/fontisan/tables/cmap.rb +24 -2
  220. data/lib/fontisan/tables/cmap_table.rb +0 -3
  221. data/lib/fontisan/tables/colr.rb +0 -1
  222. data/lib/fontisan/tables/cpal.rb +0 -1
  223. data/lib/fontisan/tables/cvar.rb +0 -2
  224. data/lib/fontisan/tables/fvar.rb +0 -1
  225. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +0 -2
  226. data/lib/fontisan/tables/glyf/glyph_builder.rb +0 -3
  227. data/lib/fontisan/tables/glyf.rb +0 -6
  228. data/lib/fontisan/tables/glyf_table.rb +0 -3
  229. data/lib/fontisan/tables/gpos.rb +0 -2
  230. data/lib/fontisan/tables/gsub.rb +0 -2
  231. data/lib/fontisan/tables/gvar.rb +0 -2
  232. data/lib/fontisan/tables/head.rb +0 -2
  233. data/lib/fontisan/tables/head_table.rb +0 -3
  234. data/lib/fontisan/tables/hhea.rb +0 -2
  235. data/lib/fontisan/tables/hhea_table.rb +0 -3
  236. data/lib/fontisan/tables/hmtx.rb +0 -2
  237. data/lib/fontisan/tables/hmtx_table.rb +0 -3
  238. data/lib/fontisan/tables/hvar.rb +0 -3
  239. data/lib/fontisan/tables/loca.rb +0 -2
  240. data/lib/fontisan/tables/loca_table.rb +0 -3
  241. data/lib/fontisan/tables/maxp.rb +0 -2
  242. data/lib/fontisan/tables/maxp_table.rb +0 -3
  243. data/lib/fontisan/tables/mvar.rb +0 -3
  244. data/lib/fontisan/tables/name.rb +0 -2
  245. data/lib/fontisan/tables/name_table.rb +0 -3
  246. data/lib/fontisan/tables/os2_table.rb +0 -3
  247. data/lib/fontisan/tables/post_table.rb +0 -3
  248. data/lib/fontisan/tables/sbix.rb +0 -1
  249. data/lib/fontisan/tables/svg.rb +0 -1
  250. data/lib/fontisan/tables/variation_common.rb +0 -1
  251. data/lib/fontisan/tables/vvar.rb +0 -3
  252. data/lib/fontisan/tables.rb +54 -0
  253. data/lib/fontisan/true_type_collection.rb +6 -14
  254. data/lib/fontisan/true_type_font.rb +8 -2
  255. data/lib/fontisan/true_type_font_extensions.rb +9 -9
  256. data/lib/fontisan/type1/afm_generator.rb +0 -4
  257. data/lib/fontisan/type1/conversion_options.rb +0 -2
  258. data/lib/fontisan/type1/encodings.rb +0 -2
  259. data/lib/fontisan/type1/generator.rb +0 -8
  260. data/lib/fontisan/type1/pfa_generator.rb +0 -3
  261. data/lib/fontisan/type1/pfb_generator.rb +0 -5
  262. data/lib/fontisan/type1/pfm_generator.rb +0 -4
  263. data/lib/fontisan/type1.rb +42 -69
  264. data/lib/fontisan/type1_font.rb +40 -11
  265. data/lib/fontisan/ucd/aggregator.rb +73 -0
  266. data/lib/fontisan/ucd/cache_manager.rb +111 -0
  267. data/lib/fontisan/ucd/config.rb +59 -0
  268. data/lib/fontisan/ucd/download_error.rb +9 -0
  269. data/lib/fontisan/ucd/downloader.rb +88 -0
  270. data/lib/fontisan/ucd/error.rb +8 -0
  271. data/lib/fontisan/ucd/index.rb +103 -0
  272. data/lib/fontisan/ucd/index_builder.rb +107 -0
  273. data/lib/fontisan/ucd/range_entry.rb +56 -0
  274. data/lib/fontisan/ucd/unknown_version_error.rb +9 -0
  275. data/lib/fontisan/ucd/version_resolver.rb +79 -0
  276. data/lib/fontisan/ucd.rb +23 -0
  277. data/lib/fontisan/utilities/checksum_calculator.rb +0 -1
  278. data/lib/fontisan/utilities.rb +10 -0
  279. data/lib/fontisan/utils.rb +10 -0
  280. data/lib/fontisan/validation/collection_validator.rb +0 -2
  281. data/lib/fontisan/validation.rb +9 -0
  282. data/lib/fontisan/validators/basic_validator.rb +0 -2
  283. data/lib/fontisan/validators/font_book_validator.rb +0 -2
  284. data/lib/fontisan/validators/opentype_validator.rb +0 -2
  285. data/lib/fontisan/validators/profile_loader.rb +0 -5
  286. data/lib/fontisan/validators/validator.rb +0 -2
  287. data/lib/fontisan/validators/web_font_validator.rb +0 -2
  288. data/lib/fontisan/validators.rb +14 -0
  289. data/lib/fontisan/variable/delta_applicator.rb +0 -4
  290. data/lib/fontisan/variable/instancer.rb +0 -3
  291. data/lib/fontisan/variable/static_font_builder.rb +0 -3
  292. data/lib/fontisan/variable.rb +16 -0
  293. data/lib/fontisan/variation/blend_applier.rb +0 -2
  294. data/lib/fontisan/variation/cache.rb +0 -2
  295. data/lib/fontisan/variation/converter.rb +0 -3
  296. data/lib/fontisan/variation/data_extractor.rb +0 -2
  297. data/lib/fontisan/variation/delta_applier.rb +0 -5
  298. data/lib/fontisan/variation/inspector.rb +0 -1
  299. data/lib/fontisan/variation/instance_generator.rb +0 -6
  300. data/lib/fontisan/variation/instance_writer.rb +0 -5
  301. data/lib/fontisan/variation/metrics_adjuster.rb +0 -4
  302. data/lib/fontisan/variation/optimizer.rb +0 -3
  303. data/lib/fontisan/variation/parallel_generator.rb +0 -3
  304. data/lib/fontisan/variation/subsetter.rb +0 -4
  305. data/lib/fontisan/variation/tuple_variation_header.rb +0 -2
  306. data/lib/fontisan/variation/variable_svg_generator.rb +0 -3
  307. data/lib/fontisan/variation/variation_context.rb +0 -3
  308. data/lib/fontisan/variation/variation_preserver.rb +0 -3
  309. data/lib/fontisan/variation.rb +31 -0
  310. data/lib/fontisan/version.rb +1 -1
  311. data/lib/fontisan/woff2.rb +13 -0
  312. data/lib/fontisan/woff2_font.rb +31 -9
  313. data/lib/fontisan/woff_font.rb +31 -2
  314. data/lib/fontisan.rb +124 -196
  315. metadata +114 -7
  316. data/fontisan.gemspec +0 -48
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Formatters
5
+ # Human-readable overview of a {Models::Audit::LibrarySummary}.
6
+ #
7
+ # Lists the per-face rollup counts, aggregate metrics, script coverage
8
+ # matrix, duplicate groups, and license distribution. The full per-face
9
+ # AuditReports are attached to the model; this view only shows the
10
+ # cross-face summaries (use YAML/JSON output for the full per-face data).
11
+ class LibrarySummaryTextRenderer
12
+ SEPARATOR = "=" * 80
13
+ LIST_LIMIT = 15
14
+
15
+ # @param summary [Models::Audit::LibrarySummary]
16
+ def initialize(summary)
17
+ @summary = summary
18
+ @lines = []
19
+ end
20
+
21
+ # @return [String]
22
+ def render
23
+ render_header
24
+ render_aggregates
25
+ render_script_coverage
26
+ render_duplicates
27
+ render_license_distribution
28
+ @lines.join("\n")
29
+ end
30
+
31
+ private
32
+
33
+ def render_header
34
+ @lines << "LIBRARY SUMMARY"
35
+ @lines << SEPARATOR
36
+ @lines << "root: #{@summary.root_path}"
37
+ @lines << "files: #{@summary.total_files} faces: #{@summary.total_faces}"
38
+ exts = Array(@summary.scanned_extensions)
39
+ @lines << "formats: #{exts.empty? ? '(none)' : exts.join(', ')}"
40
+ end
41
+
42
+ def render_aggregates
43
+ m = @summary.aggregate_metrics || {}
44
+ section("AGGREGATES")
45
+ @lines << " codepoints: #{m[:total_codepoints] || 0}"
46
+ @lines << " glyphs: #{m[:total_glyphs] || 0}"
47
+ @lines << " total size: #{format_bytes(m[:total_size_bytes] || 0)}"
48
+ end
49
+
50
+ def render_script_coverage
51
+ rows = Array(@summary.script_coverage)
52
+ return if rows.empty?
53
+
54
+ section("SCRIPT COVERAGE (top #{LIST_LIMIT})")
55
+ rows.first(LIST_LIMIT).each do |row|
56
+ @lines << " #{row.script}: #{row.face_count} face#{'s' unless row.face_count == 1}"
57
+ end
58
+ end
59
+
60
+ def render_duplicates
61
+ groups = Array(@summary.duplicate_groups)
62
+ return if groups.empty?
63
+
64
+ section("DUPLICATES (#{groups.size} group#{'s' unless groups.size == 1})")
65
+ groups.each do |group|
66
+ @lines << " sha #{group.source_sha256[0, 12]}:"
67
+ group.files.each { |path| @lines << " #{path}" }
68
+ end
69
+ end
70
+
71
+ def render_license_distribution
72
+ dist = @summary.license_distribution || {}
73
+ return if dist.empty?
74
+
75
+ section("LICENSE DISTRIBUTION")
76
+ dist.sort_by { |_url, count| -count }.each do |url, count|
77
+ @lines << " #{count} #{url}"
78
+ end
79
+ end
80
+
81
+ def section(title)
82
+ @lines << ""
83
+ @lines << title
84
+ end
85
+
86
+ def format_bytes(bytes)
87
+ return "0 B" if bytes.nil? || bytes.zero?
88
+
89
+ if bytes < 1024
90
+ "#{bytes} B"
91
+ elsif bytes < 1024 * 1024
92
+ "#{(bytes / 1024.0).round(2)} KB"
93
+ else
94
+ "#{(bytes / (1024.0 * 1024)).round(2)} MB"
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -44,6 +44,12 @@ module Fontisan
44
44
  format_collection_info(model)
45
45
  when Models::CollectionBriefInfo
46
46
  format_collection_brief_info(model)
47
+ when Models::Audit::AuditReport
48
+ Formatters::AuditTextRenderer.new(model).render
49
+ when Models::Audit::AuditDiff
50
+ Formatters::AuditDiffTextRenderer.new(model).render
51
+ when Models::Audit::LibrarySummary
52
+ Formatters::LibrarySummaryTextRenderer.new(model).render
47
53
  else
48
54
  model.to_s
49
55
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Autoload hub for the Fontisan::Formatters namespace.
4
+
5
+ module Fontisan
6
+ module Formatters
7
+ autoload :AuditDiffTextRenderer, "fontisan/formatters/audit_diff_text_renderer"
8
+ autoload :AuditTextRenderer, "fontisan/formatters/audit_text_renderer"
9
+ autoload :LibrarySummaryTextRenderer, "fontisan/formatters/library_summary_text_renderer"
10
+ autoload :TextFormatter, "fontisan/formatters/text_formatter"
11
+ end
12
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
- require_relative "../models/hint"
5
4
 
6
5
  module Fontisan
7
6
  module Hints
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../models/hint"
4
3
  require "json"
5
4
 
6
5
  module Fontisan
@@ -80,9 +79,6 @@ module Fontisan
80
79
  cff2_data = tables["CFF2"] || tables["CFF2 "]
81
80
 
82
81
  begin
83
- require_relative "../tables/cff2/table_reader"
84
- require_relative "../tables/cff2/table_builder"
85
-
86
82
  reader = Tables::Cff2::TableReader.new(cff2_data)
87
83
 
88
84
  # Validate CFF2 version
@@ -131,11 +127,6 @@ module Fontisan
131
127
 
132
128
  # Apply hints (both font-level and per-glyph)
133
129
  begin
134
- require_relative "../tables/cff/table_builder"
135
- require_relative "../tables/cff/charstring_rebuilder"
136
- require_relative "../tables/cff/hint_operation_injector"
137
- require_relative "../tables/cff"
138
-
139
130
  # Parse CFF binary data into Cff object if needed
140
131
  cff_data = tables["CFF "]
141
132
  cff_table = if cff_data.is_a?(Tables::Cff)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../models/hint"
4
-
5
3
  module Fontisan
6
4
  module Hints
7
5
  # Extracts rendering hints from PostScript/CFF CharString data
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../models/hint"
4
-
5
3
  module Fontisan
6
4
  module Hints
7
5
  # Extracts rendering hints from TrueType glyph data
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Autoload hub for the Fontisan::Hints namespace.
4
+
5
+ module Fontisan
6
+ module Hints
7
+ autoload :HintConverter, "fontisan/hints/hint_converter"
8
+ autoload :HintValidator, "fontisan/hints/hint_validator"
9
+ autoload :PostScriptHintApplier, "fontisan/hints/postscript_hint_applier"
10
+ autoload :PostScriptHintExtractor, "fontisan/hints/postscript_hint_extractor"
11
+ autoload :TrueTypeHintApplier, "fontisan/hints/truetype_hint_applier"
12
+ autoload :TrueTypeHintExtractor, "fontisan/hints/truetype_hint_extractor"
13
+ autoload :TrueTypeInstructionAnalyzer, "fontisan/hints/truetype_instruction_analyzer"
14
+ autoload :TrueTypeInstructionGenerator, "fontisan/hints/truetype_instruction_generator"
15
+ end
16
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "constants"
4
-
5
3
  module Fontisan
6
4
  # High-level utility class for accessing font metrics
7
5
  #
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
- require_relative "features_info"
5
4
 
6
5
  module Fontisan
7
6
  module Models
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ module Audit
8
+ # One fvar axis descriptor on an AuditReport.
9
+ #
10
+ # `min_value` / `default_value` / `max_value` are used (rather than
11
+ # `min` / `default` / `max`) to avoid colliding with Ruby's built-in
12
+ # `default` method on classes.
13
+ class AuditAxis < Lutaml::Model::Serializable
14
+ attribute :tag, :string
15
+ attribute :min_value, :float
16
+ attribute :default_value, :float
17
+ attribute :max_value, :float
18
+ attribute :name, :string
19
+
20
+ key_value do
21
+ map "tag", to: :tag
22
+ map "min_value", to: :min_value
23
+ map "default_value", to: :default_value
24
+ map "max_value", to: :max_value
25
+ map "name", to: :name
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ module Audit
8
+ # One Unicode block coverage row on an AuditReport.
9
+ class AuditBlock < Lutaml::Model::Serializable
10
+ attribute :name, :string
11
+ attribute :first_cp, :integer
12
+ attribute :last_cp, :integer
13
+ attribute :range, :string
14
+ attribute :total, :integer
15
+ attribute :covered, :integer
16
+ attribute :fill_ratio, :float
17
+ attribute :complete, Lutaml::Model::Type::Boolean
18
+
19
+ key_value do
20
+ map "name", to: :name
21
+ map "first_cp", to: :first_cp
22
+ map "last_cp", to: :last_cp
23
+ map "range", to: :range
24
+ map "total", to: :total
25
+ map "covered", to: :covered
26
+ map "fill_ratio", to: :fill_ratio
27
+ map "complete", to: :complete
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ module Audit
8
+ # Structural diff between two AuditReports.
9
+ #
10
+ # `left_source`/`right_source` are the original source_file paths
11
+ # (or report paths) so a consumer reading the diff alone can locate
12
+ # the inputs.
13
+ #
14
+ # `field_changes` lists scalar fields whose values changed.
15
+ # `codepoints` is the cmap delta (CodepointSetDiff).
16
+ # The remaining fields are array set-diffs over the report's
17
+ # structural inventory: OpenType features, scripts, UCD blocks, and
18
+ # CLDR languages. Each is split into `added_*` (in right, not left)
19
+ # and `removed_*` (in left, not right).
20
+ class AuditDiff < Lutaml::Model::Serializable
21
+ attribute :left_source, :string
22
+ attribute :right_source, :string
23
+ attribute :field_changes, FieldChange, collection: true
24
+ attribute :codepoints, CodepointSetDiff
25
+ attribute :added_features, :string, collection: true
26
+ attribute :removed_features, :string, collection: true
27
+ attribute :added_scripts, :string, collection: true
28
+ attribute :removed_scripts, :string, collection: true
29
+ attribute :added_blocks, :string, collection: true
30
+ attribute :removed_blocks, :string, collection: true
31
+ attribute :added_languages, :string, collection: true
32
+ attribute :removed_languages, :string, collection: true
33
+
34
+ key_value do
35
+ map "left_source", to: :left_source
36
+ map "right_source", to: :right_source
37
+ map "field_changes", to: :field_changes
38
+ map "codepoints", to: :codepoints
39
+ map "added_features", to: :added_features
40
+ map "removed_features", to: :removed_features
41
+ map "added_scripts", to: :added_scripts
42
+ map "removed_scripts", to: :removed_scripts
43
+ map "added_blocks", to: :added_blocks
44
+ map "removed_blocks", to: :removed_blocks
45
+ map "added_languages", to: :added_languages
46
+ map "removed_languages", to: :removed_languages
47
+ end
48
+
49
+ # True when nothing differs. Useful for the text formatter.
50
+ #
51
+ # @return [Boolean]
52
+ def empty?
53
+ collection_empty?(field_changes) &&
54
+ added_codepoints.zero? && removed_codepoints.zero? &&
55
+ collection_empty?(added_features) && collection_empty?(removed_features) &&
56
+ collection_empty?(added_scripts) && collection_empty?(removed_scripts) &&
57
+ collection_empty?(added_blocks) && collection_empty?(removed_blocks) &&
58
+ collection_empty?(added_languages) && collection_empty?(removed_languages)
59
+ end
60
+
61
+ def added_codepoints
62
+ codepoints&.added_count || 0
63
+ end
64
+
65
+ def removed_codepoints
66
+ codepoints&.removed_count || 0
67
+ end
68
+
69
+ private
70
+
71
+ def collection_empty?(value)
72
+ value.nil? || value.empty?
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ module Audit
8
+ # Complete font audit report for a single face.
9
+ #
10
+ # Self-describing: one face per file. Carries source provenance
11
+ # (`source_file`, `source_sha256`, `font_index`, `num_fonts_in_source`)
12
+ # so a consumer reading a single face report knows whether the
13
+ # source was a standalone font or a collection face, and can locate
14
+ # siblings via the source hash.
15
+ #
16
+ # Constructed by Commands::AuditCommand. The model is passive —
17
+ # no font-parsing logic lives here.
18
+ class AuditReport < Lutaml::Model::Serializable
19
+ # Provenance
20
+ attribute :generated_at, :string
21
+ attribute :fontisan_version, :string
22
+ attribute :source_file, :string
23
+ attribute :source_sha256, :string
24
+ attribute :source_format, :string
25
+
26
+ # Source layout
27
+ attribute :font_index, :integer
28
+ attribute :num_fonts_in_source, :integer
29
+
30
+ # Identity (name table)
31
+ attribute :family_name, :string
32
+ attribute :subfamily_name, :string
33
+ attribute :full_name, :string
34
+ attribute :postscript_name, :string
35
+ attribute :version, :string
36
+ attribute :font_revision, :float
37
+
38
+ # Style (OS/2 + head)
39
+ attribute :weight_class, :integer
40
+ attribute :width_class, :integer
41
+ attribute :italic, Lutaml::Model::Type::Boolean
42
+ attribute :bold, Lutaml::Model::Type::Boolean
43
+ attribute :panose, :string
44
+
45
+ # Coverage
46
+ attribute :total_codepoints, :integer
47
+ attribute :total_glyphs, :integer
48
+ attribute :cmap_subtables, :integer, collection: true
49
+ attribute :codepoint_ranges, CodepointRange, collection: true
50
+ attribute :codepoints, :string, collection: true
51
+
52
+ # Aggregations (require UCD)
53
+ attribute :ucd_version, :string
54
+ attribute :blocks, AuditBlock, collection: true
55
+ attribute :unicode_scripts, :string, collection: true
56
+
57
+ # Per-language coverage (requires CLDR; opt-in via
58
+ # `--with-language-coverage`)
59
+ attribute :cldr_version, :string
60
+ attribute :language_coverage, Models::Cldr::LanguageCoverage, collection: true
61
+
62
+ # Licensing + embedding permissions (nil for Type 1)
63
+ attribute :licensing, Licensing
64
+
65
+ # Layout-critical metrics from head/hhea/OS/2/post (nil for Type 1)
66
+ attribute :metrics, Metrics
67
+
68
+ # Hinting summary from fpgm/prep/cvt/gasp/CFF charstrings (nil for Type 1)
69
+ attribute :hinting, Hinting
70
+
71
+ # Color-font capability summary from COLR/CPAL/SVG/CBDT/CBLC/sbix
72
+ # (nil for Type 1)
73
+ attribute :color_capabilities, ColorCapabilities
74
+
75
+ # Variable-font detail from fvar + variation side-tables
76
+ # (nil for non-variable faces and Type 1)
77
+ attribute :variation, VariationDetail
78
+
79
+ # OpenType layout summary from GSUB + GPOS
80
+ # (nil for Type 1)
81
+ attribute :opentype_layout, OpenTypeLayout
82
+
83
+ # Set when UCD download failed or any non-fatal issue was encountered.
84
+ attribute :warning, :string
85
+
86
+ key_value do
87
+ # Provenance
88
+ map "generated_at", to: :generated_at
89
+ map "fontisan_version", to: :fontisan_version
90
+ map "source_file", to: :source_file
91
+ map "source_sha256", to: :source_sha256
92
+ map "source_format", to: :source_format
93
+
94
+ # Source layout
95
+ map "font_index", to: :font_index
96
+ map "num_fonts_in_source", to: :num_fonts_in_source
97
+
98
+ # Identity
99
+ map "family_name", to: :family_name
100
+ map "subfamily_name", to: :subfamily_name
101
+ map "full_name", to: :full_name
102
+ map "postscript_name", to: :postscript_name
103
+ map "version", to: :version
104
+ map "font_revision", to: :font_revision
105
+
106
+ # Style
107
+ map "weight_class", to: :weight_class
108
+ map "width_class", to: :width_class
109
+ map "italic", to: :italic
110
+ map "bold", to: :bold
111
+ map "panose", to: :panose
112
+
113
+ # Coverage
114
+ map "total_codepoints", to: :total_codepoints
115
+ map "total_glyphs", to: :total_glyphs
116
+ map "cmap_subtables", to: :cmap_subtables
117
+ map "codepoint_ranges", to: :codepoint_ranges
118
+ map "codepoints", to: :codepoints
119
+
120
+ # Aggregations
121
+ map "ucd_version", to: :ucd_version
122
+ map "blocks", to: :blocks
123
+ map "unicode_scripts", to: :unicode_scripts
124
+
125
+ # CLDR per-language coverage
126
+ map "cldr_version", to: :cldr_version
127
+ map "language_coverage", to: :language_coverage
128
+
129
+ # Licensing
130
+ map "licensing", to: :licensing
131
+
132
+ # Metrics
133
+ map "metrics", to: :metrics
134
+
135
+ # Hinting
136
+ map "hinting", to: :hinting
137
+
138
+ # Color capabilities
139
+ map "color_capabilities", to: :color_capabilities
140
+
141
+ # Variation detail
142
+ map "variation", to: :variation
143
+
144
+ # OpenType layout
145
+ map "opentype_layout", to: :opentype_layout
146
+
147
+ # Warning
148
+ map "warning", to: :warning
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ module Audit
8
+ # A contiguous run of covered codepoints.
9
+ #
10
+ # `first_cp`/`last_cp` are inclusive integer endpoints. A single-codepoint
11
+ # "range" has first_cp == last_cp and renders as `U+XXXX` (no dash).
12
+ #
13
+ # Produced by {Audit::CodepointRangeCoalescer} from the cmap coverage.
14
+ # The range view replaces the previous flat per-codepoint list as the
15
+ # default report shape — a 60k-codepoint CJK font produces tens of
16
+ # ranges rather than 60k strings.
17
+ class CodepointRange < Lutaml::Model::Serializable
18
+ attribute :first_cp, :integer
19
+ attribute :last_cp, :integer
20
+
21
+ key_value do
22
+ map "first_cp", to: :first_cp
23
+ map "last_cp", to: :last_cp
24
+ end
25
+
26
+ # Human-readable form: `U+XXXX` for single codepoints,
27
+ # `U+XXXX-U+XXXX` for true ranges.
28
+ #
29
+ # @return [String]
30
+ def to_s
31
+ if first_cp == last_cp
32
+ format("U+%04<cp>X", cp: first_cp)
33
+ else
34
+ format("U+%04<first>X-U+%04<last>X", first: first_cp, last: last_cp)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ module Audit
8
+ # Diff between two cmap codepoint sets.
9
+ #
10
+ # `added`/`removed` are compact range lists (CodepointRange) so a
11
+ # large delta (e.g. CJK extension added) renders as a handful of
12
+ # ranges rather than thousands of codepoints.
13
+ #
14
+ # `unchanged_count` is the intersection size — useful as a sanity
15
+ # check that the two reports share enough coverage to be meaningfully
16
+ # comparable.
17
+ class CodepointSetDiff < Lutaml::Model::Serializable
18
+ attribute :added, CodepointRange, collection: true
19
+ attribute :removed, CodepointRange, collection: true
20
+ attribute :added_count, :integer
21
+ attribute :removed_count, :integer
22
+ attribute :unchanged_count, :integer
23
+
24
+ key_value do
25
+ map "added", to: :added
26
+ map "removed", to: :removed
27
+ map "added_count", to: :added_count
28
+ map "removed_count", to: :removed_count
29
+ map "unchanged_count", to: :unchanged_count
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ module Audit
8
+ # Color-font capability summary for one face.
9
+ #
10
+ # Answers: is this a color font, and if so, which format(s)?
11
+ # Modern color font formats are mutually exclusive in design but a
12
+ # single face can carry more than one (e.g. NotoColorEmoji ships
13
+ # COLR + CBDT + SVG so legacy and modern stacks all render).
14
+ #
15
+ # `color_formats` is derived at extraction time so consumers read a
16
+ # flat string list instead of re-deriving from the boolean lattice.
17
+ # Empty array ⇒ no color support.
18
+ class ColorCapabilities < Lutaml::Model::Serializable
19
+ # COLR (vector color glyphs).
20
+ attribute :has_colr, Lutaml::Model::Type::Boolean
21
+ attribute :colr_version, :integer # 0 or 1
22
+ attribute :colr_base_glyph_count, :integer
23
+ attribute :colr_layer_count, :integer
24
+
25
+ # CPAL (color palette).
26
+ attribute :has_cpal, Lutaml::Model::Type::Boolean
27
+ attribute :cpal_palette_count, :integer
28
+ attribute :cpal_color_count, :integer
29
+
30
+ # SVG-in-OpenType.
31
+ attribute :has_svg, Lutaml::Model::Type::Boolean
32
+ attribute :svg_document_count, :integer
33
+
34
+ # CBDT/CBLC (color bitmaps — paired tables).
35
+ attribute :has_cbdt, Lutaml::Model::Type::Boolean
36
+ attribute :has_cblc, Lutaml::Model::Type::Boolean
37
+ # Strike count comes from the paired CBLC locator table.
38
+ attribute :cbdt_strike_count, :integer
39
+
40
+ # sbix (Apple color bitmaps).
41
+ attribute :has_sbix, Lutaml::Model::Type::Boolean
42
+ attribute :sbix_strike_count, :integer
43
+
44
+ # Derived: ordered list of active color format tags. One of
45
+ # "colr_v0", "colr_v1", "cpal", "svg", "cbdt", "sbix".
46
+ attribute :color_formats, :string, collection: true
47
+
48
+ key_value do
49
+ map "has_colr", to: :has_colr
50
+ map "colr_version", to: :colr_version
51
+ map "colr_base_glyph_count", to: :colr_base_glyph_count
52
+ map "colr_layer_count", to: :colr_layer_count
53
+ map "has_cpal", to: :has_cpal
54
+ map "cpal_palette_count", to: :cpal_palette_count
55
+ map "cpal_color_count", to: :cpal_color_count
56
+ map "has_svg", to: :has_svg
57
+ map "svg_document_count", to: :svg_document_count
58
+ map "has_cbdt", to: :has_cbdt
59
+ map "has_cblc", to: :has_cblc
60
+ map "cbdt_strike_count", to: :cbdt_strike_count
61
+ map "has_sbix", to: :has_sbix
62
+ map "sbix_strike_count", to: :sbix_strike_count
63
+ map "color_formats", to: :color_formats
64
+ end
65
+
66
+ # Canonical format tags, kept in spec order.
67
+ FORMAT_COLR_V0 = "colr_v0"
68
+ FORMAT_COLR_V1 = "colr_v1"
69
+ FORMAT_CPAL = "cpal"
70
+ FORMAT_SVG = "svg"
71
+ FORMAT_CBDT = "cbdt"
72
+ FORMAT_SBIX = "sbix"
73
+
74
+ # Derive the canonical color_formats list from individual flags.
75
+ # COLR v1 takes precedence over v0 — a v1 table can serve both.
76
+ #
77
+ # @return [Array<String>]
78
+ def self.derive_formats(has_colr:, colr_version:, has_cpal:,
79
+ has_svg:, has_cbdt:, has_sbix:)
80
+ [].tap do |arr|
81
+ if has_colr
82
+ arr << (colr_version == 1 ? FORMAT_COLR_V1 : FORMAT_COLR_V0)
83
+ end
84
+ arr << FORMAT_CPAL if has_cpal
85
+ arr << FORMAT_SVG if has_svg
86
+ arr << FORMAT_CBDT if has_cbdt
87
+ arr << FORMAT_SBIX if has_sbix
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end