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
@@ -2,45 +2,171 @@
2
2
 
3
3
  module Fontisan
4
4
  module Converters
5
- # Interface module for font format conversion strategies
5
+ # Interface module and declarative options DSL for conversion strategies
6
6
  #
7
7
  # [`ConversionStrategy`](lib/fontisan/converters/conversion_strategy.rb)
8
- # defines the contract that all conversion strategy classes must implement.
9
- # This follows the Strategy pattern to enable polymorphic handling of
10
- # different conversion types (TTF→OTF, OTF→TTF, same-format copying).
8
+ # defines the contract that all conversion strategy classes must implement,
9
+ # plus a declarative DSL for declaring the options each strategy accepts.
11
10
  #
12
- # Each strategy must implement:
13
- # - convert(font, options) - Perform the actual conversion
14
- # - supported_conversions - Return array of [source, target] format pairs
15
- # - validate(font, target_format) - Validate conversion is possible
11
+ # ## Why a declarative options DSL
16
12
  #
17
- # Strategies are selected by [`FormatConverter`](lib/fontisan/converters/format_converter.rb)
18
- # based on source and target formats.
13
+ # Each format has its own spec-mandated knobs (WOFF: zlib level,
14
+ # WOFF2: Brotli quality, etc.). Letting each strategy declare its own
15
+ # options keeps the schema with the code that consumes it (encapsulation),
16
+ # and makes adding a new format a pure additive change (OCP): write a new
17
+ # strategy class, declare its options, done — no edits to central option
18
+ # lists, the CLI option parser, or ConversionOptions.
19
19
  #
20
- # @example Implementing a strategy
21
- # class MyStrategy
22
- # include Fontisan::Converters::ConversionStrategy
20
+ # The strategy is also the sole validator of its own options. The
21
+ # runtime check at `FormatConverter#convert` calls
22
+ # `strategy.class.validate_options!` to enforce the format ↔ option
23
+ # mapping (e.g., rejecting `--zlib-level` on a WOFF2 conversion). This
24
+ # is the MECE guarantee: every option belongs to exactly one strategy,
25
+ # and a strategy rejects anything it did not declare.
23
26
  #
24
- # def convert(font, options = {})
25
- # # Perform conversion
26
- # tables = {...}
27
- # tables
28
- # end
27
+ # @example Declaring options
28
+ # class WoffWriter
29
+ # include ConversionStrategy
29
30
  #
30
- # def supported_conversions
31
- # [[:ttf, :otf], [:otf, :ttf]]
32
- # end
31
+ # option :zlib_level, type: :integer, range: 0..9, default: 6,
32
+ # cli: "--zlib-level", desc: "zlib compression level"
33
+ # option :uncompressed, type: :boolean, default: false,
34
+ # cli: "--uncompressed", desc: "store tables uncompressed"
33
35
  #
34
- # def validate(font, target_format)
35
- # # Validate font can be converted
36
- # raise Error unless valid
36
+ # def convert(font, options = {})
37
+ # self.class.validate_options!(options)
38
+ # # ...
37
39
  # end
38
40
  # end
39
41
  module ConversionStrategy
