fontisan 0.2.17 → 0.2.23

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 (249) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/.rubocop_todo.yml +86 -86
  4. data/CHANGELOG.md +12 -2
  5. data/README.adoc +53 -1
  6. data/docs/.vitepress/config.ts +68 -8
  7. data/docs/.vitepress/theme/style.css +570 -272
  8. data/docs/CONVERSION_GUIDE.adoc +31 -8
  9. data/docs/EXTRACT_TTC_MIGRATION.md +1 -1
  10. data/docs/WOFF_WOFF2_FORMATS.adoc +53 -0
  11. data/docs/api/conversion-options.md +37 -14
  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/base_collection.rb +23 -9
  33. data/lib/fontisan/binary/structures.rb +0 -2
  34. data/lib/fontisan/binary.rb +11 -0
  35. data/lib/fontisan/cldr/aggregator.rb +33 -0
  36. data/lib/fontisan/cldr/cache_manager.rb +110 -0
  37. data/lib/fontisan/cldr/config.rb +59 -0
  38. data/lib/fontisan/cldr/download_error.rb +9 -0
  39. data/lib/fontisan/cldr/downloader.rb +79 -0
  40. data/lib/fontisan/cldr/error.rb +8 -0
  41. data/lib/fontisan/cldr/index.rb +64 -0
  42. data/lib/fontisan/cldr/index_builder.rb +72 -0
  43. data/lib/fontisan/cldr/unicode_set_parser.rb +189 -0
  44. data/lib/fontisan/cldr/unknown_version_error.rb +9 -0
  45. data/lib/fontisan/cldr/version_resolver.rb +91 -0
  46. data/lib/fontisan/cldr.rb +23 -0
  47. data/lib/fontisan/cli/cldr_cli.rb +85 -0
  48. data/lib/fontisan/cli.rb +31 -2
  49. data/lib/fontisan/collection/builder.rb +0 -4
  50. data/lib/fontisan/collection/dfont_builder.rb +0 -4
  51. data/lib/fontisan/collection/shared_logic.rb +0 -2
  52. data/lib/fontisan/collection/writer.rb +0 -3
  53. data/lib/fontisan/collection.rb +15 -0
  54. data/lib/fontisan/commands/base_command.rb +0 -3
  55. data/lib/fontisan/commands/convert_command.rb +25 -20
  56. data/lib/fontisan/commands/dump_table_command.rb +0 -3
  57. data/lib/fontisan/commands/export_command.rb +0 -4
  58. data/lib/fontisan/commands/features_command.rb +0 -3
  59. data/lib/fontisan/commands/instance_command.rb +0 -5
  60. data/lib/fontisan/commands/ls_command.rb +0 -6
  61. data/lib/fontisan/commands/optical_size_command.rb +0 -3
  62. data/lib/fontisan/commands/pack_command.rb +0 -5
  63. data/lib/fontisan/commands/scripts_command.rb +0 -2
  64. data/lib/fontisan/commands/subset_command.rb +0 -3
  65. data/lib/fontisan/commands/unicode_command.rb +0 -3
  66. data/lib/fontisan/commands/unpack_command.rb +0 -7
  67. data/lib/fontisan/commands/validate_command.rb +0 -8
  68. data/lib/fontisan/commands/variable_command.rb +0 -3
  69. data/lib/fontisan/commands.rb +26 -0
  70. data/lib/fontisan/config/cldr.yml +22 -0
  71. data/lib/fontisan/config/conversion_matrix.yml +38 -0
  72. data/lib/fontisan/constants.rb +19 -0
  73. data/lib/fontisan/conversion_options.rb +30 -19
  74. data/lib/fontisan/converters/cff_table_builder.rb +0 -3
  75. data/lib/fontisan/converters/collection_converter.rb +0 -8
  76. data/lib/fontisan/converters/conversion_strategy.rb +161 -46
  77. data/lib/fontisan/converters/format_converter.rb +143 -32
  78. data/lib/fontisan/converters/glyf_table_builder.rb +0 -2
  79. data/lib/fontisan/converters/outline_converter.rb +0 -19
  80. data/lib/fontisan/converters/outline_extraction.rb +0 -5
  81. data/lib/fontisan/converters/outline_optimizer.rb +0 -5
  82. data/lib/fontisan/converters/svg_generator.rb +0 -4
  83. data/lib/fontisan/converters/table_copier.rb +0 -2
  84. data/lib/fontisan/converters/type1_converter.rb +0 -11
  85. data/lib/fontisan/converters/woff2_encoder.rb +49 -20
  86. data/lib/fontisan/converters/woff_writer.rb +211 -282
  87. data/lib/fontisan/converters.rb +21 -0
  88. data/lib/fontisan/dfont_collection.rb +29 -10
  89. data/lib/fontisan/export/exporter.rb +0 -6
  90. data/lib/fontisan/export/transformers/font_to_ttx.rb +0 -9
  91. data/lib/fontisan/export/transformers/head_transformer.rb +0 -2
  92. data/lib/fontisan/export/transformers/hhea_transformer.rb +0 -2
  93. data/lib/fontisan/export/transformers/maxp_transformer.rb +0 -2
  94. data/lib/fontisan/export/transformers/name_transformer.rb +0 -2
  95. data/lib/fontisan/export/transformers/os2_transformer.rb +0 -2
  96. data/lib/fontisan/export/transformers/post_transformer.rb +0 -2
  97. data/lib/fontisan/export/transformers.rb +17 -0
  98. data/lib/fontisan/export.rb +13 -0
  99. data/lib/fontisan/font_loader.rb +14 -19
  100. data/lib/fontisan/font_writer.rb +0 -1
  101. data/lib/fontisan/formatters.rb +9 -0
  102. data/lib/fontisan/hints/hint_converter.rb +0 -1
  103. data/lib/fontisan/hints/postscript_hint_applier.rb +0 -9
  104. data/lib/fontisan/hints/postscript_hint_extractor.rb +0 -2
  105. data/lib/fontisan/hints/truetype_hint_extractor.rb +0 -2
  106. data/lib/fontisan/hints.rb +19 -0
  107. data/lib/fontisan/metrics_calculator.rb +0 -2
  108. data/lib/fontisan/models/all_scripts_features_info.rb +0 -1
  109. data/lib/fontisan/models/cldr/language_coverage.rb +31 -0
  110. data/lib/fontisan/models/cldr.rb +12 -0
  111. data/lib/fontisan/models/collection_brief_info.rb +0 -1
  112. data/lib/fontisan/models/collection_info.rb +0 -2
  113. data/lib/fontisan/models/collection_list_info.rb +0 -1
  114. data/lib/fontisan/models/collection_validation_report.rb +0 -2
  115. data/lib/fontisan/models/color_glyph.rb +0 -1
  116. data/lib/fontisan/models/font_report.rb +0 -1
  117. data/lib/fontisan/models/ttx/tables.rb +21 -0
  118. data/lib/fontisan/models/ttx/ttfont.rb +0 -8
  119. data/lib/fontisan/models/ttx.rb +14 -0
  120. data/lib/fontisan/models.rb +47 -0
  121. data/lib/fontisan/open_type_collection.rb +6 -5
  122. data/lib/fontisan/open_type_font.rb +8 -2
  123. data/lib/fontisan/open_type_font_extensions.rb +9 -9
  124. data/lib/fontisan/optimizers/pattern_analyzer.rb +0 -1
  125. data/lib/fontisan/optimizers.rb +14 -0
  126. data/lib/fontisan/outline_extractor.rb +0 -2
  127. data/lib/fontisan/parsers/dfont_parser.rb +0 -1
  128. data/lib/fontisan/parsers.rb +10 -0
  129. data/lib/fontisan/pipeline/format_detector.rb +29 -102
  130. data/lib/fontisan/pipeline/output_writer.rb +11 -9
  131. data/lib/fontisan/pipeline/strategies/instance_strategy.rb +0 -4
  132. data/lib/fontisan/pipeline/strategies/named_strategy.rb +0 -4
  133. data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +0 -2
  134. data/lib/fontisan/pipeline/strategies.rb +16 -0
  135. data/lib/fontisan/pipeline/transformation_pipeline.rb +0 -7
  136. data/lib/fontisan/pipeline/variation_resolver.rb +0 -7
  137. data/lib/fontisan/pipeline.rb +14 -0
  138. data/lib/fontisan/sfnt_font.rb +29 -14
  139. data/lib/fontisan/sfnt_table.rb +0 -4
  140. data/lib/fontisan/subset/builder.rb +0 -6
  141. data/lib/fontisan/subset.rb +13 -0
  142. data/lib/fontisan/svg/font_generator.rb +0 -4
  143. data/lib/fontisan/svg/glyph_generator.rb +0 -2
  144. data/lib/fontisan/svg.rb +12 -0
  145. data/lib/fontisan/tables/cbdt.rb +0 -1
  146. data/lib/fontisan/tables/cblc.rb +0 -1
  147. data/lib/fontisan/tables/cff/charset.rb +0 -1
  148. data/lib/fontisan/tables/cff/charstring.rb +0 -1
  149. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +0 -4
  150. data/lib/fontisan/tables/cff/charstrings_index.rb +0 -3
  151. data/lib/fontisan/tables/cff/dict.rb +0 -1
  152. data/lib/fontisan/tables/cff/encoding.rb +0 -1
  153. data/lib/fontisan/tables/cff/header.rb +0 -2
  154. data/lib/fontisan/tables/cff/hint_operation_injector.rb +0 -2
  155. data/lib/fontisan/tables/cff/index.rb +0 -1
  156. data/lib/fontisan/tables/cff/private_dict.rb +0 -2
  157. data/lib/fontisan/tables/cff/private_dict_writer.rb +0 -2
  158. data/lib/fontisan/tables/cff/table_builder.rb +0 -6
  159. data/lib/fontisan/tables/cff/top_dict.rb +0 -2
  160. data/lib/fontisan/tables/cff.rb +23 -15
  161. data/lib/fontisan/tables/cff2/charstring_parser.rb +0 -2
  162. data/lib/fontisan/tables/cff2/table_builder.rb +0 -11
  163. data/lib/fontisan/tables/cff2/table_reader.rb +0 -2
  164. data/lib/fontisan/tables/cff2.rb +13 -14
  165. data/lib/fontisan/tables/cmap.rb +24 -2
  166. data/lib/fontisan/tables/cmap_table.rb +0 -3
  167. data/lib/fontisan/tables/colr.rb +0 -1
  168. data/lib/fontisan/tables/cpal.rb +0 -1
  169. data/lib/fontisan/tables/cvar.rb +0 -2
  170. data/lib/fontisan/tables/fvar.rb +0 -1
  171. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +0 -2
  172. data/lib/fontisan/tables/glyf/glyph_builder.rb +0 -3
  173. data/lib/fontisan/tables/glyf.rb +0 -6
  174. data/lib/fontisan/tables/glyf_table.rb +0 -3
  175. data/lib/fontisan/tables/gpos.rb +0 -2
  176. data/lib/fontisan/tables/gsub.rb +0 -2
  177. data/lib/fontisan/tables/gvar.rb +0 -2
  178. data/lib/fontisan/tables/head.rb +0 -2
  179. data/lib/fontisan/tables/head_table.rb +0 -3
  180. data/lib/fontisan/tables/hhea.rb +0 -2
  181. data/lib/fontisan/tables/hhea_table.rb +0 -3
  182. data/lib/fontisan/tables/hmtx.rb +0 -2
  183. data/lib/fontisan/tables/hmtx_table.rb +0 -3
  184. data/lib/fontisan/tables/hvar.rb +0 -3
  185. data/lib/fontisan/tables/loca.rb +0 -2
  186. data/lib/fontisan/tables/loca_table.rb +0 -3
  187. data/lib/fontisan/tables/maxp.rb +0 -2
  188. data/lib/fontisan/tables/maxp_table.rb +0 -3
  189. data/lib/fontisan/tables/mvar.rb +0 -3
  190. data/lib/fontisan/tables/name.rb +0 -2
  191. data/lib/fontisan/tables/name_table.rb +0 -3
  192. data/lib/fontisan/tables/os2_table.rb +0 -3
  193. data/lib/fontisan/tables/post_table.rb +0 -3
  194. data/lib/fontisan/tables/sbix.rb +0 -1
  195. data/lib/fontisan/tables/svg.rb +0 -1
  196. data/lib/fontisan/tables/variation_common.rb +0 -1
  197. data/lib/fontisan/tables/vvar.rb +0 -3
  198. data/lib/fontisan/tables.rb +55 -0
  199. data/lib/fontisan/true_type_collection.rb +6 -14
  200. data/lib/fontisan/true_type_font.rb +8 -2
  201. data/lib/fontisan/true_type_font_extensions.rb +9 -9
  202. data/lib/fontisan/type1/afm_generator.rb +0 -4
  203. data/lib/fontisan/type1/conversion_options.rb +0 -2
  204. data/lib/fontisan/type1/encodings.rb +0 -2
  205. data/lib/fontisan/type1/generator.rb +0 -8
  206. data/lib/fontisan/type1/pfa_generator.rb +0 -3
  207. data/lib/fontisan/type1/pfb_generator.rb +0 -5
  208. data/lib/fontisan/type1/pfm_generator.rb +0 -4
  209. data/lib/fontisan/type1.rb +42 -69
  210. data/lib/fontisan/type1_font.rb +40 -11
  211. data/lib/fontisan/utilities/checksum_calculator.rb +0 -1
  212. data/lib/fontisan/utilities.rb +10 -0
  213. data/lib/fontisan/utils.rb +10 -0
  214. data/lib/fontisan/validation/collection_validator.rb +0 -2
  215. data/lib/fontisan/validation.rb +9 -0
  216. data/lib/fontisan/validators/basic_validator.rb +0 -2
  217. data/lib/fontisan/validators/font_book_validator.rb +0 -2
  218. data/lib/fontisan/validators/opentype_validator.rb +0 -2
  219. data/lib/fontisan/validators/profile_loader.rb +0 -5
  220. data/lib/fontisan/validators/validator.rb +0 -2
  221. data/lib/fontisan/validators/web_font_validator.rb +0 -2
  222. data/lib/fontisan/validators.rb +14 -0
  223. data/lib/fontisan/variable/delta_applicator.rb +0 -4
  224. data/lib/fontisan/variable/instancer.rb +0 -3
  225. data/lib/fontisan/variable/static_font_builder.rb +0 -3
  226. data/lib/fontisan/variable.rb +16 -0
  227. data/lib/fontisan/variation/blend_applier.rb +0 -2
  228. data/lib/fontisan/variation/cache.rb +0 -2
  229. data/lib/fontisan/variation/converter.rb +0 -3
  230. data/lib/fontisan/variation/data_extractor.rb +0 -2
  231. data/lib/fontisan/variation/delta_applier.rb +0 -5
  232. data/lib/fontisan/variation/inspector.rb +0 -1
  233. data/lib/fontisan/variation/instance_generator.rb +0 -6
  234. data/lib/fontisan/variation/instance_writer.rb +0 -5
  235. data/lib/fontisan/variation/metrics_adjuster.rb +0 -4
  236. data/lib/fontisan/variation/optimizer.rb +0 -3
  237. data/lib/fontisan/variation/parallel_generator.rb +0 -3
  238. data/lib/fontisan/variation/subsetter.rb +0 -4
  239. data/lib/fontisan/variation/tuple_variation_header.rb +0 -2
  240. data/lib/fontisan/variation/variable_svg_generator.rb +0 -3
  241. data/lib/fontisan/variation/variation_context.rb +0 -3
  242. data/lib/fontisan/variation/variation_preserver.rb +0 -3
  243. data/lib/fontisan/variation.rb +31 -0
  244. data/lib/fontisan/version.rb +1 -1
  245. data/lib/fontisan/woff2.rb +13 -0
  246. data/lib/fontisan/woff2_font.rb +31 -9
  247. data/lib/fontisan/woff_font.rb +31 -2
  248. data/lib/fontisan.rb +121 -196
  249. metadata +46 -2
