fontisan 0.1.0 → 0.2.1

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 (214) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +672 -69
  3. data/Gemfile +1 -0
  4. data/LICENSE +5 -1
  5. data/README.adoc +1477 -297
  6. data/Rakefile +63 -41
  7. data/benchmark/variation_quick_bench.rb +47 -0
  8. data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
  9. data/fontisan.gemspec +4 -1
  10. data/lib/fontisan/binary/base_record.rb +22 -1
  11. data/lib/fontisan/cli.rb +364 -4
  12. data/lib/fontisan/collection/builder.rb +341 -0
  13. data/lib/fontisan/collection/offset_calculator.rb +227 -0
  14. data/lib/fontisan/collection/table_analyzer.rb +204 -0
  15. data/lib/fontisan/collection/table_deduplicator.rb +317 -0
  16. data/lib/fontisan/collection/writer.rb +306 -0
  17. data/lib/fontisan/commands/base_command.rb +24 -1
  18. data/lib/fontisan/commands/convert_command.rb +218 -0
  19. data/lib/fontisan/commands/export_command.rb +161 -0
  20. data/lib/fontisan/commands/info_command.rb +40 -6
  21. data/lib/fontisan/commands/instance_command.rb +286 -0
  22. data/lib/fontisan/commands/ls_command.rb +113 -0
  23. data/lib/fontisan/commands/pack_command.rb +241 -0
  24. data/lib/fontisan/commands/subset_command.rb +245 -0
  25. data/lib/fontisan/commands/unpack_command.rb +338 -0
  26. data/lib/fontisan/commands/validate_command.rb +203 -0
  27. data/lib/fontisan/commands/variable_command.rb +30 -1
  28. data/lib/fontisan/config/collection_settings.yml +56 -0
  29. data/lib/fontisan/config/conversion_matrix.yml +212 -0
  30. data/lib/fontisan/config/export_settings.yml +66 -0
  31. data/lib/fontisan/config/subset_profiles.yml +100 -0
  32. data/lib/fontisan/config/svg_settings.yml +60 -0
  33. data/lib/fontisan/config/validation_rules.yml +149 -0
  34. data/lib/fontisan/config/variable_settings.yml +99 -0
  35. data/lib/fontisan/config/woff2_settings.yml +77 -0
  36. data/lib/fontisan/constants.rb +79 -0
  37. data/lib/fontisan/converters/conversion_strategy.rb +96 -0
  38. data/lib/fontisan/converters/format_converter.rb +408 -0
  39. data/lib/fontisan/converters/outline_converter.rb +998 -0
  40. data/lib/fontisan/converters/svg_generator.rb +244 -0
  41. data/lib/fontisan/converters/table_copier.rb +117 -0
  42. data/lib/fontisan/converters/woff2_encoder.rb +416 -0
  43. data/lib/fontisan/converters/woff_writer.rb +391 -0
  44. data/lib/fontisan/error.rb +203 -0
  45. data/lib/fontisan/export/exporter.rb +262 -0
  46. data/lib/fontisan/export/table_serializer.rb +255 -0
  47. data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
  48. data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
  49. data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
  50. data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
  51. data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
  52. data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
  53. data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
  54. data/lib/fontisan/export/ttx_generator.rb +527 -0
  55. data/lib/fontisan/export/ttx_parser.rb +300 -0
  56. data/lib/fontisan/font_loader.rb +122 -15
  57. data/lib/fontisan/font_writer.rb +302 -0
  58. data/lib/fontisan/formatters/text_formatter.rb +102 -0
  59. data/lib/fontisan/glyph_accessor.rb +503 -0
  60. data/lib/fontisan/hints/hint_converter.rb +310 -0
  61. data/lib/fontisan/hints/postscript_hint_applier.rb +266 -0
  62. data/lib/fontisan/hints/postscript_hint_extractor.rb +354 -0
  63. data/lib/fontisan/hints/truetype_hint_applier.rb +117 -0
  64. data/lib/fontisan/hints/truetype_hint_extractor.rb +289 -0
  65. data/lib/fontisan/loading_modes.rb +115 -0
  66. data/lib/fontisan/metrics_calculator.rb +277 -0
  67. data/lib/fontisan/models/collection_font_summary.rb +52 -0
  68. data/lib/fontisan/models/collection_info.rb +76 -0
  69. data/lib/fontisan/models/collection_list_info.rb +37 -0
  70. data/lib/fontisan/models/font_export.rb +158 -0
  71. data/lib/fontisan/models/font_summary.rb +48 -0
  72. data/lib/fontisan/models/glyph_outline.rb +343 -0
  73. data/lib/fontisan/models/hint.rb +405 -0
  74. data/lib/fontisan/models/outline.rb +664 -0
  75. data/lib/fontisan/models/table_sharing_info.rb +40 -0
  76. data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
  77. data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
  78. data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
  79. data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
  80. data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
  81. data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
  82. data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
  83. data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
  84. data/lib/fontisan/models/ttx/ttfont.rb +49 -0
  85. data/lib/fontisan/models/validation_report.rb +203 -0
  86. data/lib/fontisan/open_type_collection.rb +156 -2
  87. data/lib/fontisan/open_type_font.rb +321 -19
  88. data/lib/fontisan/open_type_font_extensions.rb +54 -0
  89. data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
  90. data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
  91. data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
  92. data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
  93. data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
  94. data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
  95. data/lib/fontisan/outline_extractor.rb +423 -0
  96. data/lib/fontisan/pipeline/format_detector.rb +249 -0
  97. data/lib/fontisan/pipeline/output_writer.rb +154 -0
  98. data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
  99. data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
  100. data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
  101. data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
  102. data/lib/fontisan/pipeline/transformation_pipeline.rb +411 -0
  103. data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
  104. data/lib/fontisan/subset/builder.rb +268 -0
  105. data/lib/fontisan/subset/glyph_mapping.rb +215 -0
  106. data/lib/fontisan/subset/options.rb +142 -0
  107. data/lib/fontisan/subset/profile.rb +152 -0
  108. data/lib/fontisan/subset/table_subsetter.rb +461 -0
  109. data/lib/fontisan/svg/font_face_generator.rb +278 -0
  110. data/lib/fontisan/svg/font_generator.rb +264 -0
  111. data/lib/fontisan/svg/glyph_generator.rb +168 -0
  112. data/lib/fontisan/svg/view_box_calculator.rb +137 -0
  113. data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
  114. data/lib/fontisan/tables/cff/charset.rb +282 -0
  115. data/lib/fontisan/tables/cff/charstring.rb +934 -0
  116. data/lib/fontisan/tables/cff/charstring_builder.rb +356 -0
  117. data/lib/fontisan/tables/cff/charstring_parser.rb +237 -0
  118. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
  119. data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
  120. data/lib/fontisan/tables/cff/dict.rb +351 -0
  121. data/lib/fontisan/tables/cff/dict_builder.rb +257 -0
  122. data/lib/fontisan/tables/cff/encoding.rb +274 -0
  123. data/lib/fontisan/tables/cff/header.rb +102 -0
  124. data/lib/fontisan/tables/cff/hint_operation_injector.rb +207 -0
  125. data/lib/fontisan/tables/cff/index.rb +237 -0
  126. data/lib/fontisan/tables/cff/index_builder.rb +170 -0
  127. data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
  128. data/lib/fontisan/tables/cff/private_dict.rb +284 -0
  129. data/lib/fontisan/tables/cff/private_dict_writer.rb +125 -0
  130. data/lib/fontisan/tables/cff/table_builder.rb +221 -0
  131. data/lib/fontisan/tables/cff/top_dict.rb +236 -0
  132. data/lib/fontisan/tables/cff.rb +489 -0
  133. data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
  134. data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
  135. data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
  136. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +246 -0
  137. data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
  138. data/lib/fontisan/tables/cff2/table_builder.rb +574 -0
  139. data/lib/fontisan/tables/cff2/table_reader.rb +419 -0
  140. data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
  141. data/lib/fontisan/tables/cff2.rb +346 -0
  142. data/lib/fontisan/tables/cvar.rb +203 -0
  143. data/lib/fontisan/tables/fvar.rb +2 -2
  144. data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
  145. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
  146. data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
  147. data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
  148. data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
  149. data/lib/fontisan/tables/glyf.rb +235 -0
  150. data/lib/fontisan/tables/gvar.rb +231 -0
  151. data/lib/fontisan/tables/hhea.rb +124 -0
  152. data/lib/fontisan/tables/hmtx.rb +287 -0
  153. data/lib/fontisan/tables/hvar.rb +191 -0
  154. data/lib/fontisan/tables/loca.rb +322 -0
  155. data/lib/fontisan/tables/maxp.rb +192 -0
  156. data/lib/fontisan/tables/mvar.rb +185 -0
  157. data/lib/fontisan/tables/name.rb +99 -30
  158. data/lib/fontisan/tables/variation_common.rb +346 -0
  159. data/lib/fontisan/tables/vvar.rb +234 -0
  160. data/lib/fontisan/true_type_collection.rb +156 -2
  161. data/lib/fontisan/true_type_font.rb +321 -20
  162. data/lib/fontisan/true_type_font_extensions.rb +54 -0
  163. data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
  164. data/lib/fontisan/utilities/checksum_calculator.rb +60 -0
  165. data/lib/fontisan/utils/thread_pool.rb +134 -0
  166. data/lib/fontisan/validation/checksum_validator.rb +170 -0
  167. data/lib/fontisan/validation/consistency_validator.rb +197 -0
  168. data/lib/fontisan/validation/structure_validator.rb +198 -0
  169. data/lib/fontisan/validation/table_validator.rb +158 -0
  170. data/lib/fontisan/validation/validator.rb +152 -0
  171. data/lib/fontisan/validation/variable_font_validator.rb +218 -0
  172. data/lib/fontisan/variable/axis_normalizer.rb +215 -0
  173. data/lib/fontisan/variable/delta_applicator.rb +313 -0
  174. data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
  175. data/lib/fontisan/variable/instancer.rb +344 -0
  176. data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
  177. data/lib/fontisan/variable/region_matcher.rb +208 -0
  178. data/lib/fontisan/variable/static_font_builder.rb +213 -0
  179. data/lib/fontisan/variable/table_updater.rb +219 -0
  180. data/lib/fontisan/variation/blend_applier.rb +199 -0
  181. data/lib/fontisan/variation/cache.rb +298 -0
  182. data/lib/fontisan/variation/cache_key_builder.rb +162 -0
  183. data/lib/fontisan/variation/converter.rb +375 -0
  184. data/lib/fontisan/variation/data_extractor.rb +86 -0
  185. data/lib/fontisan/variation/delta_applier.rb +266 -0
  186. data/lib/fontisan/variation/delta_parser.rb +228 -0
  187. data/lib/fontisan/variation/inspector.rb +275 -0
  188. data/lib/fontisan/variation/instance_generator.rb +273 -0
  189. data/lib/fontisan/variation/instance_writer.rb +341 -0
  190. data/lib/fontisan/variation/interpolator.rb +231 -0
  191. data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
  192. data/lib/fontisan/variation/optimizer.rb +418 -0
  193. data/lib/fontisan/variation/parallel_generator.rb +150 -0
  194. data/lib/fontisan/variation/region_matcher.rb +221 -0
  195. data/lib/fontisan/variation/subsetter.rb +463 -0
  196. data/lib/fontisan/variation/table_accessor.rb +105 -0
  197. data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
  198. data/lib/fontisan/variation/validator.rb +345 -0
  199. data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
  200. data/lib/fontisan/variation/variation_context.rb +211 -0
  201. data/lib/fontisan/variation/variation_preserver.rb +288 -0
  202. data/lib/fontisan/version.rb +1 -1
  203. data/lib/fontisan/version.rb.orig +9 -0
  204. data/lib/fontisan/woff2/directory.rb +257 -0
  205. data/lib/fontisan/woff2/glyf_transformer.rb +666 -0
  206. data/lib/fontisan/woff2/header.rb +101 -0
  207. data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
  208. data/lib/fontisan/woff2/table_transformer.rb +163 -0
  209. data/lib/fontisan/woff2_font.rb +717 -0
  210. data/lib/fontisan/woff_font.rb +488 -0
  211. data/lib/fontisan.rb +132 -0
  212. data/scripts/compare_stack_aware.rb +187 -0
  213. data/scripts/measure_optimization.rb +141 -0
  214. metadata +234 -4