40
- # Convert font to target format
42
+ # Declarative description of a single strategy option.
43
+ # `allowed_values` is spelled out (not `values`) to avoid shadowing
44
+ # Struct#values.
45
+ Option = Struct.new(:name, :type, :default, :cli, :desc, :range,
46
+ :allowed_values, keyword_init: true)
47
+
48
+ # Class methods mixed into including classes via `included`.
49
+ module ClassMethods
50
+ # Declare an option this strategy accepts.
51
+ #
52
+ # @param name [Symbol] Option name (the hash key used in `convert`)
53
+ # @param type [Symbol] One of :integer, :boolean, :string
54
+ # @param default [Object] Default value when the caller omits the option
55
+ # @param cli [String] CLI flag shape (for help text generation)
56
+ # @param desc [String] Human-readable description
57
+ # @param range [Range, nil] For :integer; valid range
58
+ # @param values [Array, nil] For :string; allowed values
59
+ # @return [void]
60
+ def option(name, type:, default:, cli:, desc:, range: nil, values: nil)
61
+ declared_options << Option.new(
62
+ name: name, type: type, default: default, cli: cli, desc: desc,
63
+ range: range, allowed_values: values
64
+ )
65
+ end
66
+
67
+ # All options declared by this class.
68
+ #
69
+ # @return [Array<Option>]
70
+ def declared_options
71
+ @declared_options ||= []
72
+ end
73
+
74
+ # Public accessor: the full list of options for this strategy.
75
+ alias supported_options declared_options
76
+
77
+ # Find the option schema for a given key (symbol or string).
78
+ #
79
+ # @param name [Symbol, String]
80
+ # @return [Option, nil]
81
+ def option_for(name)
82
+ supported_options.find { |o| o.name == name.to_sym }
83
+ end
84
+
85
+ # Default values for every declared option.
86
+ #
87
+ # @return [Hash{Symbol => Object}]
88
+ def default_options
89
+ supported_options.to_h { |o| [o.name, o.default] }
90
+ end
91
+
92
+ # Validate a user-provided options hash against this strategy's schema.
93
+ #
94
+ # Raises ArgumentError for any unknown key, wrong type, or
95
+ # out-of-range value. Called by FormatConverter after strategy
96
+ # selection, so cross-format misuse (e.g., `--zlib-level` on a
97
+ # WOFF2 conversion) is caught here.
98
+ #
99
+ # @param user_options [Hash{Symbol, String => Object}]
100
+ # @return [void]
101
+ # @raise [ArgumentError] if any key is unknown or any value is invalid
102
+ def validate_options!(user_options)
103
+ user_options.each_key do |key|
104
+ opt = option_for(key)
105
+ next if opt
106
+
107
+ names = supported_options.map(&:name)
108
+ list = names.empty? ? "(none)" : names.join(", ")
109
+ raise ArgumentError,
110
+ "Unknown option #{key.inspect} for #{name}. " \
111
+ "Supported: #{list}"
112
+ end
113
+
114
+ user_options.each do |key, value|
115
+ opt = option_for(key)
116
+ validate_option_value!(opt, value) unless value.nil?
117
+ end
118
+ end
119
+
120
+ private
121
+
122
+ # Type-check a single value against its declared schema.
123
+ #
124
+ # @param opt [Option]
125
+ # @param value [Object]
126
+ # @return [void]
127
+ # @raise [ArgumentError] if value fails type or range/values check
128
+ def validate_option_value!(opt, value)
129
+ case opt.type
130
+ when :integer
131
+ unless value.is_a?(Integer)
132
+ raise ArgumentError,
133
+ "#{opt.name} must be an Integer, got #{value.inspect} " \
134
+ "(#{value.class})"
135
+ end
136
+ return unless opt.range && !opt.range.cover?(value)
137
+
138
+ raise ArgumentError,
139
+ "#{opt.name} must be in #{opt.range}, got #{value}"
140
+ when :boolean
141
+ return if [true, false].include?(value)
142
+
143
+ raise ArgumentError,
144
+ "#{opt.name} must be true or false, got #{value.inspect}"
145
+ when :string
146
+ unless value.is_a?(String)
147
+ raise ArgumentError,
148
+ "#{opt.name} must be a String, got #{value.class}"
149
+ end
150
+ return unless opt.allowed_values && !opt.allowed_values.include?(value)
151
+
152
+ raise ArgumentError,
153
+ "#{opt.name} must be one of #{opt.allowed_values.join(', ')}, " \
154
+ "got #{value.inspect}"
155
+ else
156
+ raise "Unknown option type #{opt.type.inspect} on #{opt.name}"
157
+ end
158
+ end
159
+ end
160
+
161
+ # Mix ClassMethods into any class that includes this module.
162
+ def self.included(base)
163
+ base.extend(ClassMethods)
164
+ end
165
+
166
+ # Convert font to target format.
41
167
  #
