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,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "yaml"
5
+
6
+ module Fontisan
7
+ module Ucd
8
+ # Sorted, run-length-encoded lookup table over Unicode codepoints.
9
+ #
10
+ # One Index answers "what <thing> does codepoint N belong to?" for one
11
+ # property (block, or script). Lookup is O(log N) via bsearch.
12
+ class Index
13
+ include Enumerable
14
+
15
+ # @param entries [Array<RangeEntry>] sorted, disjoint
16
+ def initialize(entries)
17
+ @entries = entries.sort
18
+ end
19
+
20
+ # @return [Array<RangeEntry>]
21
+ attr_reader :entries
22
+
23
+ def each(&)
24
+ @entries.each(&)
25
+ end
26
+
27
+ def size
28
+ @entries.size
29
+ end
30
+
31
+ # @param codepoint [Integer] Unicode codepoint
32
+ # @return [String, nil] the name of the range covering `codepoint`, or nil
33
+ def lookup(codepoint)
34
+ idx = bsearch_index(codepoint)
35
+ idx && @entries[idx].name
36
+ end
37
+
38
+ # Enumerate every range whose [first_cp, last_cp] overlaps the
39
+ # inclusive query range.
40
+ # @param first [Integer]
41
+ # @param last [Integer]
42
+ # @return [Enumerator<RangeEntry>]
43
+ def each_overlapping(first, last, &)
44
+ return enum_for(:each_overlapping, first, last) unless block_given?
45
+
46
+ start_idx = bsearch_first_overlap(first)
47
+ return if start_idx.nil?
48
+
49
+ @entries[start_idx..].each do |entry|
50
+ break if entry.first_cp > last
51
+
52
+ yield entry if entry.last_cp >= first
53
+ end
54
+ end
55
+
56
+ # Serialize to a YAML file.
57
+ # @param path [String, Pathname]
58
+ # @return [void]
59
+ def save(path)
60
+ File.open(path, "w") do |file|
61
+ YAML.dump(@entries.map(&:to_h), file)
62
+ end
63
+ end
64
+
65
+ # Load from a YAML file previously written by #save.
66
+ # @param path [String, Pathname] (required)
67
+ # @return [Index]
68
+ def self.load(path)
69
+ hashes = YAML.load_file(path)
70
+ new(hashes.map { |h| RangeEntry.from_h(h) })
71
+ end
72
+
73
+ # Build an Index from raw [first_cp, last_cp, name] triples.
74
+ # @param triples [Array<Array(Integer, Integer, String)>]
75
+ # @return [Index]
76
+ def self.from_triples(triples)
77
+ new(triples.map { |first, last, name| RangeEntry.new(first, last, name) })
78
+ end
79
+
80
+ private
81
+
82
+ # Binary search for the entry whose range contains `codepoint`.
83
+ # Returns the index in @entries, or nil.
84
+ def bsearch_index(codepoint)
85
+ @entries.bsearch_index do |entry|
86
+ if codepoint < entry.first_cp
87
+ -1
88
+ elsif codepoint > entry.last_cp
89
+ 1
90
+ else
91
+ 0
92
+ end
93
+ end
94
+ end
95
+
96
+ # Find the first entry whose last_cp >= `first`. Returns nil if no
97
+ # entry overlaps anything >= `first`.
98
+ def bsearch_first_overlap(first)
99
+ @entries.bsearch_index { |entry| entry.last_cp >= first }
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ucd
5
+ # Turns a parsed Models::Ucd::Ucd instance into two compact
6
+ # run-length-encoded indices (blocks + scripts), and persists them to
7
+ # the cache for future Index loads.
8
+ #
9
+ # Index layout on disk (YAML):
10
+ #
11
+ # <root>/<version>/index/
12
+ # blocks.yml
13
+ # scripts.yml
14
+ #
15
+ # Each file is an array of `{ first_cp:, last_cp:, name: }` hashes,
16
+ # sorted by first_cp, disjoint.
17
+ module IndexBuilder
18
+ class << self
19
+ # Build + persist both indices for a cached version.
20
+ # @param version [String]
21
+ # @return [Array(Index, Index)] blocks_index, scripts_index
22
+ def build(version)
23
+ ucd = load_ucd(version)
24
+ blocks, scripts = build_from_ucd(ucd)
25
+ CacheManager.index_dir(version).mkpath
26
+ blocks.save(CacheManager.blocks_index_path(version))
27
+ scripts.save(CacheManager.scripts_index_path(version))
28
+ [blocks, scripts]
29
+ end
30
+
31
+ # Pure: build both indices from an in-memory Ucd model.
32
+ # @param ucd [Models::Ucd::Ucd]
33
+ # @return [Array(Index, Index)]
34
+ def build_from_ucd(ucd)
35
+ blocks_runs = collect_runs(ucd, :block)
36
+ scripts_runs = collect_runs(ucd, :script)
37
+ [Index.new(to_entries(blocks_runs)), Index.new(to_entries(scripts_runs))]
38
+ end
39
+
40
+ private
41
+
42
+ def load_ucd(version)
43
+ path = CacheManager.ucdxml_path(version)
44
+ xml = File.read(path)
45
+ Models::Ucd::Ucd.from_xml(xml)
46
+ end
47
+
48
+ # Walk all UcdChar entries, group by the given property
49
+ # (:block or :script), and accumulate codepoint ranges per name.
50
+ # Returns Hash<String, Array<[Integer, Integer]>>.
51
+ def collect_runs(ucd, property)
52
+ runs_by_name = Hash.new { |h, k| h[k] = [] }
53
+
54
+ ucd.chars.each do |char|
55
+ name = char.public_send(property)
56
+ next if name.nil? || name.empty?
57
+
58
+ ranges_for_char(char).each do |first, last|
59
+ runs_by_name[name] << [first, last]
60
+ end
61
+ end
62
+
63
+ runs_by_name.each_value { |runs| coalesce!(runs) }
64
+ runs_by_name
65
+ end
66
+
67
+ # Returns Array<[Integer, Integer]> — the codepoint range(s) this
68
+ # char covers.
69
+ def ranges_for_char(char)
70
+ if char.range?
71
+ [[char.first_cp.to_i(16), char.last_cp.to_i(16)]]
72
+ elsif char.cp
73
+ cp_int = char.cp.to_i(16)
74
+ [[cp_int, cp_int]]
75
+ else
76
+ []
77
+ end
78
+ end
79
+
80
+ # Sort + merge adjacent/overlapping ranges in place.
81
+ # Input: Array<[Integer, Integer]>, mutated.
82
+ def coalesce!(runs)
83
+ return if runs.empty?
84
+
85
+ runs.sort!
86
+ merged = [runs.first]
87
+ runs[1..].each do |first, last|
88
+ prev = merged.last
89
+ if first <= prev[1] + 1
90
+ prev[1] = [prev[1], last].max
91
+ else
92
+ merged << [first, last]
93
+ end
94
+ end
95
+ runs.replace(merged)
96
+ end
97
+
98
+ # Flatten {name => [[first,last],...]} into Array<RangeEntry>.
99
+ def to_entries(runs_by_name)
100
+ runs_by_name.flat_map do |name, runs|
101
+ runs.map { |first, last| RangeEntry.new(first, last, name) }
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ucd
5
+ # Value object representing one row in a run-length-encoded UCD index.
6
+ #
7
+ # Sorted by `first_cp`. Entries within a single Index are disjoint
8
+ # (no overlapping ranges).
9
+ class RangeEntry
10
+ include Comparable
11
+
12
+ attr_reader :first_cp, :last_cp, :name
13
+
14
+ def initialize(first_cp, last_cp, name)
15
+ @first_cp = first_cp
16
+ @last_cp = last_cp
17
+ @name = name
18
+ end
19
+
20
+ def covers?(codepoint)
21
+ codepoint >= @first_cp && codepoint <= @last_cp
22
+ end
23
+
24
+ def size
25
+ @last_cp - @first_cp + 1
26
+ end
27
+
28
+ def <=>(other)
29
+ [@first_cp, @last_cp] <=> [other.first_cp, other.last_cp]
30
+ end
31
+
32
+ def ==(other)
33
+ other.is_a?(RangeEntry) &&
34
+ @first_cp == other.first_cp &&
35
+ @last_cp == other.last_cp &&
36
+ @name == other.name
37
+ end
38
+ alias eql? ==
39
+
40
+ def hash
41
+ [@first_cp, @last_cp, @name].hash
42
+ end
43
+
44
+ # Compact YAML-friendly form.
45
+ def to_h
46
+ { first_cp: @first_cp, last_cp: @last_cp, name: @name }
47
+ end
48
+
49
+ def self.from_h(hash)
50
+ new(hash[:first_cp] || hash["first_cp"],
51
+ hash[:last_cp] || hash["last_cp"],
52
+ hash[:name] || hash["name"])
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ucd
5
+ # Raised by Ucd::VersionResolver when a user-supplied version string
6
+ # is not in Ucd::Config.known_versions.
7
+ class UnknownVersionError < Ucd::Error; end
8
+ end
9
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "rubygems"
6
+
7
+ module Fontisan
8
+ module Ucd
9
+ # Resolves a user-supplied version intent to a concrete version string.
10
+ #
11
+ # Three input modes:
12
+ #
13
+ # resolve(nil) # default_version from config
14
+ # resolve(:default) # default_version from config
15
+ # resolve("17.0.0") # explicit; validated against known_versions
16
+ # resolve(:latest) # probes listing_url, picks highest; falls
17
+ # # back to default on failure
18
+ #
19
+ module VersionResolver
20
+ class << self
21
+ # @param intent [nil, :default, :latest, String]
22
+ # @return [String] a concrete version string
23
+ def resolve(intent)
24
+ case intent
25
+ when nil, :default
26
+ Config.default_version
27
+ when :latest
28
+ probe_latest
29
+ else
30
+ validate!(intent)
31
+ intent
32
+ end
33
+ end
34
+
35
+ # Raise UnknownVersionError unless `version` is in known_versions.
36
+ # @param version [String]
37
+ # @return [void]
38
+ def validate!(version)
39
+ return if Config.known?(version)
40
+
41
+ raise UnknownVersionError,
42
+ "UCD version #{version.inspect} is not recognized. " \
43
+ "Known versions: #{Config.known_versions.join(', ')}"
44
+ end
45
+
46
+ private
47
+
48
+ # Best-effort scrape of the unicode.org ucdxml directory listing.
49
+ # Returns the highest semver found, or Config.default_version on
50
+ # any failure.
51
+ def probe_latest
52
+ versions = fetch_directory_versions
53
+ return fallback_latest("directory listing was empty") if versions.empty?
54
+
55
+ highest = versions.max_by { |v| Gem::Version.new(v) }
56
+ if Config.known?(highest)
57
+ highest
58
+ else
59
+ fallback_latest("#{highest.inspect} is not in known_versions; using default")
60
+ end
61
+ rescue StandardError => e
62
+ fallback_latest(e.message)
63
+ end
64
+
65
+ def fallback_latest(reason)
66
+ warn "Ucd::VersionResolver: --latest probe failed (#{reason}); " \
67
+ "falling back to default #{Config.default_version.inspect}"
68
+ Config.default_version
69
+ end
70
+
71
+ def fetch_directory_versions
72
+ uri = URI(Config.listing_url)
73
+ html = Net::HTTP.get(uri)
74
+ html.scan(%r{href="(\d+\.\d+\.\d+)/?"}i).flatten.uniq
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Namespace file for UCD (Unicode Character Database) support.
4
+ #
5
+ # All Ucd::* constants are autoloaded from here. Top-level callers
6
+ # (`lib/fontisan.rb`) require this file; downstream files reference
7
+ # constants like Ucd::CacheManager and let autoload do the work.
8
+
9
+ module Fontisan
10
+ module Ucd
11
+ autoload :Config, "fontisan/ucd/config"
12
+ autoload :CacheManager, "fontisan/ucd/cache_manager"
13
+ autoload :Downloader, "fontisan/ucd/downloader"
14
+ autoload :VersionResolver, "fontisan/ucd/version_resolver"
15
+ autoload :IndexBuilder, "fontisan/ucd/index_builder"
16
+ autoload :Index, "fontisan/ucd/index"
17
+ autoload :Aggregator, "fontisan/ucd/aggregator"
18
+ autoload :RangeEntry, "fontisan/ucd/range_entry"
19
+ autoload :Error, "fontisan/ucd/error"
20
+ autoload :DownloadError, "fontisan/ucd/download_error"
21
+ autoload :UnknownVersionError, "fontisan/ucd/unknown_version_error"
22
+ end
23
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "stringio"
4
- require_relative "../constants"
5
4
 
