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,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Formatters
5
+ # Human-readable diff of two {Models::Audit::AuditReport}s.
6
+ #
7
+ # Output groups changes by kind: scalar field changes, codepoint set
8
+ # deltas (added/removed counts and a preview of the ranges), then
9
+ # structural inventory changes (scripts, features, blocks, languages).
10
+ # Empty sections are omitted so a no-op diff prints only the header.
11
+ class AuditDiffTextRenderer
12
+ SEPARATOR = "=" * 80
13
+ LIST_LIMIT = 10
14
+
15
+ # @param diff [Models::Audit::AuditDiff]
16
+ def initialize(diff)
17
+ @diff = diff
18
+ @lines = []
19
+ end
20
+
21
+ # @return [String]
22
+ def render
23
+ render_header
24
+ render_field_changes
25
+ render_codepoint_delta
26
+ render_structural_changes
27
+ render_empty_note
28
+ @lines.join("\n")
29
+ end
30
+
31
+ private
32
+
33
+ def render_header
34
+ @lines << "AUDIT DIFF"
35
+ @lines << SEPARATOR
36
+ @lines << "left: #{@diff.left_source}"
37
+ @lines << "right: #{@diff.right_source}"
38
+ end
39
+
40
+ def render_field_changes
41
+ changes = Array(@diff.field_changes)
42
+ return if changes.empty?
43
+
44
+ section("FIELD CHANGES (#{changes.size})")
45
+ changes.each do |change|
46
+ @lines << " #{change.field}: #{change.left.inspect} → #{change.right.inspect}"
47
+ end
48
+ end
49
+
50
+ def render_codepoint_delta
51
+ delta = @diff.codepoints
52
+ return unless delta && (delta.added_count.to_i.positive? || delta.removed_count.to_i.positive?)
53
+
54
+ section("CODEPOINT COVERAGE")
55
+ @lines << " added: #{delta.added_count}"
56
+ @lines << " removed: #{delta.removed_count}"
57
+ @lines << " unchanged: #{delta.unchanged_count}"
58
+ preview_added(delta)
59
+ preview_removed(delta)
60
+ end
61
+
62
+ def preview_added(delta)
63
+ ranges = Array(delta.added)
64
+ return if ranges.empty?
65
+
66
+ @lines << " + #{format_ranges(ranges)}"
67
+ end
68
+
69
+ def preview_removed(delta)
70
+ ranges = Array(delta.removed)
71
+ return if ranges.empty?
72
+
73
+ @lines << " - #{format_ranges(ranges)}"
74
+ end
75
+
76
+ def render_structural_changes
77
+ render_set("SCRIPTS", @diff.added_scripts, @diff.removed_scripts)
78
+ render_set("FEATURES", @diff.added_features, @diff.removed_features)
79
+ render_set("BLOCKS", @diff.added_blocks, @diff.removed_blocks)
80
+ render_set("LANGUAGES", @diff.added_languages, @diff.removed_languages)
81
+ end
82
+
83
+ def render_set(name, added, removed)
84
+ added = Array(added)
85
+ removed = Array(removed)
86
+ return if added.empty? && removed.empty?
87
+
88
+ section("#{name} CHANGES")
89
+ @lines << " + #{truncate(added)}" unless added.empty?
90
+ @lines << " - #{truncate(removed)}" unless removed.empty?
91
+ end
92
+
93
+ def render_empty_note
94
+ return unless @diff.empty?
95
+
96
+ @lines << ""
97
+ @lines << "(no differences)"
98
+ end
99
+
100
+ # ---- helpers --------------------------------------------------------
101
+
102
+ def section(title)
103
+ @lines << ""
104
+ @lines << title
105
+ end
106
+
107
+ def truncate(list)
108
+ shown = list.first(LIST_LIMIT).join(", ")
109
+ shown += ", ..." if list.size > LIST_LIMIT
110
+ shown
111
+ end
112
+
113
+ def format_ranges(ranges)
114
+ shown = ranges.first(LIST_LIMIT).map do |r|
115
+ "U+#{format('%04X', r.first_cp)}-U+#{format('%04X', r.last_cp)}"
116
+ end.join(", ")
117
+ shown += ", ..." if ranges.size > LIST_LIMIT
118
+ shown
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,324 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Formatters
5
+ # Human-readable, sectioned view of an {Models::Audit::AuditReport}.
6
+ #
7
+ # The text formatter is the default `--format text` output for
8
+ # `fontisan audit`. Complements YAML/JSON (machine-facing) with a
9
+ # terse, scannable terminal view. Every section is nil-safe so the
10
+ # same renderer covers full OpenType/TrueType faces, Type 1 fonts
11
+ # (no OS/2, no metrics, no layout), and partial reports.
12
+ class AuditTextRenderer
13
+ SEPARATOR = "=" * 80
14
+ LABEL_WIDTH = 18
15
+ LIST_LIMIT = 10
16
+
17
+ WIDTH_NAMES = {
18
+ 1 => "Ultra-condensed", 2 => "Extra-condensed", 3 => "Condensed",
19
+ 4 => "Semi-condensed", 5 => "Medium", 6 => "Semi-expanded",
20
+ 7 => "Expanded", 8 => "Extra-expanded", 9 => "Ultra-expanded"
21
+ }.freeze
22
+
23
+ # @param report [Models::Audit::AuditReport]
24
+ def initialize(report)
25
+ @report = report
26
+ @lines = []
27
+ end
28
+
29
+ # @return [String]
30
+ def render
31
+ render_header
32
+ render_identity
33
+ render_style
34
+ render_metrics
35
+ render_coverage
36
+ render_blocks
37
+ render_licensing
38
+ render_hinting
39
+ render_color
40
+ render_variation
41
+ render_opentype_layout
42
+ render_language_coverage
43
+ render_warnings
44
+ @lines.join("\n")
45
+ end
46
+
47
+ private
48
+
49
+ def render_header
50
+ @lines << (@report.postscript_name || @report.family_name || "(unknown)")
51
+ @lines << SEPARATOR
52
+ @lines << two_col("generated_at:", @report.generated_at,
53
+ "fontisan:", @report.fontisan_version)
54
+ @lines << "source_sha256: #{@report.source_sha256}"
55
+ @lines << "source_file: #{@report.source_file}"
56
+ @lines << two_col("source_format:", @report.source_format,
57
+ "layout:", layout_descriptor)
58
+ end
59
+
60
+ def layout_descriptor
61
+ if @report.num_fonts_in_source.nil? || @report.num_fonts_in_source <= 1
62
+ "single face (1/1)"
63
+ else
64
+ format("collection face (%<idx>d/%<total>d)",
65
+ idx: (@report.font_index || 0) + 1,
66
+ total: @report.num_fonts_in_source)
67
+ end
68
+ end
69
+
70
+ def render_identity
71
+ section("IDENTITY")
72
+ row("Family", @report.family_name)
73
+ row("Subfamily", @report.subfamily_name)
74
+ row("Full name", @report.full_name)
75
+ row("PostScript", @report.postscript_name)
76
+ row("Version", @report.version)
77
+ row("Revision", @report.font_revision)
78
+ end
79
+
80
+ def render_style
81
+ section("STYLE")
82
+ row("Weight class", weight_descriptor)
83
+ row("Width class", width_descriptor)
84
+ row("Bold", yes_no(@report.bold))
85
+ row("Italic", yes_no(@report.italic))
86
+ row("PANOSE", @report.panose)
87
+ end
88
+
89
+ def render_metrics
90
+ return unless @report.metrics
91
+
92
+ m = @report.metrics
93
+ section("METRICS")
94
+ row("unitsPerEm", m.units_per_em)
95
+ row("hhea", "ascent: #{m.hhea_ascent} / descent: #{m.hhea_descent} / line gap: #{m.hhea_line_gap}") if m.hhea_ascent
96
+ row("OS/2 typo", "ascent: #{m.typo_ascender} / descent: #{m.typo_descender} / line gap: #{m.typo_line_gap}") if m.typo_ascender
97
+ row("OS/2 win", "ascent: #{m.win_ascent} / descent: #{m.win_descent}") if m.win_ascent
98
+ row("x-height", m.x_height)
99
+ row("cap height", m.cap_height)
100
+ row("bbox", bbox_descriptor(m)) if m.bbox_x_min || m.bbox_x_max
101
+ end
102
+
103
+ def render_coverage
104
+ section("COVERAGE")
105
+ row("Codepoints", @report.total_codepoints)
106
+ row("Glyphs", @report.total_glyphs)
107
+ row("cmap subtables", format("%s", Array(@report.cmap_subtables).join(", "))) unless Array(@report.cmap_subtables).empty?
108
+ row("Ranges (top #{LIST_LIMIT})", codepoint_range_preview)
109
+ row("Unicode scripts", truncate_list(@report.unicode_scripts))
110
+ end
111
+
112
+ def render_blocks
113
+ blocks = Array(@report.blocks)
114
+ return if blocks.empty?
115
+
116
+ section("UNICODE BLOCKS (top #{LIST_LIMIT} by fill ratio)")
117
+ blocks.sort_by { |b| -(b.fill_ratio || 0) }.first(LIST_LIMIT).each do |block|
118
+ ratio = block.fill_ratio ? format("%<r>d%%", r: (block.fill_ratio * 100).round) : "?"
119
+ @lines << format(" %<name>-40s %<covered>d/%<total>d (%<ratio>s)",
120
+ name: "#{block.name}:", covered: block.covered || 0,
121
+ total: block.total || 0, ratio: ratio)
122
+ end
123
+ end
124
+
125
+ def render_licensing
126
+ return unless @report.licensing
127
+
128
+ l = @report.licensing
129
+ section("LICENSING")
130
+ row("Copyright", l.copyright)
131
+ row("Trademark", l.trademark)
132
+ row("Manufacturer", l.manufacturer)
133
+ row("Designer", l.designer)
134
+ row("License", l.license_description)
135
+ row("License URL", l.license_url)
136
+ row("Vendor URL", l.vendor_url)
137
+ row("Designer URL", l.designer_url)
138
+ row("Vendor ID", l.vendor_id)
139
+ row("Embedding", l.embedding_type)
140
+ end
141
+
142
+ def render_hinting
143
+ return unless @report.hinting
144
+
145
+ h = @report.hinting
146
+ section("HINTING")
147
+ row("Format", h.hinting_format || (h.is_unhinted ? "unhinted" : "unknown"))
148
+ row("fpgm", instruction_line(h.has_fpgm, h.fpgm_instruction_count))
149
+ row("prep", instruction_line(h.has_prep, h.prep_instruction_count))
150
+ row("cvt", cvt_line(h))
151
+ row("gasp", gasp_line(h))
152
+ row("CFF hints", h.cff_hint_count)
153
+ end
154
+
155
+ def render_color
156
+ return unless @report.color_capabilities
157
+
158
+ c = @report.color_capabilities
159
+ section("COLOR")
160
+ formats = Array(c.color_formats)
161
+ row("Color formats", formats.empty? ? "(none)" : truncate_list(formats))
162
+ row("COLR", colr_line(c)) if c.has_colr
163
+ row("CPAL", "palettes: #{c.cpal_palette_count}, colors: #{c.cpal_color_count}") if c.has_cpal
164
+ row("SVG documents", c.svg_document_count) if c.has_svg && c.svg_document_count
165
+ row("CBDT strikes", c.cbdt_strike_count) if c.has_cbdt && c.cbdt_strike_count
166
+ row("sbix strikes", c.sbix_strike_count) if c.has_sbix && c.sbix_strike_count
167
+ end
168
+
169
+ def render_variation
170
+ v = @report.variation
171
+ section("VARIABLE FONT")
172
+ if v.nil? || Array(v.axes).empty?
173
+ @lines << " (not variable)"
174
+ return
175
+ end
176
+
177
+ v.axes.each do |axis|
178
+ row(axis.tag, format("%<min>s .. %<max>s default %<default>s",
179
+ min: axis.min_value, max: axis.max_value,
180
+ default: axis.default_value))
181
+ end
182
+ return if Array(v.named_instances).empty?
183
+
184
+ @lines << " Named instances:"
185
+ v.named_instances.each do |inst|
186
+ @lines << " #{inst.postscript_name || inst.subfamily_name}: #{inst.coordinates}"
187
+ end
188
+ end
189
+
190
+ def render_opentype_layout
191
+ return unless @report.opentype_layout
192
+
193
+ l = @report.opentype_layout
194
+ section("OPENTYPE LAYOUT")
195
+ row("GSUB", yes_no(l.has_gsub))
196
+ row("GPOS", yes_no(l.has_gpos))
197
+ row("Scripts (#{Array(l.scripts).size})", truncate_list(l.scripts))
198
+ row("Features (#{Array(l.features).size})", truncate_list(l.features))
199
+ end
200
+
201
+ def render_language_coverage
202
+ langs = Array(@report.language_coverage)
203
+ return if langs.empty?
204
+
205
+ section("LANGUAGE COVERAGE (CLDR #{@report.cldr_version})")
206
+ langs.first(LIST_LIMIT).each do |lang|
207
+ pct = lang.coverage_ratio ? format("%<r>d%%", r: (lang.coverage_ratio * 100).round) : "?"
208
+ mark = lang.fully_supported ? "*" : " "
209
+ @lines << format(" %<mark>s %<lang>-8s %<covered>d/%<total>d (%<pct>s)",
210
+ mark: mark, lang: "#{lang.language}:", covered: lang.covered,
211
+ total: lang.total, pct: pct)
212
+ end
213
+ end
214
+
215
+ def render_warnings
216
+ section("WARNINGS")
217
+ @lines << if @report.warning
218
+ " #{@report.warning}"
219
+ else
220
+ " (none)"
221
+ end
222
+ end
223
+
224
+ # ---- formatting helpers --------------------------------------------
225
+
226
+ def section(title)
227
+ @lines << ""
228
+ @lines << title
229
+ end
230
+
231
+ def row(label, value)
232
+ return if value.nil?
233
+ return if value.is_a?(String) && value.empty?
234
+
235
+ @lines << " #{label}:#{' ' * [LABEL_WIDTH - label.to_s.length - 1, 1].max}#{value}"
236
+ end
237
+
238
+ def two_col(left_label, left_value, right_label, right_value)
239
+ left = "#{left_label} #{left_value}".ljust(40)
240
+ "#{left}#{right_label} #{right_value}"
241
+ end
242
+
243
+ def yes_no(bool)
244
+ bool ? "yes" : "no"
245
+ end
246
+
247
+ def truncate_list(items)
248
+ list = Array(items)
249
+ return "(none)" if list.empty?
250
+
251
+ shown = list.first(LIST_LIMIT).join(", ")
252
+ shown += ", ..." if list.size > LIST_LIMIT
253
+ shown
254
+ end
255
+
256
+ def weight_descriptor
257
+ return nil unless @report.weight_class
258
+
259
+ name = weight_name(@report.weight_class)
260
+ "#{@report.weight_class}#{" (#{name})" if name}"
261
+ end
262
+
263
+ def width_descriptor
264
+ return nil unless @report.width_class
265
+
266
+ name = WIDTH_NAMES[@report.width_class]
267
+ "#{@report.width_class}#{" (#{name})" if name}"
268
+ end
269
+
270
+ def weight_name(value)
271
+ case value
272
+ when 100 then "Thin"
273
+ when 200 then "Extra-light"
274
+ when 300 then "Light"
275
+ when 400 then "Regular"
276
+ when 500 then "Medium"
277
+ when 600 then "Semi-bold"
278
+ when 700 then "Bold"
279
+ when 800 then "Extra-bold"
280
+ when 900 then "Black"
281
+ end
282
+ end
283
+
284
+ def bbox_descriptor(metrics)
285
+ "(#{metrics.bbox_x_min}, #{metrics.bbox_y_min}) → (#{metrics.bbox_x_max}, #{metrics.bbox_y_max})"
286
+ end
287
+
288
+ def codepoint_range_preview
289
+ ranges = Array(@report.codepoint_ranges)
290
+ return "(none)" if ranges.empty?
291
+
292
+ shown = ranges.first(LIST_LIMIT).map do |r|
293
+ "U+#{format('%04X', r.first_cp)}-U+#{format('%04X', r.last_cp)}"
294
+ end.join(", ")
295
+ shown += ", ..." if ranges.size > LIST_LIMIT
296
+ shown
297
+ end
298
+
299
+ def instruction_line(has, count)
300
+ return "no" unless has
301
+
302
+ count ? "#{count} instructions" : "present"
303
+ end
304
+
305
+ def cvt_line(hinting)
306
+ return "no" unless hinting.has_cvt
307
+
308
+ hinting.cvt_entry_count ? "#{hinting.cvt_entry_count} entries" : "present"
309
+ end
310
+
311
+ def gasp_line(hinting)
312
+ ranges = Array(hinting.gasp_ranges)
313
+ return "no" if ranges.empty?
314
+
315
+ ppems = ranges.map(&:max_ppem).compact
316
+ "#{ranges.size} ranges (#{ppems.join('/')} ppem)"
317
+ end
318
+
319
+ def colr_line(color)
320
+ "v#{color.colr_version}, #{color.colr_base_glyph_count} base glyphs, #{color.colr_layer_count} layers"
321
+ end
322
+ end
323
+ end
324
+ end
@@ -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