42
- # This method must return a hash of table tags to binary data,
43
- # which will be assembled into a complete font by FontWriter.
168
+ # Strategies must implement this. Subclasses should call
169
+ # `self.class.validate_options!(options)` first to enforce their schema.
44
170
  #
45
171
  # @param font [TrueTypeFont, OpenTypeFont] Source font
46
172
  # @param options [Hash] Conversion options
@@ -51,30 +177,19 @@ module Fontisan
51
177
  "#{self.class.name} must implement convert(font, options)"
52
178
  end
53
179
 
54
- # Get list of supported conversions
55
- #
56
- # Returns an array of [source_format, target_format] pairs that
57
- # this strategy can handle.
180
+ # Get list of supported conversions.
58
181
  #
59
- # @return [Array<Array<Symbol>>] Supported conversion pairs
182
+ # @return [Array<Array<Symbol>>] Supported [source, target] pairs
60
183
  # @raise [NotImplementedError] If not implemented by strategy
61
- #
62
- # @example
63
- # strategy.supported_conversions
64
- # # => [[:ttf, :otf], [:otf, :ttf]]
65
184
  def supported_conversions
66
185
  raise NotImplementedError,
67
186
  "#{self.class.name} must implement supported_conversions"
68
187
  end
69
188
 
70
- # Validate that conversion is possible
71
- #
72
- # Checks if the given font can be converted to the target format.
73
- # Should raise an error with a clear message if conversion is not
74
- # possible.
189
+ # Validate that conversion is possible.
75
190
  #
76
191
  # @param font [TrueTypeFont, OpenTypeFont] Font to validate
77
- # @param target_format [Symbol] Target format (:ttf, :otf, etc.)
192
+ # @param target_format [Symbol] Target format
78
193
  # @return [Boolean] True if valid
79
194
  # @raise [Error] If conversion is not possible
80
195
  # @raise [NotImplementedError] If not implemented by strategy
@@ -83,11 +198,11 @@ module Fontisan
83
198
  "#{self.class.name} must implement validate(font, target_format)"
84
199
  end
85
200
 
86
- # Check if strategy supports a conversion
201
+ # Check if strategy supports a given conversion.
87
202
  #
88
- # @param source_format [Symbol] Source format
89
- # @param target_format [Symbol] Target format
90
- # @return [Boolean] True if supported
203
+ # @param source_format [Symbol]
204
+ # @param target_format [Symbol]
205
+ # @return [Boolean]
91
206
  def supports?(source_format, target_format)
92
207
  supported_conversions.include?([source_format, target_format])
93
208
  end
@@ -1,12 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "conversion_strategy"
4
- require_relative "table_copier"
5
- require_relative "outline_converter"
6
- require_relative "woff_writer"
7
- require_relative "woff2_encoder"
8
- require_relative "svg_generator"
9
- require_relative "type1_converter"
10
3
  require "yaml"
11
4
 
12
5
  module Fontisan
@@ -17,18 +10,14 @@ module Fontisan
17
10
  # primary entry point for all format conversion operations. It:
18
11
  # - Selects appropriate conversion strategy based on source/target formats
19
12
  # - Validates conversions against the conversion matrix
13
+ # - Validates user-supplied options against the selected strategy's schema
20
14
  # - Delegates actual conversion to strategy implementations
21
15
  # - Provides clean error messages for unsupported conversions
22
16
  #
23
17
  # The converter uses a strategy pattern with pluggable strategies for
24
- # different conversion types:
25
- # - OutlineConverter: TTF OTF conversions
26
- # - TableCopier: Same-format operations
27
- # - Woff2Encoder: TTF/OTF → WOFF2 compression
28
- # - SvgGenerator: TTF/OTF → SVG font generation
29
- #
30
- # Supported conversions are defined in the conversion matrix configuration
31
- # file, making it easy to extend without modifying code.
18
+ # different conversion types. Each strategy declares its own options via
19
+ # the `ConversionStrategy.option` DSL; the converter enforces the
20
+ # format option mapping at runtime.
32
21
  #
33
22
  # @example Converting TTF to OTF