6
5
  module Fontisan
7
6
  module Utilities
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Autoload hub for the Fontisan::Utilities namespace.
4
+
5
+ module Fontisan
6
+ module Utilities
7
+ autoload :BrotliWrapper, "fontisan/utilities/brotli_wrapper"
8
+ autoload :ChecksumCalculator, "fontisan/utilities/checksum_calculator"
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Autoload hub for the Fontisan::Utils namespace.
4
+
5
+ module Fontisan
6
+ module Utils
7
+ autoload :Future, "fontisan/utils/thread_pool"
8
+ autoload :ThreadPool, "fontisan/utils/thread_pool"
9
+ end
10
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../error"
4
-
5
3
  module Fontisan
6
4
  module Validation
7
5
  # CollectionValidator validates font compatibility for collection formats
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Autoload hub for the Fontisan::Validation namespace.
4
+
5
+ module Fontisan
6
+ module Validation
7
+ autoload :CollectionValidator, "fontisan/validation/collection_validator"
8
+ end
9
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "validator"
4
-
5
3
  module Fontisan
6
4
  module Validators
7
5
  # BasicValidator provides minimal validation for fast font indexing
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "basic_validator"
4
-
5
3
  module Fontisan
6
4
  module Validators
7
5
  # FontBookValidator provides macOS Font Book installation compatibility checks
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "font_book_validator"
4
-
5
3
  module Fontisan