data/lib/fontisan/cli.rb CHANGED
@@ -25,12 +25,38 @@ module Fontisan
25
25
  desc: "Suppress non-error output",
26
26
  aliases: "-q"
27
27
 
28
- desc "info FONT_FILE", "Display font information"
28
+ desc "info PATH", "Display font information"
29
29
  # Extract and display comprehensive font metadata.
30
30
  #
31
- # @param font_file [String] Path to the font file
32
- def info(font_file)
33
- command = Commands::InfoCommand.new(font_file, options)
31
+ # @param path [String] Path to the font file or collection
32
+ def info(path)
33
+ command = Commands::InfoCommand.new(path, options)
34
+ info = command.run
35
+ output_result(info) unless options[:quiet]
36
+ rescue Errno::ENOENT => e
37
+ if options[:verbose]
38
+ raise
39
+ else
40
+ warn "File not found: #{path}" unless options[:quiet]
41
+ exit 1
42
+ end
43
+ end
44
+
45
+ desc "ls FILE", "List contents (fonts in collection or font summary)"
46
+ # List contents of font files with auto-detection.
47
+ #
48
+ # For collections (TTC/OTC): Lists all fonts in the collection
49
+ # For individual fonts (TTF/OTF): Shows quick font summary
50
+ #
51
+ # @param file [String] Path to the font or collection file
52
+ #
53
+ # @example List fonts in collection
54
+ # fontisan ls fonts.ttc
55
+ #
56
+ # @example Show font summary
57
+ # fontisan ls font.ttf
58
+ def ls(file)
59
+ command = Commands::LsCommand.new(file, options)
34
60
  result = command.run