34
23
  # converter = Fontisan::Converters::FormatConverter.new
@@ -41,6 +30,81 @@ module Fontisan
41
30
  # tables = converter.convert(font, :ttf) # TTF to TTF
42
31
  # FontWriter.write_to_file(tables, 'copy.ttf')
43
32
  class FormatConverter
33
+ # Registry of all strategy classes. Single source of truth for option
34
+ # discovery and strategy lookup. Add a new format by appending its
35
+ # strategy class here.
36
+ STRATEGY_CLASSES = [
37
+ TableCopier,
38
+ OutlineConverter,
39
+ Type1Converter,
40
+ WoffWriter,
41
+ Woff2Encoder,
42
+ SvgGenerator,
43
+ ].freeze
44
+
45
+ class << self
46
+ # All option names declared by any strategy. Used by ConversionOptions
47
+ # to keep its generating-options list in sync without duplicating the
48
+ # schema.
49
+ #
50
+ # @return [Array<Symbol>]
51
+ def all_strategy_option_names
52
+ STRATEGY_CLASSES.flat_map { |k| k.supported_options.map(&:name) }.uniq
53
+ end
54
+
55
+ # Look up the strategy class that handles a given conversion.
56
+ #
57
+ # @param source_format [Symbol]
58
+ # @param target_format [Symbol]
59
+ # @return [Class, nil]
60
+ def strategy_class_for(source_format, target_format)
61
+ STRATEGY_CLASSES.find do |klass|
62
+ klass.new.supports?(source_format, target_format)
63
+ rescue NotImplementedError
64
+ false
65
+ end
66
+ end
67
+
68
+ # Strategies whose supported_conversions include the given target.
69
+ #
70
+ # @param target_format [Symbol]
71
+ # @return [Array[Class]]
72
+ def strategies_for_target(target_format)
73
+ STRATEGY_CLASSES.select do |klass|
74
+ klass.new.supported_conversions.any? { |(_, t)| t == target_format }
75
+ rescue NotImplementedError
76
+ false
77
+ end
78
+ end
79
+
80
+ # Cross-format option check, independent of source format. Used by
81
+ # OutputWriter (which doesn't know the source format at write time)
82
+ # to catch e.g. `--brotli-quality` passed with `--to woff`.
83
+ #
84
+ # @param target_format [Symbol]
85
+ # @param options [Hash]
86
+ # @return [void]
87
+ # @raise [ArgumentError] if a user-supplied option is declared only by
88
+ # strategies that don't handle this target
89
+ def validate_options_for_target!(target_format, options)
90
+ handlers = strategies_for_target(target_format)
91
+ handler_names = handlers.flat_map do |k|
92
+ k.supported_options.map(&:name)
93
+ end.to_set
94
+ non_handler_names = (STRATEGY_CLASSES - handlers)
95
+ .flat_map { |k| k.supported_options.map(&:name) }
96
+
97
+ conflicts = options.keys.select do |k|
98
+ non_handler_names.include?(k.to_sym)
99
+ end
100
+ return if conflicts.empty?
101
+
102
+ accepted = handler_names.empty? ? "(none)" : handler_names.join(", ")
103
+ raise ArgumentError,
104
+ "Option(s) #{conflicts.map(&:inspect).join(', ')} do not apply " \
105
+ "to --to #{target_format}. Accepted for #{target_format}: #{accepted}"
106
+ end
107
+ end
44
108
  # @return [Hash] Conversion matrix loaded from config
45
109
  attr_reader :conversion_matrix
46
110
 
@@ -52,15 +116,7 @@ module Fontisan
52
116
  # @param conversion_matrix_path [String, nil] Path to conversion matrix
53
117
  # config. If nil, uses default.
54
118
  def initialize(conversion_matrix_path: nil)
55
- @strategies = [
56
- TableCopier.new,
57
- OutlineConverter.new,
58
- Type1Converter.new,
59
- WoffWriter.new,
60
- Woff2Encoder.new,
61
- SvgGenerator.new,
62
- ]
63
-
119
+ @strategies = self.class::STRATEGY_CLASSES.map(&:new)
64
120
  load_conversion_matrix(conversion_matrix_path)