@@ -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
@@ -1,11 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "conversion_strategy"
4
- require_relative "../woff2/header"
5
- require_relative "../woff2/directory"
6
- require_relative "../woff2/table_transformer"
7
- require_relative "../utilities/brotli_wrapper"
8
- require_relative "../utilities/checksum_calculator"
9
3
  # Validation temporarily disabled - will be reimplemented with new DSL framework in Week 3+
10
4
  # require_relative "../validation/woff2_validator"
11
5
  require "yaml"
@@ -50,31 +44,45 @@ module Fontisan
50
44
  # @return [Hash] Configuration settings
51
45
  attr_reader :config
52
46
 
53
- # Initialize encoder with configuration
47
+ # Initialize encoder with configuration.
48
+ #
49
+ # The encoder is stateless per call; all conversion knobs come through
50
+ # the per-convert options hash. The config file supplies default values
51
+ # only for fields not expressed via the DSL.
54
52
  #
55
53
  # @param config_path [String, nil] Path to config file
56
54
  def initialize(config_path: nil)
57
55
  @config = load_configuration(config_path)
58
56
  end
59
57
 
60
- # Convert font to WOFF2 format
58
+ option :brotli_quality, type: :integer, range: 0..11, default: 11,
59
+ cli: "--brotli-quality=N",
60
+ desc: "Brotli quality (0=fastest, 11=smallest)"
61
+ option :transform_tables, type: :boolean, default: false,
62
+ cli: "--[no-]transform-tables",
63
+ desc: "apply glyf/loca and hmtx transformations"
64
+
65
+ # Convert font to WOFF2 format.
61
66
  #