35
61
  output_result(result)
36
62
  rescue Errno::ENOENT, Error => e
@@ -125,6 +151,183 @@ module Fontisan
125
151
  handle_error(e)
126
152
  end
127
153
 
154
+ desc "subset FONT_FILE", "Subset a font to specific glyphs"
155
+ option :text, type: :string,
156
+ desc: "Text to subset (e.g., 'Hello World')",
157
+ aliases: "-t"
158
+ option :glyphs, type: :string,
159
+ desc: "Comma-separated glyph IDs (e.g., '0,1,65,66,67')",
160
+ aliases: "-g"
161
+ option :unicode, type: :string,
162
+ desc: "Comma-separated Unicode codepoints (e.g., 'U+0041,U+0042' or '0x41,0x42')",
163
+ aliases: "-u"
164
+ option :output, type: :string, required: true,
165
+ desc: "Output file path",
166
+ aliases: "-o"
167
+ option :profile, type: :string, default: "pdf",
168
+ desc: "Subsetting profile (pdf, web, minimal)",
169
+ aliases: "-p"
170
+ option :retain_gids, type: :boolean, default: false,
171
+ desc: "Retain original glyph IDs (leave gaps)"
172
+ option :drop_hints, type: :boolean, default: false,
173
+ desc: "Drop hinting instructions"
174
+ option :drop_names, type: :boolean, default: false,
175
+ desc: "Drop glyph names from post table"
176
+ option :unicode_ranges, type: :boolean, default: true,
177
+ desc: "Prune OS/2 Unicode ranges"
178
+ # Subset a font to specific glyphs.
179
+ #
180
+ # You must specify one of --text, --glyphs, or --unicode to define
181
+ # which glyphs to include in the subset.
182
+ #
183
+ # @param font_file [String] Path to the font file
184
+ def subset(font_file)
185
+ command = Commands::SubsetCommand.new(font_file, options)
186
+ result = command.run
187
+
188
+ unless options[:quiet]
189
+ puts "Subset font created:"
190
+ puts " Input: #{result[:input]}"
191
+ puts " Output: #{result[:output]}"
192
+ puts " Original glyphs: #{result[:original_glyphs]}"
193
+ puts " Subset glyphs: #{result[:subset_glyphs]}"
194
+ puts " Profile: #{result[:profile]}"
195
+ puts " Size: #{format_size(result[:size])}"
196
+ end
197
+ rescue Errno::ENOENT, Error => e
198
+ handle_error(e)
199
+ end
200
+
201
+ desc "convert FONT_FILE", "Convert font to different format"
202
+ option :to, type: :string, required: true,
203
+ desc: "Target format (ttf, otf, woff, woff2)",
204
+ aliases: "-t"
205
+ option :output, type: :string, required: true,
206
+ desc: "Output file path",
207
+ aliases: "-o"
208
+ option :coordinates, type: :string,
209
+ desc: "Instance coordinates (e.g., wght=700,wdth=100)",
210
+ aliases: "-c"
211
+ option :instance_index, type: :numeric,
212
+ desc: "Named instance index",
213
+ aliases: "-n"
214
+ option :preserve_variation, type: :boolean,
215
+ desc: "Force variation preservation (auto-detected by default)"
216
+ option :no_validate, type: :boolean, default: false,
217
+ desc: "Skip output validation"
218
+ option :preserve_hints, type: :boolean, default: false,
219
+ desc: "Preserve rendering hints during conversion (TTF→OTF preservations may be limited)"
220
+ option :wght, type: :numeric,
221
+ desc: "Weight axis value (alternative to --coordinates)"
222
+ option :wdth, type: :numeric,
223
+ desc: "Width axis value (alternative to --coordinates)"
224
+ option :slnt, type: :numeric,
225
+ desc: "Slant axis value (alternative to --coordinates)"
226
+ option :ital, type: :numeric,
227
+ desc: "Italic axis value (alternative to --coordinates)"
228
+ option :opsz, type: :numeric,
229
+ desc: "Optical size axis value (alternative to --coordinates)"
230
+ # Convert a font to a different format using the universal transformation pipeline.
231
+ #
232
+ # Supported conversions:
233
+ # - TTF ↔ OTF: Outline format conversion
234
+ # - WOFF/WOFF2: Web font packaging
235
+ # - Variable fonts: Automatic variation preservation or instance generation
236
+ #
237
+ # Variable Font Operations:
238
+ # The pipeline automatically detects whether variation data can be preserved based on
239
+ # source and target formats. For same outline family (TTF→WOFF or OTF→WOFF2), variation
240
+ # is preserved automatically. For cross-family conversions (TTF↔OTF), an instance is
241
+ # generated unless --preserve-variation is explicitly set.
242
+ #
243
+ # Instance Generation:
244
+ # Use --coordinates to specify exact axis values (e.g., wght=700,wdth=100) or
245
+ # --instance-index to use a named instance. Individual axis options (--wght, --wdth)
246
+ # are also supported for convenience.
247
+ #
248
+ # @param font_file [String] Path to the font file
249
+ #
250
+ # @example Convert TTF to OTF
251
+ # fontisan convert font.ttf --to otf --output font.otf
252
+ #
253
+ # @example Generate bold instance at specific coordinates
254
+ # fontisan convert variable.ttf --to ttf --output bold.ttf --coordinates "wght=700,wdth=100"
255
+ #
256
+ # @example Generate bold instance using individual axis options
257
+ # fontisan convert variable.ttf --to ttf --output bold.ttf --wght 700
258
+ #
259
+ # @example Use named instance
260
+ # fontisan convert variable.ttf --to woff2 --output bold.woff2 --instance-index 0
261
+ #
262
+ # @example Force variation preservation (if compatible)
263
+ # fontisan convert variable.ttf --to woff2 --output variable.woff2 --preserve-variation
264
+ #
265
+ # @example Convert without validation
266
+ # fontisan convert font.ttf --to otf --output font.otf --no-validate
267
+ def convert(font_file)
268
+ # Build instance coordinates from axis options
269
+ instance_coords = build_instance_coordinates(options)
270
+
271
+ # Merge coordinates into options
272
+ convert_options = options.to_h.dup
273
+ convert_options[:instance_coordinates] = instance_coords if instance_coords.any?
274
+
275
+ command = Commands::ConvertCommand.new(font_file, convert_options)
276
+ command.run
277
+ rescue Errno::ENOENT, Error => e
278
+ handle_error(e)
279
+ end
280
+
281
+ desc "instance FONT_FILE",
282
+ "Generate static font instance from variable font"
283
+ option :output, type: :string,
284
+ desc: "Output file path",
285
+ aliases: "-o"
286
+ option :wght, type: :numeric,
287
+ desc: "Weight axis value"
288
+ option :wdth, type: :numeric,
289
+ desc: "Width axis value"
290
+ option :slnt, type: :numeric,
291
+ desc: "Slant axis value"
292
+ option :ital, type: :numeric,
293
+ desc: "Italic axis value"
294
+ option :opsz, type: :numeric,
295
+ desc: "Optical size axis value"
296
+ option :named_instance, type: :string,
297
+ desc: "Use named instance (e.g., 'Bold', 'Light')",
298
+ aliases: "-n"
299
+ option :list_instances, type: :boolean, default: false,
300
+ desc: "List available named instances",
301
+ aliases: "-l"
302
+ option :to, type: :string,
303
+ desc: "Convert to format (ttf, otf, woff, woff2, svg)",
304
+ aliases: "-t"
305
+ # Generate static font instance from variable font.
306
+ #
307
+ # You can specify axis coordinates using --wght, --wdth, etc., or use
308
+ # a predefined named instance with --named-instance. Use --list-instances
309
+ # to see available named instances.
310
+ #
311
+ # @param font_file [String] Path to the variable font file
312
+ #
313
+ # @example Generate bold instance
314
+ # fontisan instance variable.ttf --wght=700 --output=bold.ttf
315
+ #
316
+ # @example Use named instance
317
+ # fontisan instance variable.ttf --named-instance="Bold" --output=bold.ttf
318
+ #
319
+ # @example Instance with format conversion
320
+ # fontisan instance variable.ttf --wght=700 --to=woff2 --output=bold.woff2
321
+ #
322
+ # @example List available instances
323
+ # fontisan instance variable.ttf --list-instances
324
+ def instance(font_file)
325
+ command = Commands::InstanceCommand.new
326
+ command.execute(font_file, options)
327
+ rescue Errno::ENOENT, Error => e
328
+ handle_error(e)
329
+ end
330
+
128
331
  desc "dump-table FONT_FILE TABLE_TAG", "Dump raw table data to stdout"