6
4
  module Validators
7
5
  # OpenTypeValidator provides comprehensive OpenType specification compliance checks
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "basic_validator"
4
- require_relative "font_book_validator"
5
- require_relative "opentype_validator"
6
- require_relative "web_font_validator"
7
-
8
3
  module Fontisan
9
4
  module Validators
10
5
  # ProfileLoader manages validation profiles and loads appropriate validators
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../models/validation_report"
4
-
5
3
  module Fontisan
6
4
  module Validators
7
5
  # Base class for all validators using block-based DSL
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "basic_validator"
4
-
5
3
  module Fontisan
6
4
  module Validators
7
5
  # WebFontValidator provides web font optimization and embedding compatibility checks
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Autoload hub for the Fontisan::Validators namespace.
4
+
5
+ module Fontisan
6
+ module Validators
7
+ autoload :BasicValidator, "fontisan/validators/basic_validator"
8
+ autoload :FontBookValidator, "fontisan/validators/font_book_validator"
9
+ autoload :OpenTypeValidator, "fontisan/validators/opentype_validator"
10
+ autoload :ProfileLoader, "fontisan/validators/profile_loader"
11
+ autoload :Validator, "fontisan/validators/validator"
12
+ autoload :WebFontValidator, "fontisan/validators/web_font_validator"
13
+ end
14
+ end
@@ -1,10 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "yaml"
4
- require_relative "axis_normalizer"
5
- require_relative "region_matcher"
6
- require_relative "glyph_delta_processor"
7
- require_relative "metric_delta_processor"
8
4
 