65
121
  end
66
122
 
@@ -105,8 +161,20 @@ module Fontisan
105
161
  end
106
162
 
107
163
  strategy = select_strategy(source_format, target_format)
108
- tables = strategy.convert(font,
109
- options.merge(target_format: target_format))
164
+
165
+ # Enforce format ↔ option mapping at the orchestrator level so each
166
+ # strategy only sees options it declared. Cross-format misuse
167
+ # (e.g., `--zlib-level` on a WOFF2 conversion) raises here with a
168
+ # clear message; the strategy itself never has to defensively
169
+ # ignore unknown keys.
170
+ validate_strategy_options!(strategy, source_format, target_format,
171
+ options)
172
+ sliced = slice_strategy_options(options, strategy)
173
+
174
+ tables = strategy.convert(
175
+ font,
176
+ sliced.merge(target_format: target_format),
177
+ )
110
178
 
111
179
  # Preserve variation data if requested and font is variable
112
180
  if options.fetch(:preserve_variation, true) && variable_font?(font)
@@ -178,8 +246,6 @@ module Fontisan
178
246
  # @option options [Integer] :instance_index Named instance index
179
247
  # @return [Hash] Hash with :svg_xml key
180
248
  def convert_variable_to_svg(font, options = {})
181
- require_relative "../variation/variable_svg_generator"
182
-
183
249
  coordinates = options[:instance_coordinates] || {}
184
250
  generator = Variation::VariableSvgGenerator.new(font, coordinates)
185
251
 
@@ -214,7 +280,6 @@ module Fontisan
214
280
  options)
215
281
  # Case 1: Compatible formats (same outline format) - just copy tables
216
282
  if compatible_variation_formats?(source_format, target_format)
217
- require_relative "../variation/variation_preserver"
218
283
  Variation::VariationPreserver.preserve(font, tables, options)
219
284
 
220
285
  # Case 2: Different outline formats - convert variation data
@@ -278,9 +343,6 @@ options)
278
343
  # @return [Hash<String, String>] Tables with converted variation
279
344
  def convert_variation_data(font, tables, source_format, target_format,
280
345
  _options)
281
- require_relative "../variation/variation_preserver"
282
- require_relative "../variation/converter"
283
-
284
346
  # For now, just preserve common tables and warn about conversion
285
347
  warn "WARNING: Full variation conversion (#{source_format} → " \
286
348
  "#{target_format}) not yet implemented. " \
@@ -420,6 +482,55 @@ _options)
420
482
  "Cannot detect font format: missing both CFF and glyf tables"
421
483
  end
422
484
  end
485
+
486
+ # Cross-format option check. Any user-supplied key that belongs to
487
+ # another strategy (i.e., is valid for some other format) raises
488
+ # immediately — this is what makes `--zlib-level --to woff2` fail
489
+ # predictably.
490
+ #
491
+ # @param strategy [Object] Selected strategy instance
492
+ # @param source_format [Symbol]
493
+ # @param target_format [Symbol]
494
+ # @param options [Hash]
495
+ # @return [void]
496
+ # @raise [ArgumentError] if a user option is declared by another strategy
497
+ # @raise [ArgumentError] if a user option fails type/range validation
498
+ def validate_strategy_options!(strategy, source_format, target_format,
499
+ options)
500
+ this_class = strategy.class
501
+ other_classes = self.class::STRATEGY_CLASSES.reject do |k|
502
+ k == this_class
503
+ end
504
+ other_names = other_classes.flat_map do |k|
505
+ k.supported_options.map(&:name)
506
+ end
507
+
508
+ conflicts = options.keys.select do |k|
509
+ other_names.include?(k.to_sym)
510
+ end
511
+
512
+ if conflicts.any?
513
+ accepted = this_class.supported_options.map(&:name)
514
+ accepted_list = accepted.empty? ? "(none)" : accepted.join(", ")
515
+ raise ArgumentError,
516
+ "Option(s) #{conflicts.map(&:inspect).join(', ')} do not apply " \
517
+ "to #{source_format}→#{target_format} " \
518
+ "(#{this_class.name.demodulize}). " \
519
+ "Accepted: #{accepted_list}"
520
+ end
521
+
522
+ this_class.validate_options!(slice_strategy_options(options, strategy))
523
+ end
524
+
525
+ # Slice an options hash to only the keys declared by the given strategy.
526
+ #
527
+ # @param options [Hash]
528
+ # @param strategy [Object]
529
+ # @return [Hash]
530
+ def slice_strategy_options(options, strategy)
531
+ names = strategy.class.supported_options.to_set(&:name)
532
+ options.select { |k, _| names.include?(k.to_sym) }
533
+ end
423
534
  end