129
332
  # Dump raw binary table data to stdout.
130
333
  #
@@ -141,14 +344,157 @@ module Fontisan
141
344
  handle_error(e)
142
345
  end
143
346
 
347
+ desc "validate FONT_FILE", "Validate font file structure and checksums"
348
+ option :verbose, type: :boolean, default: false,
349
+ desc: "Show detailed validation information"
350
+ def validate(font_file)
351
+ command = Commands::ValidateCommand.new(font_file, verbose: options[:verbose])
352
+ exit command.run
353
+ end
354
+
355
+ desc "export FONT_FILE", "Export font to TTX/YAML/JSON format"
356
+ option :output, type: :string,
357
+ desc: "Output file path (default: stdout)",
358
+ aliases: "-o"
359
+ option :format, type: :string, default: "yaml",
360
+ desc: "Export format (yaml, json, ttx)",
361
+ aliases: "-f"
362
+ option :tables, type: :array,
363
+ desc: "Specific tables to export",
364
+ aliases: "-t"
365
+ option :binary_format, type: :string, default: "hex",
366
+ desc: "Binary encoding (hex, base64)",
367
+ aliases: "-b"
368
+
369
+ def export(font_file)
370
+ command = Commands::ExportCommand.new(
371
+ font_file,
372
+ output: options[:output],
373
+ format: options[:format].to_sym,
374
+ tables: options[:tables],
375
+ binary_format: options[:binary_format].to_sym,
376
+ )
377
+ exit command.run
378
+ end
379
+
144
380
  desc "version", "Display version information"
