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,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
@@ -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
+ # Hinting summary for one face.
9
+ #
10
+ # Answers the practical questions a designer or QA engineer asks:
11
+ # "Is this font hinted at all? What flavour? How much hinting, by
12
+ # byte / instruction count?" Unhinted fonts render poorly at small
13
+ # sizes; heavily hinted fonts can be 20%+ bytecode by file size.
14
+ #
15
+ # TrueType hinting surfaces as the fpgm/prep/cvt programs plus the
16
+ # gasp per-ppem policy. CFF/CFF2 hinting surfaces as stem hints
17
+ # encoded inside each CharString. This model carries both, plus a
18
+ # derived `is_unhinted` flag and `hinting_format` classification so
19
+ # downstream tooling does not need to re-derive either.
20
+ #
21
+ # All counts are nil-safe: a face with no hinting at all produces
22
+ # `Hinting.new` with every field falsy/nil rather than raising.
23
+ class Hinting < Lutaml::Model::Serializable
24
+ # TrueType bytecode programs.
25
+ attribute :has_fpgm, Lutaml::Model::Type::Boolean
26
+ attribute :fpgm_instruction_count, :integer
27
+ attribute :has_prep, Lutaml::Model::Type::Boolean
28
+ attribute :prep_instruction_count, :integer
29
+
30
+ # TrueType Control Value Table (hinting metrics).
31
+ attribute :has_cvt, Lutaml::Model::Type::Boolean
32
+ attribute :cvt_entry_count, :integer
33
+
34
+ # CVT variation table for variable TrueType fonts. Carried for
35
+ # context only — never included in cvt_entry_count.
36
+ attribute :has_cvar, Lutaml::Model::Type::Boolean
37
+
38
+ # gasp policy ranges, ordered by ascending max_ppem.
39
+ attribute :gasp_ranges, GaspRange, collection: true
40
+
41
+ # CFF/CFF2 hinting. cff_has_private_dict is true for every CFF
42
+ # face (Private DICT is mandatory); cff_hint_count sums stem
43
+ # declarations across all CharStrings, nil when unparsable.
44
+ attribute :cff_has_private_dict, Lutaml::Model::Type::Boolean
45
+ attribute :cff_hint_count, :integer
46
+
47
+ # Derived at extraction time so consumers read flat fields.
48
+ attribute :is_unhinted, Lutaml::Model::Type::Boolean
49
+ attribute :hinting_format, :string
50
+
51
+ key_value do
52
+ map "has_fpgm", to: :has_fpgm
53
+ map "fpgm_instruction_count", to: :fpgm_instruction_count
54
+ map "has_prep", to: :has_prep
55
+ map "prep_instruction_count", to: :prep_instruction_count
56
+ map "has_cvt", to: :has_cvt
57
+ map "cvt_entry_count", to: :cvt_entry_count
58
+ map "has_cvar", to: :has_cvar
59
+ map "gasp_ranges", to: :gasp_ranges
60
+ map "cff_has_private_dict", to: :cff_has_private_dict
61
+ map "cff_hint_count", to: :cff_hint_count
62
+ map "is_unhinted", to: :is_unhinted
63
+ map "hinting_format", to: :hinting_format
64
+ end
65
+
66
+ FORMAT_TRUETYPE = "truetype"
67
+ FORMAT_CFF = "cff"
68
+ FORMAT_MIXED = "mixed"
69
+ FORMAT_NONE = "none"
70
+
71
+ # Derive {is_unhinted} and {hinting_format} from individual flags.
72
+ # Called by the extractor before construction so the values land
73
+ # in serialized output without recomputation at read time.
74
+ #
75
+ # gasp is a TrueType-specific table, so it counts toward the
76
+ # TrueType hinting bucket even when no fpgm/prep/cvt is present.
77
+ #
78
+ # @return [Hash] keys :is_unhinted, :hinting_format
79
+ def self.derive_flags(has_tt:, has_cff:, has_gasp:)
80
+ tt_hints = has_tt || has_gasp
81
+ any = tt_hints || has_cff
82
+ format =
83
+ if tt_hints && has_cff then FORMAT_MIXED
84
+ elsif tt_hints then FORMAT_TRUETYPE
85
+ elsif has_cff then FORMAT_CFF
86
+ else FORMAT_NONE
87
+ end
88
+ { is_unhinted: !any, hinting_format: format }
89
+ end
90
+ end
91
+ end
92
+ end
93
+ 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
+ # Aggregate view over a directory (tree) of audited fonts.
9
+ #
10
+ # Built by {Audit::LibraryAuditor}. Combines a flat list of
11
+ # per-face {AuditReport}s with derived cross-face rollups:
12
+ # script coverage matrix, duplicate detection (by source_sha256),
13
+ # and license distribution. Lets a librarian inventory a font
14
+ # collection in one pass.
15
+ class LibrarySummary < Lutaml::Model::Serializable
16
+ attribute :root_path, :string
17
+ attribute :total_files, :integer
18
+ attribute :total_faces, :integer
19
+ attribute :scanned_extensions, :string, collection: true
20
+ attribute :aggregate_metrics, :hash
21
+ attribute :script_coverage, ScriptCoverageRow, collection: true
22
+ attribute :duplicate_groups, DuplicateGroup, collection: true
23
+ attribute :license_distribution, :hash
24
+ attribute :per_face_reports, AuditReport, collection: true
25
+
26
+ key_value do
27
+ map "root_path", to: :root_path
28
+ map "total_files", to: :total_files
29
+ map "total_faces", to: :total_faces
30
+ map "scanned_extensions", to: :scanned_extensions
31
+ map "aggregate_metrics", to: :aggregate_metrics
32
+ map "script_coverage", to: :script_coverage
33
+ map "duplicate_groups", to: :duplicate_groups
34
+ map "license_distribution", to: :license_distribution
35
+ map "per_face_reports", to: :per_face_reports
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ module Audit
8
+ # Licensing + embedding + vendor provenance fields for a face.
9
+ #
10
+ # Combines the human-readable legal/identity fields from the name
11
+ # table with the machine-readable embedding permissions from OS/2.
12
+ # Type 1 fonts have no OS/2 — callers must tolerate a nil
13
+ # embedding_type / fs_selection_flags / vendor_id.
14
+ class Licensing < Lutaml::Model::Serializable
15
+ # Name-table fields (English name IDs)
16
+ attribute :copyright, :string
17
+ attribute :trademark, :string
18
+ attribute :manufacturer, :string
19
+ attribute :designer, :string
20
+ attribute :description, :string
21
+ attribute :vendor_url, :string
22
+ attribute :designer_url, :string
23
+ attribute :license_description, :string
24
+ attribute :license_url, :string
25
+
26
+ # OS/2 fields
27
+ attribute :vendor_id, :string
28
+ attribute :embedding_type, :string
29
+ attribute :fs_selection_flags, :string, collection: true
30
+
31
+ key_value do
32
+ map "copyright", to: :copyright
33
+ map "trademark", to: :trademark
34
+ map "manufacturer", to: :manufacturer
35
+ map "designer", to: :designer
36
+ map "description", to: :description
37
+ map "vendor_url", to: :vendor_url
38
+ map "designer_url", to: :designer_url
39
+ map "license_description", to: :license_description
40
+ map "license_url", to: :license_url
41
+ map "vendor_id", to: :vendor_id
42
+ map "embedding_type", to: :embedding_type
43
+ map "fs_selection_flags", to: :fs_selection_flags
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ module Audit
8
+ # Layout-critical metrics for a face, consolidated from head, hhea,
9
+ # OS/2, and post tables. Designers and engineers can read all
10
+ # spacing-relevant numbers in one place instead of cross-referencing
11
+ # raw table dumps.
12
+ #
13
+ # All fields are nil-safe — Type 1 fonts and stripped WOFF builds
14
+ # may not carry every table. Derived booleans (e.g. metrics_consistent?)
15
+ # tolerate nil inputs and return false rather than raising.
16
+ class Metrics < Lutaml::Model::Serializable
17
+ # head
18
+ attribute :units_per_em, :integer
19
+ attribute :bbox_x_min, :integer
20
+ attribute :bbox_y_min, :integer
21
+ attribute :bbox_x_max, :integer
22
+ attribute :bbox_y_max, :integer
23
+
24
+ # hhea (horizontal)
25
+ attribute :hhea_ascent, :integer
26
+ attribute :hhea_descent, :integer
27
+ attribute :hhea_line_gap, :integer
28
+
29
+ # OS/2 typo
30
+ attribute :typo_ascender, :integer
31
+ attribute :typo_descender, :integer
32
+ attribute :typo_line_gap, :integer
33
+
34
+ # OS/2 win
35
+ attribute :win_ascent, :integer
36
+ attribute :win_descent, :integer
37
+
38
+ # OS/2 v2+ (optional)
39
+ attribute :x_height, :integer
40
+ attribute :cap_height, :integer
41
+
42
+ # OS/2 subscript/superscript
43
+ attribute :subscript_x_size, :integer
44
+ attribute :subscript_y_size, :integer
45
+ attribute :subscript_x_offset, :integer
46
+ attribute :subscript_y_offset, :integer
47
+ attribute :superscript_x_size, :integer
48
+ attribute :superscript_y_size, :integer
49
+ attribute :superscript_x_offset, :integer
50
+ attribute :superscript_y_offset, :integer
51
+
52
+ # OS/2 strikeout
53
+ attribute :strikeout_size, :integer
54
+ attribute :strikeout_position, :integer
55
+
56
+ # post underline
57
+ attribute :underline_position, :float
58
+ attribute :underline_thickness, :float
59
+
60
+ key_value do
61
+ map "units_per_em", to: :units_per_em
62
+ map "bbox_x_min", to: :bbox_x_min
63
+ map "bbox_y_min", to: :bbox_y_min
64
+ map "bbox_x_max", to: :bbox_x_max
65
+ map "bbox_y_max", to: :bbox_y_max
66
+
67
+ map "hhea_ascent", to: :hhea_ascent
68
+ map "hhea_descent", to: :hhea_descent
69
+ map "hhea_line_gap", to: :hhea_line_gap
70
+
71
+ map "typo_ascender", to: :typo_ascender
72
+ map "typo_descender", to: :typo_descender
73
+ map "typo_line_gap", to: :typo_line_gap
74
+
75
+ map "win_ascent", to: :win_ascent
76
+ map "win_descent", to: :win_descent
77
+
78
+ map "x_height", to: :x_height
79
+ map "cap_height", to: :cap_height
80
+
81
+ map "subscript_x_size", to: :subscript_x_size
82
+ map "subscript_y_size", to: :subscript_y_size
83
+ map "subscript_x_offset", to: :subscript_x_offset
84
+ map "subscript_y_offset", to: :subscript_y_offset
85
+ map "superscript_x_size", to: :superscript_x_size
86
+ map "superscript_y_size", to: :superscript_y_size
87
+ map "superscript_x_offset", to: :superscript_x_offset
88
+ map "superscript_y_offset", to: :superscript_y_offset
89
+
90
+ map "strikeout_size", to: :strikeout_size
91
+ map "strikeout_position", to: :strikeout_position
92
+
93
+ map "underline_position", to: :underline_position
94
+ map "underline_thickness", to: :underline_thickness
95
+ end
96
+
97
+ # True when hhea ascent/descent match OS/2 typo ascent/descent.
98
+ # Mismatch is a common font bug that causes inconsistent line
99
+ # height across platforms.
100
+ #
101
+ # @return [Boolean]
102
+ def metrics_consistent?
103
+ return false if hhea_ascent.nil? || typo_ascender.nil?
104
+ return false if hhea_descent.nil? || typo_descender.nil?
105
+
106
+ hhea_ascent == typo_ascender && hhea_descent == typo_descender
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ module Audit
8
+ # One fvar named instance (e.g. "Bold", "SemiCondensed").
9
+ #
10
+ # `coordinates` is serialized as a compact "tag=value,tag=value" string
11
+ # (e.g. "wght=700,wdth=100") for human readability. The AuditReport is
12
+ # primarily a human-facing artifact; downstream tooling that needs
13
+ # structured coordinates can re-derive them from fvar.
14
+ class NamedInstance < Lutaml::Model::Serializable
15
+ attribute :subfamily_name, :string
16
+ attribute :postscript_name, :string
17
+ attribute :coordinates, :string
18
+
19
+ key_value do
20
+ map "subfamily_name", to: :subfamily_name
21
+ map "postscript_name", to: :postscript_name
22
+ map "coordinates", to: :coordinates
23
+ end
24
+
25
+ # Build the coordinates string from a parallel array of axis tags
26
+ # and fvar coordinate values. Returns nil if either side is empty.
27
+ #
28
+ # @param axis_tags [Array<String>] ordered axis tags (e.g. ["wght", "wdth"])
29
+ # @param values [Array<Numeric>] ordered coordinate values
30
+ # @return [String, nil]
31
+ def self.format_coordinates(axis_tags, values)
32
+ return nil if axis_tags.nil? || values.nil?
33
+ return nil if axis_tags.empty? || values.empty?
34
+
35
+ pairs = axis_tags.zip(values).map { |tag, val| "#{tag}=#{val}" }
36
+ pairs.join(",")
37
+ end
38
+ end
39
+ end
40
+ end
41
+ 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
+ # Structured OpenType layout summary for one face.
9
+ #
10
+ # Replaces the previous flat `opentype_scripts` + `features` pair
11
+ # on AuditReport for MECE cleanliness. Carries:
12
+ #
13
+ # - `scripts`: union of GSUB + GPOS script tags (sorted, unique).
14
+ # - `features`: union of GSUB + GPOS feature tags across every
15
+ # script (sorted, unique).
16
+ # - `by_script`: per-script breakdown preserving the
17
+ # "feature X is for script Y" relationship that the flat arrays
18
+ # discarded.
19
+ # - `has_gsub` / `has_gpos`: presence flags so consumers can tell
20
+ # "font has no layout" from "font has GSUB but no GPOS".
21
+ #
22
+ # nil for Type 1 fonts (no SFNT table structure).
23
+ class OpenTypeLayout < Lutaml::Model::Serializable
24
+ attribute :scripts, :string, collection: true
25
+ attribute :features, :string, collection: true
26
+ attribute :by_script, ScriptFeatures, collection: true
27
+ attribute :has_gsub, Lutaml::Model::Type::Boolean
28
+ attribute :has_gpos, Lutaml::Model::Type::Boolean
29
+
30
+ key_value do
31
+ map "scripts", to: :scripts
32
+ map "features", to: :features
33
+ map "by_script", to: :by_script
34
+ map "has_gsub", to: :has_gsub
35
+ map "has_gpos", to: :has_gpos
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ module Audit
8
+ # One row in a LibrarySummary's script-coverage matrix.
9
+ #
10
+ # Lists every face (by postscript_name) whose cmap covers at least
11
+ # one codepoint assigned to a Unicode script. Lets a librarian
12
+ # answer "which fonts cover Cyrillic?" without re-auditing.
13
+ class ScriptCoverageRow < Lutaml::Model::Serializable
14
+ attribute :script, :string
15
+ attribute :face_count, :integer
16
+ attribute :faces, :string, collection: true
17
+
18
+ key_value do
19
+ map "script", to: :script
20
+ map "face_count", to: :face_count
21
+ map "faces", to: :faces
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end