62
- # Returns a hash with :woff2_binary key containing complete WOFF2 file.
63
- # This is different from other converters that return table data.
67
+ # Returns a hash with `:woff2_binary` containing the complete WOFF2 file.
64
68
  #
65
69
  # @param font [TrueTypeFont, OpenTypeFont] Source font
66
- # @param options [Hash] Conversion options
67
- # @option options [Integer] :quality Brotli quality (0-11)
68
- # @option options [Boolean] :transform_tables Apply table transformations
69
- # @option options [Boolean] :validate Run validation after encoding
70
- # @option options [Symbol] :validation_level Validation level (:strict, :standard, :lenient)
71
- # @return [Hash] Hash with :woff2_binary and optional :validation_report keys
72
- # @raise [Error] If encoding fails
70
+ # @param options [Hash{Symbol => Object}] Per-call options:
71
+ # - `:brotli_quality` (0–11, default from config or 11)
72
+ # - `:transform_tables` (bool, default false)
73
+ # - `:quality` legacy alias for `:brotli_quality` (backward compat)
74
+ # @return [Hash{Symbol => String}] `{ woff2_binary: <bytes> }`
75
+ # @raise [ArgumentError] if any option fails validation
76
+ # @raise [Fontisan::Error] if encoding fails
73
77
  def convert(font, options = {})