424
535
  end
425
536
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../tables/glyf/glyph_builder"
4
-
5
3
  module Fontisan
6
4
  module Converters
7
5
  # Builds glyf and loca tables from glyph outlines
@@ -1,24 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../conversion_options"
4
- require_relative "conversion_strategy"
5
- require_relative "../outline_extractor"
6
- require_relative "../models/outline"
7
- require_relative "../tables/cff/charstring_builder"
8
- require_relative "outline_extraction"
9
- require_relative "cff_table_builder"
10
- require_relative "glyf_table_builder"
11
- require_relative "outline_optimizer"
12
- require_relative "../hints/truetype_hint_extractor"
13
- require_relative "../hints/postscript_hint_extractor"
14
- require_relative "../hints/hint_converter"
15
- require_relative "../hints/truetype_hint_applier"
16
- require_relative "../hints/postscript_hint_applier"
17
- require_relative "../tables/cff2"
18
- require_relative "../variation/data_extractor"
19
- require_relative "../variation/instance_generator"
20
- require_relative "../variation/converter"
21
-
22
3
  module Fontisan
23
4
  module Converters
24
5
  # Strategy for converting between TTF and OTF outline formats
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../outline_extractor"
4
- require_relative "../tables/cff/charstring_builder"
5
- require_relative "../tables/glyf/glyph_builder"
6
- require_relative "../tables/glyf/compound_glyph_resolver"
7
-
8
3
  module Fontisan
9
4
  module Converters
10
5
  # Extracts all glyph outlines from a font for conversion purposes
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../optimizers/pattern_analyzer"
4
- require_relative "../optimizers/subroutine_optimizer"
5
- require_relative "../optimizers/subroutine_builder"
6
- require_relative "../optimizers/charstring_rewriter"
7
-
8
3
  module Fontisan
9
4
  module Converters
10
5
  # Optimizes CFF CharStrings using subroutine extraction
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "conversion_strategy"
4
- require_relative "../outline_extractor"
5
- require_relative "../svg/font_generator"
6
-
7
3
  module Fontisan
8
4
  module Converters
9
5
  # SVG font generator conversion strategy
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "conversion_strategy"
4
-
5
3
  module Fontisan
6
4
  module Converters
7
5
  # Strategy for same-format font operations (copy/optimize)
@@ -1,14 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../conversion_options"
4
- require_relative "../type1/charstring_converter"
5
- require_relative "../type1/cff_to_type1_converter"
6
- require_relative "../type1/font_dictionary"
7
- require_relative "../type1/charstrings"
8
- require_relative "../type1/seac_expander"
9
- require_relative "../type1_font"
10
- require_relative "cff_table_builder"
11
-
12
3
  module Fontisan
13
4
  module Converters
14
5
  # Converter for Adobe Type 1 fonts to/from SFNT formats.
@@ -1054,8 +1045,6 @@ module Fontisan
1054
1045
  # @param font [Type1Font] Source Type 1 font
1055
1046
  # @return [String] cmap table binary data
1056
1047
  def build_cmap_table(font)
1057
- require_relative "../type1/agl"
1058
-
1059
1048
  data = (+"").b
1060
1049
 
1061
1050
  # Get encoding from Type1Font