9
5
  module Fontisan
10
6
  module Variable
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "delta_applicator"
4
- require_relative "static_font_builder"
5
-
6
3
  module Fontisan
7
4
  module Variable
8
5
  # Main entry point for variable font instancing
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "table_updater"
4
- require_relative "../font_writer"
5
-
6
3
  module Fontisan
7
4
  module Variable
8
5
  # Builds static font instances from variable font data
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Autoload hub for the Fontisan::Variable namespace.
4
+
5
+ module Fontisan
6
+ module Variable
7
+ autoload :AxisNormalizer, "fontisan/variable/axis_normalizer"
8
+ autoload :DeltaApplicator, "fontisan/variable/delta_applicator"
9
+ autoload :GlyphDeltaProcessor, "fontisan/variable/glyph_delta_processor"
10
+ autoload :Instancer, "fontisan/variable/instancer"
11
+ autoload :MetricDeltaProcessor, "fontisan/variable/metric_delta_processor"
12
+ autoload :RegionMatcher, "fontisan/variable/region_matcher"
13
+ autoload :StaticFontBuilder, "fontisan/variable/static_font_builder"
14
+ autoload :TableUpdater, "fontisan/variable/table_updater"
15
+ end
16
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "interpolator"
4
-
5
3
  module Fontisan
