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,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base_command"
4
- require_relative "../models/optical_size_info"
5
-
6
3
  module Fontisan
7
4
  module Commands
8
5
  # Command to extract optical size information from fonts
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base_command"
4
- require_relative "../collection/builder"
5
- require_relative "../collection/dfont_builder"
6
- require_relative "../font_loader"
7
-
8
3
  module Fontisan
9
4
  module Commands
10
5
  # Command for packing multiple fonts into a TTC/OTC collection
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "yaml"
4
- require_relative "base_command"
5
- require_relative "../models/scripts_info"
6
4
 
7
5
  module Fontisan
8
6
  module Commands
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base_command"
4
- require_relative "../subset/builder"
5
- require_relative "../subset/options"
6
3
  require "fileutils"
7
4
 
8
5
  module Fontisan
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base_command"
4
- require_relative "../models/unicode_mappings"
5
-
6
3
  module Fontisan
7
4
  module Commands
8
5
  # Command to list Unicode to glyph index mappings from a font file
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base_command"
4
- require_relative "../font_loader"
5
- require_relative "../font_writer"
6
3
  require "fileutils"
7
4
 
8
5
  module Fontisan
@@ -144,10 +141,8 @@ module Fontisan
144
141
  # Load as TTC or OTC based on extension hint
145
142
  # Both use same structure, main difference is expected font types
146
143
  if ext == ".otc"
147
- require_relative "../open_type_collection"
148
144
  OpenTypeCollection.read(io)
149
145
  else
150
- require_relative "../true_type_collection"
151
146
  TrueTypeCollection.read(io)
152
147
  end
153
148
  end
@@ -252,8 +247,6 @@ module Fontisan
252
247
  # @param output_path [String] Output file path
253
248
  # @return [void]
254
249
  def convert_and_write(font, output_path)
255
- require_relative "../converters/format_converter"
256
-
257
250
  converter = Converters::FormatConverter.new
258
251
  converter.convert(font, @format, output_path: output_path)
259
252
  rescue StandardError => e
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base_command"
4
- require_relative "../validators/profile_loader"
5
- require_relative "../font_loader"
6
- require_relative "../tables/name"
7
-
8
3
  module Fontisan
9
4
  module Commands
10
5
  # ValidateCommand provides CLI interface for font validation
@@ -138,9 +133,6 @@ module Fontisan
138
133
  # @param mode [Symbol] Loading mode
139
134
  # @return [Integer] Exit code
140
135
  def validate_collection(mode)
141
- require_relative "../models/collection_validation_report"
142
- require_relative "../models/font_report"
143
-
144
136
  # Load collection metadata
145
137
  collection = FontLoader.load_collection(@input)
146
138
 
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base_command"
4
- require_relative "../variation/inspector"
5
-
6
3
  module Fontisan
7
4
  module Commands
8
5
  # Command to extract variable font information.
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Autoload hub for the Fontisan::Commands namespace.
4
+
5
+ module Fontisan
6
+ module Commands
7
+ autoload :BaseCommand, "fontisan/commands/base_command"
8
+ autoload :ConvertCommand, "fontisan/commands/convert_command"
9
+ autoload :DumpTableCommand, "fontisan/commands/dump_table_command"
10
+ autoload :ExportCommand, "fontisan/commands/export_command"
11
+ autoload :FeaturesCommand, "fontisan/commands/features_command"
12
+ autoload :GlyphsCommand, "fontisan/commands/glyphs_command"
13
+ autoload :InfoCommand, "fontisan/commands/info_command"
14
+ autoload :InstanceCommand, "fontisan/commands/instance_command"
15
+ autoload :LsCommand, "fontisan/commands/ls_command"
16
+ autoload :OpticalSizeCommand, "fontisan/commands/optical_size_command"
17
+ autoload :PackCommand, "fontisan/commands/pack_command"
18
+ autoload :ScriptsCommand, "fontisan/commands/scripts_command"
19
+ autoload :SubsetCommand, "fontisan/commands/subset_command"
20
+ autoload :TablesCommand, "fontisan/commands/tables_command"
21
+ autoload :UnicodeCommand, "fontisan/commands/unicode_command"
22
+ autoload :UnpackCommand, "fontisan/commands/unpack_command"
23
+ autoload :ValidateCommand, "fontisan/commands/validate_command"
24
+ autoload :VariableCommand, "fontisan/commands/variable_command"
25
+ end
26
+ end
@@ -0,0 +1,22 @@
1
+ # UCD alternative: CLDR (Common Locale Data Repository) configuration.
2
+ #
3
+ # This file is the single source of truth for which CLDR version Fontisan
4
+ # uses by default, what versions are recognized, and where to fetch them.
5
+ #
6
+ # Update `default_version` and `known_versions` when a new CLDR release
7
+ # ships (typically 2× per year, alongside Unicode releases).
8
+ # Users override at runtime via `fontisan cldr download --version X.Y.Z`
9
+ # or `fontisan audit --cldr-version X.Y.Z`.
10
+
11
+ default_version: "46.0.0"
12
+
13
+ known_versions:
14
+ - "45.0.0"
15
+ - "46.0.0"
16
+
17
+ # CLDR JSON releases are published on GitHub.
18
+ # Full URL: <base_url>/<version>/cldr-<version>-json-full.zip
19
+ base_url: "https://github.com/unicode-org/cldr-json/releases/download"
20
+
21
+ # Releases listing URL for `--latest` probing. JSON API.
22
+ listing_url: "https://api.github.com/repos/unicode-org/cldr-json/releases"
@@ -174,6 +174,25 @@ conversions:
174
174
  WOFF decompression with zlib. Preserves all font data and metadata.