74
78
  validate(font, :woff2)
75
79
 
76
- # Get Brotli quality from options or config
77
- quality = options[:quality] || config["brotli"]["quality"]
80
+ resolved = normalize_legacy_quality(options)
81
+ self.class.validate_options!(strategy_options(resolved))
82
+
83
+ quality = resolved.fetch(:brotli_quality) do
84
+ config["brotli"]["quality"]
85
+ end
78
86
 
79
87
  # Detect font flavor
80
88
  flavor = detect_flavor(font)
@@ -84,7 +92,7 @@ module Fontisan
84
92
 
85
93
  # Transform tables (if enabled)
86
94
  transformer = Woff2::TableTransformer.new(font)
87
- transform_enabled = options.fetch(:transform_tables, false)
95
+ transform_enabled = resolved.fetch(:transform_tables, false)
88
96
 
89
97
  # Build table directory entries
90
98
  entries = build_table_entries(table_data, transformer,
@@ -201,6 +209,27 @@ module Fontisan
201
209
  # Extend Woff2Font with in-memory loading
202
210
  Woff2Font.singleton_class.prepend(Woff2FontMemoryLoader)
203
211
 
212
+ # Normalize legacy `:quality` option to `:brotli_quality`.
213
+ # Internal callers and older specs use `:quality`; the public DSL uses
214
+ # `:brotli_quality`. Returns a new hash; does not mutate the input.
215
+ #
216
+ # @param options [Hash]
217
+ # @return [Hash]
218
+ def normalize_legacy_quality(options)
219
+ return options unless options.key?(:quality) && !options.key?(:brotli_quality)
220
+
221
+ options.merge(brotli_quality: options[:quality])
222
+ end
223
+
224
+ # Slice options to those declared by this strategy.
225
+ #
226
+ # @param options [Hash]
227
+ # @return [Hash]
228
+ def strategy_options(options)
229
+ names = self.class.supported_options.to_set(&:name)
230
+ options.select { |k, _| names.include?(k.to_sym) }
231
+ end
232
+
204
233
  # Load configuration from YAML file
205
234
  #
206
235
  # @param path [String, nil] Path to config file