6
4
  module Variation
7
5
  # Applies CFF2 blend operators during CharString execution
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "cache_key_builder"
4
-
5
3
  module Fontisan
6
4
  module Variation
7
5
  # Caches variation calculations for performance
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "interpolator"
4
- require_relative "table_accessor"
5
-
6
3
  module Fontisan
7
4
  module Variation
8
5
  # Converts variation data between TrueType (gvar) and CFF2 (blend) formats
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "variation_context"
4
-
5
3
  module Fontisan
6
4
  module Variation
7
5
  # Extracts variation data from OpenType variable fonts
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "delta_parser"
4
- require_relative "interpolator"
5
- require_relative "region_matcher"
6
- require_relative "table_accessor"
7
-
8
3
  module Fontisan
9
4
  module Variation
10
5
  # Applies variation deltas to glyph outlines
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "json"
4
4
  require "yaml"
5
- require_relative "variation_context"
6
5
 
7
6
  module Fontisan
8
7
  module Variation
@@ -1,11 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "interpolator"
4
- require_relative "region_matcher"
5
- require_relative "metrics_adjuster"
6
- require_relative "variation_context"
7
- require_relative "table_accessor"
8
-
9
3
  module Fontisan
10
4
  module Variation
11
5
  # Generates static font instances from variable fonts
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../font_writer"
4
- require_relative "../converters/outline_converter"
5
- require_relative "../converters/woff_writer"
6
- require_relative "../error"
7
-
8
3
  module Fontisan
9
4
  module Variation
10
5
  # Writes generated static font instances to files in various formats
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "interpolator"
4
- require_relative "region_matcher"
5
- require_relative "table_accessor"
6
-
7
3
  module Fontisan
8
4
  module Variation
9
5
  # Applies metrics variation deltas to font metrics tables
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../optimizers/pattern_analyzer"
4
- require_relative "../optimizers/subroutine_optimizer"
5
-
6
3
  module Fontisan
7
4
  module Variation
8
5
  # Optimizes CFF subroutines for variable fonts
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "instance_generator"
4
- require_relative "cache"
5
- require_relative "../utils/thread_pool"
6
3
  require "etc"
7
4
 
8
5
  module Fontisan