175
175
  Automatically detects font flavor from WOFF header.
176
176
 
177
+ - from: woff
178
+ to: woff2
179
+ strategy: woff_reader
180
+ description: "Transcode WOFF to WOFF2 (re-wrap with Brotli)"
181
+ status: implemented
182
+ notes: >
183
+ Web-format transcoding. The pipeline decodes WOFF (zlib) into its
184
+ underlying SFNT tables, then re-wraps them as WOFF2 with Brotli.
185
+ Useful for migrating legacy web fonts to the smaller WOFF2 format.
186
+
187
+ - from: woff
188
+ to: svg
189
+ strategy: svg_generator
190
+ description: "Generate SVG font from WOFF"
191
+ status: implemented
192
+ notes: >
193
+ Decodes WOFF (zlib) to SFNT tables, then renders glyph outlines as
194
+ SVG paths. Note: SVG fonts are deprecated in favor of WOFF2.
195
+
177
196
  # Phase 2 conversions (Milestone 2.2) - SVG
178
197
  - from: ttf
179
198
  to: svg
@@ -254,6 +273,25 @@ conversions:
254
273
  Same-format copy operation. Decompresses and re-compresses WOFF2 data
255
274
  with Brotli, useful for normalizing compression or updating metadata.
256
275
 
276
+ - from: woff2
277
+ to: woff
278
+ strategy: woff2_decoder
279
+ description: "Transcode WOFF2 to WOFF (re-wrap with zlib)"
280
+ status: implemented
281
+ notes: >
282
+ Web-format transcoding. The pipeline decodes WOFF2 (Brotli) into its
283
+ underlying SFNT tables, then re-wraps them as WOFF with zlib. Useful
284
+ for legacy browser compatibility where WOFF2 is unavailable.
285
+
286
+ - from: woff2
287
+ to: svg
288
+ strategy: svg_generator
289
+ description: "Generate SVG font from WOFF2"
290
+ status: implemented
291
+ notes: >
292
+ Decodes WOFF2 (Brotli) to SFNT tables, then renders glyph outlines as
293
+ SVG paths. Note: SVG fonts are deprecated in favor of WOFF2.
294
+
257
295
  # Collection format conversions (TTC/OTC/dfont)
258
296
  # Same-format collection operations
259
297
  - from: ttc
@@ -89,6 +89,9 @@ module Fontisan
89
89
  # Fvar table tag identifier (Font Variations)
90
90
  FVAR_TAG = "fvar"
91
91
 
92
+ # Avar table tag identifier (Axis Variation)
93
+ AVAR_TAG = "avar"
94
+
92
95
  # Gvar table tag identifier (Glyph Variations for TrueType)
93
96
  GVAR_TAG = "gvar"
94
97
 
@@ -129,6 +132,22 @@ module Fontisan
129
132
  # Control Value Table (metrics used by TrueType hinting)
130
133
  CVT_TAG = "cvt "
131
134
 
135
+ # Grid-fitting And Scan-conversion Procedure table (per-ppem hinting policy)
136
+ GASP_TAG = "gasp"
137
+
138
+ # Color font tables.
139
+ # COLR: COLR vector color glyph table (v0 or v1).
140
+ # CPAL: Color Palette table.
141
+ # SVG: SVG-in-OpenType color glyph table (tag is "SVG " — 4 bytes incl. trailing space).
142
+ # CBDT/CBLC: Color Bitmap Data / Location (paired — EBLC/EBDT equivalents for color).
143
+ # sbix: Apple bitmap glyph table.
144
+ COLR_TAG = "COLR"
145
+ CPAL_TAG = "CPAL"
146
+ SVG_TAG = "SVG "
147
+ CBDT_TAG = "CBDT"
148
+ CBLC_TAG = "CBLC"
149
+ SBIX_TAG = "sbix"
150
+
132
151
  # Magic number used for font file checksum adjustment calculation.
133
152
  # This constant is used in conjunction with the file checksum to compute
134
153
  # the checksumAdjustment value stored in the 'head' table.
