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,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
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ module Audit
8
+ # Group of files that share an identical `source_sha256`.
9
+ #
10
+ # Detecting duplicate byte-identical files (regardless of filename)
11
+ # is the cheapest form of library hygiene: same bytes = same font.
12
+ class DuplicateGroup < Lutaml::Model::Serializable
13
+ attribute :source_sha256, :string
14
+ attribute :files, :string, collection: true
15
+
16
+ key_value do
17
+ map "source_sha256", to: :source_sha256
18
+ map "files", to: :files
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ module Audit
8
+ # Decoded OS/2 fsType bitfield → canonical embedding-permission string.
9
+ #
10
+ # Per OpenType spec, fsType is a bitfield. Only one of bits 0-3 should
11
+ # be set (the basic permission level); bits 4-7 are modifiers that
12
+ # only apply when INSTALLABLE (bit 3) is set.
13
+ #
14
+ # The decoder normalizes to one of seven canonical strings so
15
+ # downstream consumers don't need to know the bit layout.
16
+ class EmbeddingType < Lutaml::Model::Serializable
17
+ # Bit masks (OpenType fsType bitfield).
18
+ RESTRICTED_LICENSE_NO_EMBEDDING = 0x0001
19
+ PREVIEW_AND_PRINT = 0x0002
20
+ EDITABLE_EMBEDDING = 0x0004
21
+ INSTALLABLE_EMBEDDING = 0x0008
22
+ NO_SUBSETTING = 0x0100
23
+ BITMAP_EMBEDDING_ONLY = 0x0200
24
+
25
+ attribute :value, :string
26
+
27
+ # Decoded canonical string for the given fsType bitfield.
28
+ #
29
+ # @param fs_type [Integer, nil] raw OS/2 fsType value
30
+ # @return [String, nil] canonical permission name, or nil when
31
+ # fs_type is nil
32
+ def self.decode(fs_type)
33
+ return nil if fs_type.nil?
34
+
35
+ if fs_type & RESTRICTED_LICENSE_NO_EMBEDDING != 0
36
+ "restricted_license"
37
+ elsif fs_type & PREVIEW_AND_PRINT != 0
38
+ "preview_print"
39
+ elsif fs_type & EDITABLE_EMBEDDING != 0
40
+ "editable"
41
+ elsif fs_type & INSTALLABLE_EMBEDDING != 0
42
+ installable_subcategory(fs_type)
43
+ elsif fs_type.zero?
44
+ "installable"
45
+ else
46
+ "unknown"
47
+ end
48
+ end
49
+
50
+ # Construct from a decoded canonical string.
51
+ #
52
+ # @param fs_type [Integer, nil]
53
+ def self.from_fs_type(fs_type)
54
+ new(value: decode(fs_type))
55
+ end
56
+
57
+ def to_s
58
+ value
59
+ end
60
+
61
+ def self.installable_subcategory(fs_type)
62
+ if fs_type & NO_SUBSETTING != 0 && fs_type & BITMAP_EMBEDDING_ONLY != 0
63
+ "installable_no_subsetting_bitmap_only"
64
+ elsif fs_type & NO_SUBSETTING != 0
65
+ "installable_no_subsetting"
66
+ elsif fs_type & BITMAP_EMBEDDING_ONLY != 0
67
+ "installable_bitmap_only"
68
+ else
69
+ "installable"
70
+ end
71
+ end
72
+ private_class_method :installable_subcategory
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ module Audit
8
+ # One scalar field that differs between two AuditReports.
9
+ #
10
+ # `field` is the dotted attribute name (e.g. "weight_class").
11
+ # `left`/`right` are stringified values: nil → "", String → itself,
12
+ # anything else → its YAML form. Comparing the YAML form of nested
13
+ # models is intentionally avoided here — those diffs surface as
14
+ # structural add/remove lists on AuditDiff itself.
15
+ class FieldChange < Lutaml::Model::Serializable
16
+ attribute :field, :string
17
+ attribute :left, :string
18
+ attribute :right, :string
19
+
20
+ key_value do
21
+ map "field", to: :field
22
+ map "left", to: :left
23
+ map "right", to: :right
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ module Audit
8
+ # Decoded OS/2 fsSelection bitfield → sorted array of flag names.
9
+ #
10
+ # Per OpenType spec, fsSelection is a bitfield with these bits:
11
+ #
12
+ # bit 0 (0x001): italic
13
+ # bit 1 (0x002): underscore
14
+ # bit 2 (0x004): negative
15
+ # bit 3 (0x008): outlined
16
+ # bit 4 (0x010): strikeout
17
+ # bit 5 (0x020): bold
18
+ # bit 6 (0x040): regular
19
+ # bit 7 (0x080): use_typo_metrics
20
+ # bit 8 (0x100): wws
21
+ # bit 9 (0x200): oblique
22
+ #
23
+ # Returns names in spec order (bit ascending).
24
+ class FsSelectionFlags < Lutaml::Model::Serializable
25
+ FLAGS = {
26
+ 0x001 => "italic",
27
+ 0x002 => "underscore",
28
+ 0x004 => "negative",
29
+ 0x008 => "outlined",
30
+ 0x010 => "strikeout",
31
+ 0x020 => "bold",
32
+ 0x040 => "regular",
33
+ 0x080 => "use_typo_metrics",
34
+ 0x100 => "wws",
35
+ 0x200 => "oblique",
36
+ }.freeze
37
+
38
+ attribute :flags, :string, collection: true
39
+
40
+ # Decoded array of flag names in spec order (bit ascending).
41
+ #
42
+ # @param fs_selection [Integer, nil] raw OS/2 fsSelection value
43
+ # @return [Array<String>, nil]
44
+ def self.decode(fs_selection)
45
+ return nil if fs_selection.nil?
46
+
47
+ FLAGS.each_with_object([]) do |(mask, name), acc|
48
+ acc << name if fs_selection & mask != 0
49
+ end
50
+ end
51
+
52
+ # Construct from a raw fsSelection value.
53
+ #
54
+ # @param fs_selection [Integer, nil]
55
+ def self.from_fs_selection(fs_selection)
56
+ new(flags: decode(fs_selection))
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ module Audit
8
+ # One entry from the TrueType `gasp` (Grid-fitting And Scan-conversion
9
+ # Procedure) table.
10
+ #
11
+ # Each entry describes the hinting/smoothing policy that applies up to
12
+ # the given `max_ppem` (pixels-per-em). The OpenType spec defines four
13
+ # single-bit flags; the high 12 bits of the raw rangeFlags uint16 are
14
+ # reserved.
15
+ #
16
+ # Construct via {.from_flags} from the raw uint16 pair; never hand-build
17
+ # the bit decoding at call sites.
18
+ class GaspRange < Lutaml::Model::Serializable
19
+ # OpenType gasp rangeFlags bit masks.
20
+ GRIDFIT = 0x0001
21
+ DO_GRAY = 0x0002
22
+ SYMMETRIC_GRIDFIT = 0x0004
23
+ SYMMETRIC_SMOOTHING = 0x0008
24
+
25
+ attribute :max_ppem, :integer
26
+ attribute :gridfit, Lutaml::Model::Type::Boolean
27
+ attribute :do_gray, Lutaml::Model::Type::Boolean
28
+ attribute :symmetric_gridfit, Lutaml::Model::Type::Boolean
29
+ attribute :symmetric_smoothing, Lutaml::Model::Type::Boolean
30
+
31
+ key_value do
32
+ map "max_ppem", to: :max_ppem
33
+ map "gridfit", to: :gridfit
34
+ map "do_gray", to: :do_gray
35
+ map "symmetric_gridfit", to: :symmetric_gridfit
36
+ map "symmetric_smoothing", to: :symmetric_smoothing
37
+ end
38
+
39
+ # Build a GaspRange from the raw uint16 pair stored in the gasp table.
40
+ #
41
+ # @param max_ppem [Integer] rangeMaxPPEM (exclusive upper bound)
42
+ # @param flags [Integer] raw rangeFlags bitfield
43
+ # @return [GaspRange]
44
+ def self.from_flags(max_ppem, flags)
45
+ new(
46
+ max_ppem: max_ppem,
47
+ gridfit: (flags & GRIDFIT).positive?,
48
+ do_gray: (flags & DO_GRAY).positive?,
49
+ symmetric_gridfit: (flags & SYMMETRIC_GRIDFIT).positive?,
50
+ symmetric_smoothing: (flags & SYMMETRIC_SMOOTHING).positive?,
51
+ )
52
+ end
53
+
54
+ # Derived: both gridfit and do_gray are set. Mac historically treated
55
+ # this combination as "do everything". Not serialized — compute on
56
+ # demand.
57
+ def gridfit_and_smoothing?
58
+ gridfit && do_gray
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end