145
381
  # Display the Fontisan version.
146
382
  def version
147
383
  puts "Fontisan version #{Fontisan::VERSION}"
148
384
  end
149
385
 
386
+ desc "unpack FONT_FILE", "Unpack fonts from TTC/OTC collection"
387
+ option :output_dir, type: :string, required: true,
388
+ desc: "Output directory for extracted fonts",
389
+ aliases: "-d"
390
+ option :font_index, type: :numeric,
391
+ desc: "Extract specific font by index (default: extract all)",
392
+ aliases: "-i"
393
+ option :format, type: :string,
394
+ desc: "Output format (ttf, otf, woff, woff2)",
395
+ aliases: "-f"
396
+ option :prefix, type: :string,
397
+ desc: "Filename prefix for extracted fonts",
398
+ aliases: "-p"
399
+ # Extract individual fonts from a TTC (TrueType Collection) or OTC (OpenType Collection) file.
400
+ #
401
+ # This command unpacks fonts from collection files, optionally converting them
402
+ # to different formats during extraction.
403
+ #
404
+ # @param font_file [String] Path to the TTC/OTC collection file
405
+ #
406
+ # @example Extract all fonts to directory
407
+ # fontisan unpack family.ttc --output-dir extracted/
408
+ #
409
+ # @example Extract specific font by index
410
+ # fontisan unpack family.ttc --output-dir extracted/ --font-index 0
411
+ #
412
+ # @example Extract with format conversion
413
+ # fontisan unpack family.ttc --output-dir extracted/ --format woff2
414
+ #
415
+ # @example Extract with custom prefix
416
+ # fontisan unpack family.ttc --output-dir extracted/ --prefix "NotoSans"
417
+ def unpack(font_file)
418
+ command = Commands::UnpackCommand.new(font_file, options)
419
+ result = command.run
420
+
421
+ unless options[:quiet]
422
+ puts "Collection unpacked successfully:"
423
+ puts " Input: #{result[:collection]}"
424
+ puts " Output directory: #{result[:output_dir]}"
425
+ puts " Fonts extracted: #{result[:fonts_extracted]}/#{result[:num_fonts]}"
426
+ result[:extracted_files].each do |file|
427
+ size = File.size(file)
428
+ puts " - #{File.basename(file)} (#{format_size(size)})"
429
+ end
430
+ end
431
+ rescue Errno::ENOENT, Error => e
432
+ handle_error(e)
433
+ end
434
+
435
+ desc "pack FONT_FILES...", "Pack multiple fonts into TTC/OTC collection"
436
+ option :output, type: :string, required: true,
437
+ desc: "Output collection file path",
438
+ aliases: "-o"
439
+ option :format, type: :string, default: "ttc",
440
+ desc: "Collection format (ttc, otc)",
441
+ aliases: "-f"
442
+ option :optimize, type: :boolean, default: true,
443
+ desc: "Enable table sharing optimization",
444
+ aliases: "--optimize"
445
+ option :analyze, type: :boolean, default: false,
446
+ desc: "Show analysis report before building",
447
+ aliases: "--analyze"
448
+ # Create a TTC (TrueType Collection) or OTC (OpenType Collection) from multiple font files.
449
+ #
450
+ # This command combines multiple fonts into a single collection file with
451
+ # shared table deduplication to save space. It supports both TTC and OTC formats.
452
+ #
453
+ # @param font_files [Array<String>] Paths to input font files (minimum 2 required)
454
+ #
455
+ # @example Pack fonts into TTC
456
+ # fontisan pack font1.ttf font2.ttf font3.ttf --output family.ttc
457
+ #
458
+ # @example Pack into OTC with analysis
459
+ # fontisan pack Regular.otf Bold.otf Italic.otf --output family.otc --analyze
460
+ #
461
+ # @example Pack without optimization
462
+ # fontisan pack font1.ttf font2.ttf --output collection.ttc --no-optimize
463
+ def pack(*font_files)
464
+ command = Commands::PackCommand.new(font_files, options)
465
+ result = command.run
466
+
467
+ unless options[:quiet]
468
+ puts "Collection created successfully:"
469
+ puts " Output: #{result[:output]}"
470
+ puts " Format: #{result[:format].upcase}"
471
+ puts " Fonts: #{result[:num_fonts]}"
472
+ puts " Size: #{format_size(result[:output_size])}"
473
+ if result[:space_savings].positive?
474
+ puts " Space saved: #{format_size(result[:space_savings])}"
475
+ puts " Sharing: #{result[:sharing_percentage]}%"
476
+ end
477
+ end
478
+ rescue Errno::ENOENT, Error => e
479
+ handle_error(e)
480
+ end
481
+
150
482
  private