@@ -35,8 +35,12 @@ module Fontisan
35
35
  preserve_encoding
36
36
  ].freeze
37
37
 
38
- # Generating options (output processing)
39
- GENERATING_OPTIONS = %i[
38
+ # Generating options (output processing) declared statically by
39
+ # ConversionOptions itself. Format-specific compression knobs (zlib_level,
40
+ # brotli_quality, transform_tables, etc.) are declared by each strategy
41
+ # via the ConversionStrategy DSL and discovered via
42
+ # FormatConverter.all_strategy_option_names — see `.generating_options`.
43
+ STATIC_GENERATING_OPTIONS = %i[
40
44
  write_pfm
41
45
  write_afm
42
46
  write_inf
@@ -47,19 +51,28 @@ module Fontisan
47
51
  optimize_tables
48
52
  reencode_first_256
49
53
  encoding_vector
50
- compression
51
- transform_tables
52
54
  preserve_metadata
53
55
  strip_metadata
54
56
  target_format
55
57
  ].freeze
56
58
 
59
+ # Full list of recognized generating options, computed lazily because
60
+ # converters `require_relative` this file, creating a load cycle that
61
+ # prevents resolving `FormatConverter` at class-definition time. The first
62
+ # call resolves all strategies (which are loaded before any conversion
63
+ # actually runs) and caches the result.
64
+ #
65
+ # @return [Array<Symbol>]
66
+ def self.generating_options
67
+ @generating_options ||= (
68
+ STATIC_GENERATING_OPTIONS +
69
+ Converters::FormatConverter.all_strategy_option_names
70
+ ).uniq.freeze
71
+ end
72
+
57
73
  # Valid hinting modes
58
74
  HINTING_MODES = %w[preserve auto none full].freeze
59
75
 
60
- # Valid compression modes
61
- COMPRESSION_MODES = %w[zlib brotli none].freeze
62
-
63
76
  attr_reader :from, :to, :opening, :generating
64
77
 
65
78
  # Initialize conversion options
@@ -199,9 +212,9 @@ module Fontisan
199
212
  # Validate generating options
200
213
  def validate_generating_options!
201
214
  @generating.each_key do |key|
202
- unless GENERATING_OPTIONS.include?(key)
215
+ unless self.class.generating_options.include?(key)
203
216
  raise ArgumentError, "Unknown generating option: #{key}. " \
204
- "Available: #{GENERATING_OPTIONS.join(', ')}"
217
+ "Available: #{self.class.generating_options.join(', ')}"
205
218
  end
206
219
  end
207
220
 
@@ -214,15 +227,6 @@ module Fontisan
214
227
  end
215
228
  end
216
229
 
217
- # Validate compression mode
218
- if @generating[:compression]
219
- comp = @generating[:compression].to_s
220
- unless COMPRESSION_MODES.include?(comp)
221
- raise ArgumentError, "Invalid compression: #{comp}. " \
222
- "Available: #{COMPRESSION_MODES.join(', ')}"
223
- end
224
- end
225
-
226
230
  # Validate target_format for collection conversions
227
231
  if @generating[:target_format]
228
232
  target = @generating[:target_format].to_s
@@ -364,9 +368,16 @@ module Fontisan
364
368
  from: :otf,
365
369
  to: :woff2,
366
370
  opening: {},
367
- generating: { compression: "brotli", transform_tables: true,
371
+ generating: { brotli_quality: 11, transform_tables: true,
368
372
  optimize_tables: true, preserve_metadata: true },
369
373
  },
374
+ legacy_web: {
375
+ from: :otf,
376
+ to: :woff,
377
+ opening: {},
378
+ generating: { zlib_level: 9, optimize_tables: true,
379
+ preserve_metadata: true },
380
+ },
370
381
  archive_to_modern: {
371
382
  from: :ttc,
372
383
  to: :otf,
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../tables/cff/index_builder"
4
- require_relative "../tables/cff/dict_builder"
5
-
6
3
  module Fontisan
7
4
  module Converters
8
5
  # Builds CFF table data from glyph outlines
@@ -1,12 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../conversion_options"
4
- require_relative "format_converter"
5
- require_relative "../collection/builder"
6
- require_relative "../collection/dfont_builder"
7
- require_relative "../parsers/dfont_parser"
8
- require_relative "../font_loader"
9
-
10
3
  module Fontisan
11
4
  module Converters
12
5
  # CollectionConverter handles conversion between collection formats
@@ -343,7 +336,6 @@ conv_options = nil)
343
336
  # @return [Font] Font object
344
337
  def build_font_from_tables(tables, format)
345
338
  # Create temporary font from tables
346
- require_relative "../font_writer"
347
339
  require "stringio"
348
340
 
349
341
  sfnt_version = format == :otf ? 0x4F54544F : 0x00010000
@@ -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