151
483
 
484
+ # Build instance coordinates from CLI axis options
485
+ #
486
+ # @param options [Hash] CLI options
487
+ # @return [Hash] Coordinates hash
488
+ def build_instance_coordinates(options)
489
+ coords = {}
490
+ coords["wght"] = options[:wght].to_f if options[:wght]
491
+ coords["wdth"] = options[:wdth].to_f if options[:wdth]
492
+ coords["slnt"] = options[:slnt].to_f if options[:slnt]
493
+ coords["ital"] = options[:ital].to_f if options[:ital]
494
+ coords["opsz"] = options[:opsz].to_f if options[:opsz]
495
+ coords
496
+ end
497
+
152
498
  # Output the result in the requested format.
153
499
  #
154
500
  # @param result [Object] The result object to output
@@ -174,6 +520,20 @@ module Fontisan
174
520
  formatter.format(result)
175
521
  end
176
522
 
523
+ # Format file size in human-readable form
524
+ #
525
+ # @param size [Integer] Size in bytes
526
+ # @return [String] Formatted size
527
+ def format_size(size)
528
+ if size < 1024
529
+ "#{size} bytes"
530
+ elsif size < 1024 * 1024
531
+ "#{(size / 1024.0).round(2)} KB"
532
+ else
533
+ "#{(size / (1024.0 * 1024)).round(2)} MB"
534
+ end
535
+ end
536
+
177
537
  # Handle errors based on verbosity settings.
178
538
  #
179
539
  # @param error [Error, Errno::ENOENT] The error to handle