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
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_command"
4
+ require_relative "../export/exporter"
5
+ require_relative "../font_loader"
6
+
7
+ module Fontisan
8
+ module Commands
9
+ # ExportCommand provides CLI interface for font export to YAML/JSON
10
+ #
11
+ # This command exports fonts to TTX-like YAML/JSON formats for debugging
12
+ # and font analysis. Supports selective table export and both formats.
13
+ #
14
+ # @example Exporting entire font
15
+ # command = ExportCommand.new(
16
+ # input: "font.ttf",
17
+ # output: "font.yaml"
18
+ # )
19
+ # command.run
20
+ #
21
+ # @example Exporting specific tables
22
+ # command = ExportCommand.new(
23
+ # input: "font.ttf",
24
+ # output: "meta.yaml",
25
+ # tables: ["head", "name", "cmap"]
26
+ # )
27
+ # command.run
28
+ class ExportCommand < BaseCommand
29
+ # Initialize export command
30
+ #
31
+ # @param input [String] Path to input font file
32
+ # @param output [String, nil] Path to output file (default: stdout)
33
+ # @param format [Symbol] Output format (:yaml or :json)
34
+ # @param tables [Array<String>, nil] Specific tables to export
35
+ # @param binary_format [Symbol] Binary encoding (:hex or :base64)
36
+ # @param pretty [Boolean] Pretty-print output
37
+ def initialize(input:, output: nil, format: :yaml, tables: nil,
38
+ binary_format: :hex, pretty: true)
39
+ super()
40
+ @input = input
41
+ @output = output
42
+ @format = format.to_sym
43
+ @tables = tables
44
+ @binary_format = binary_format.to_sym
45
+ @pretty = pretty
46
+ end
47
+
48
+ # Run the export command
49
+ #
50
+ # @return [Integer] Exit code (0 = success, 1 = error)
51
+ def run
52
+ validate_params!
53
+
54
+ # Load font
55
+ font = load_font
56
+ return 1 unless font
57
+
58
+ # Create exporter
59
+ exporter = Export::Exporter.new(
60
+ font,
61
+ @input,
62
+ binary_format: @binary_format,
63
+ )
64
+
65
+ # Export to model
66
+ export_model = exporter.export(
67
+ tables: @tables || :all,
68
+ format: @format,
69
+ )
70
+
71
+ # Output result
72
+ output_export(export_model)
73
+
74
+ 0
75
+ rescue StandardError => e
76
+ puts "Error: #{e.message}"
77
+ puts e.backtrace.join("\n") if ENV["DEBUG"]
78
+ 1
79
+ end
80
+
81
+ private
82
+
83
+ # Validate command parameters
84
+ #
85
+ # @raise [ArgumentError] if parameters are invalid
86
+ # @return [void]
87
+ def validate_params!
88
+ if @input.nil? || @input.empty?
89
+ raise ArgumentError,
90
+ "Input file is required"
91
+ end
92
+ unless File.exist?(@input)
93
+ raise ArgumentError,
94
+ "Input file does not exist: #{@input}"
95
+ end
96
+
97
+ valid_formats = %i[yaml json ttx]
98
+ unless valid_formats.include?(@format)
99
+ raise ArgumentError,
100
+ "Invalid format: #{@format}. Must be one of: #{valid_formats.join(', ')}"
101
+ end
102
+
103
+ valid_binary_formats = %i[hex base64]
104
+ unless valid_binary_formats.include?(@binary_format)
105
+ raise ArgumentError,
106
+ "Invalid binary format: #{@binary_format}. " \
107
+ "Must be one of: #{valid_binary_formats.join(', ')}"
108
+ end
109
+
110
+ # Validate output directory exists
111
+ if @output
112
+ output_dir = File.dirname(@output)
113
+ unless Dir.exist?(output_dir)
114
+ raise ArgumentError,
115
+ "Output directory does not exist: #{output_dir}"
116
+ end
117
+ end
118
+ end
119
+
120
+ # Load the font file
121
+ #
122
+ # @return [TrueTypeFont, OpenTypeFont, nil] The loaded font or nil on error
123
+ def load_font
124
+ FontLoader.load(@input)
125
+ rescue StandardError => e
126
+ puts "Error loading font: #{e.message}"
127
+ nil
128
+ end
129
+
130
+ # Output the export
131
+ #
132
+ # @param export_model [Models::FontExport, String] The export model or TTX XML
133
+ # @return [void]
134
+ def output_export(export_model)
135
+ content = if export_model.is_a?(String)
136
+ # TTX XML string
137
+ export_model
138
+ else
139
+ # FontExport model
140
+ case @format
141
+ when :yaml
142
+ export_model.to_yaml
143
+ when :json
144
+ if @pretty
145
+ JSON.pretty_generate(JSON.parse(export_model.to_json))
146
+ else
147
+ export_model.to_json
148
+ end
149
+ end
150
+ end
151
+
152
+ if @output
153
+ File.write(@output, content)
154
+ puts "Exported to #{@output}"
155
+ else
156
+ puts content
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -2,9 +2,14 @@
2
2
 
3
3
  module Fontisan
4
4
  module Commands
5
- # Command to extract font metadata information.
5
+ # Command to extract font or collection metadata information.
6
6
  #
7
- # This command extracts comprehensive font information from various tables:
7
+ # This command auto-detects whether the input is a collection (TTC/OTC)
8
+ # or individual font (TTF/OTF) and returns the appropriate model:
9
+ # - CollectionInfo for TTC/OTC files
10
+ # - FontInfo for TTF/OTF files
11
+ #
12
+ # For individual fonts, extracts comprehensive information from various tables:
8
13
  # - name table: family names, version, copyright, etc.
9
14
  # - OS/2 table: vendor ID, embedding permissions
10
15
  # - head table: font revision, units per em
@@ -13,11 +18,42 @@ module Fontisan
13
18
  # command = InfoCommand.new("path/to/font.ttf")
14
19
  # info = command.run
15
20
  # puts info.family_name
21
+ #
22
+ # @example Extract collection information
23
+ # command = InfoCommand.new("path/to/fonts.ttc")
24
+ # info = command.run
25
+ # puts "Collection has #{info.num_fonts} fonts"
16
26
  class InfoCommand < BaseCommand
17
- # Extract font information from all available tables.
27
+ # Extract information from font or collection.
28
+ #
29
+ # Auto-detects file type and returns appropriate model.
18
30
  #
19
- # @return [Models::FontInfo] Font metadata information
31
+ # @return [Models::FontInfo, Models::CollectionInfo] Metadata information
20
32
  def run
33
+ if FontLoader.collection?(@font_path)
34
+ collection_info
35
+ else
36
+ font_info
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ # Get collection information
43
+ #
44
+ # @return [Models::CollectionInfo] Collection metadata
45
+ def collection_info
46
+ collection = FontLoader.load_collection(@font_path)
47
+
48
+ File.open(@font_path, "rb") do |io|
49
+ collection.collection_info(io, @font_path)
50
+ end
51
+ end
52
+
53
+ # Get individual font information
54
+ #
55
+ # @return [Models::FontInfo] Font metadata
56
+ def font_info
21
57
  info = Models::FontInfo.new
22
58
  populate_font_format(info)
23
59
  populate_from_name_table(info) if font.has_table?(Constants::NAME_TAG)
@@ -26,8 +62,6 @@ module Fontisan
26
62
  info
27
63
  end
28
64
 
29
- private
30
-
31
65
  # Populate font format and variable status based on font class and table presence.
32
66
  #
33
67
  # @param info [Models::FontInfo] FontInfo instance to populate
@@ -0,0 +1,286 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require_relative "base_command"
5
+ require_relative "../variation/instance_generator"
6
+ require_relative "../variation/instance_writer"
7
+ require_relative "../variation/validator"
8
+ require_relative "../error"
9
+
10
+ module Fontisan
11
+ module Commands
12
+ # CLI command for generating static font instances from variable fonts
13
+ #
14
+ # Provides command-line interface for:
15
+ # - Instancing at specific coordinates
16
+ # - Using named instances
17
+ # - Converting output format during instancing
18
+ # - Listing available instances
19
+ # - Validation before generation
20
+ # - Dry-run mode for previewing
21
+ # - Progress tracking
22
+ #
23
+ # @example Instance at coordinates
24
+ # fontisan instance variable.ttf --wght=700 --output=bold.ttf
25
+ #
26
+ # @example Instance with format conversion
27
+ # fontisan instance variable.ttf --wght=700 --to=otf --output=bold.otf
28
+ #
29
+ # @example Instance with validation
30
+ # fontisan instance variable.ttf --wght=700 --validate --output=bold.ttf
31
+ #
32
+ # @example Dry-run to preview
33
+ # fontisan instance variable.ttf --wght=700 --dry-run
34
+ class InstanceCommand < BaseCommand
35
+ # Instance a variable font at specified coordinates
36
+ #
37
+ # @param input_path [String] Path to variable font file
38
+ def execute(input_path, options = {})
39
+ # Load variable font
40
+ font = load_font(input_path)
41
+
42
+ # Validate font if requested
43
+ validate_font(font) if options[:validate]
44
+
45
+ # Handle list-instances option
46
+ if options[:list_instances]
47
+ list_instances(font)
48
+ return
49
+ end
50
+
51
+ # Handle dry-run mode
52
+ if options[:dry_run]
53
+ preview_instance(font, input_path, options)
54
+ return
55
+ end
56
+
57
+ # Determine output path
58
+ output_path = determine_output_path(input_path, options)
59
+
60
+ # Generate instance
61
+ if options[:named_instance]
62
+ instance_named(font, options[:named_instance], output_path, options)
63
+ else
64
+ instance_coords(font, extract_coordinates(options), output_path,
65
+ options)
66
+ end
67
+
68
+ puts "Static font instance written to: #{output_path}"
69
+ rescue VariationError => e
70
+ $stderr.puts "Variation Error: #{e.detailed_message}"
71
+ exit 1
72
+ rescue StandardError => e
73
+ $stderr.puts "Error: #{e.message}"
74
+ $stderr.puts e.backtrace.first(5).join("\n") if options[:verbose]
75
+ exit 1
76
+ end
77
+
78
+ private
79
+
80
+ # Validate font before generating instance
81
+ #
82
+ # @param font [Object] Font object
83
+ def validate_font(font)
84
+ puts "Validating font..." if @options[:verbose]
85
+
86
+ validator = Variation::Validator.new(font)
87
+ errors = validator.validate
88
+
89
+ if errors.any?
90
+ $stderr.puts "Validation errors found:"
91
+ errors.each do |error|
92
+ $stderr.puts " - #{error}"
93
+ end
94
+ exit 1
95
+ end
96
+
97
+ puts "Font validation passed" if @options[:verbose]
98
+ end
99
+
100
+ # Preview instance without generating
101
+ #
102
+ # @param font [Object] Font object
103
+ # @param input_path [String] Input file path
104
+ # @param options [Hash] Command options
105
+ def preview_instance(font, input_path, options)
106
+ coords = extract_coordinates(options)
107
+
108
+ if coords.empty?
109
+ raise ArgumentError,
110
+ "No coordinates specified. Use --wght=700, --wdth=100, etc."
111
+ end
112
+
113
+ puts "Dry-run mode: Preview of instance generation"
114
+ puts
115
+ puts "Coordinates:"
116
+ coords.each do |axis, value|
117
+ puts " #{axis}: #{value}"
118
+ end
119
+ puts
120
+ puts "Output would be written to: #{determine_output_path(input_path, options)}"
121
+ puts "Output format: #{options[:to] || 'same as input'}"
122
+ puts
123
+ puts "Use without --dry-run to actually generate the instance."
124
+ end
125
+
126
+ # Instance at specific coordinates
127
+ #
128
+ # @param font [Object] Font object
129
+ # @param coords [Hash] User coordinates
130
+ # @param output_path [String] Output file path
131
+ # @param options [Hash] Command options
132
+ def instance_coords(font, coords, output_path, options)
133
+ if coords.empty?
134
+ raise ArgumentError,
135
+ "No coordinates specified. Use --wght=700, --wdth=100, etc."
136
+ end
137
+
138
+ # Show progress if requested
139
+ print "Generating instance..." if options[:progress]
140
+
141
+ # Generate instance tables using InstanceGenerator
142
+ generator = Variation::InstanceGenerator.new(font, coords)
143
+ tables = generator.generate
144
+
145
+ puts " done" if options[:progress]
146
+
147
+ # Write instance using InstanceWriter
148
+ print "Writing output..." if options[:progress]
149
+
150
+ # Detect source format for conversion
151
+ source_format = detect_source_format(font)
152
+
153
+ Variation::InstanceWriter.write(
154
+ tables,
155
+ output_path,
156
+ format: options[:to]&.to_sym,
157
+ source_format: source_format,
158
+ optimize: options[:optimize] || false,
159
+ )
160
+
161
+ puts " done" if options[:progress]
162
+ end
163
+
164
+ # Instance using named instance
165
+ #
166
+ # @param font [Object] Font object
167
+ # @param instance_index [Integer] Named instance index
168
+ # @param output_path [String] Output file path
169
+ # @param options [Hash] Command options
170
+ def instance_named(font, instance_index, output_path, options)
171
+ # Generate instance using named instance
172
+ generator = Variation::InstanceGenerator.new(font)
173
+ tables = generator.generate_named_instance(instance_index)
174
+
175
+ # Detect source format
176
+ source_format = detect_source_format(font)
177
+
178
+ # Write instance
179
+ Variation::InstanceWriter.write(
180
+ tables,
181
+ output_path,
182
+ format: options[:to]&.to_sym,
183
+ source_format: source_format,
184
+ optimize: options[:optimize] || false,
185
+ )
186
+ end
187
+
188
+ # List available named instances
189
+ #
190
+ # @param font [Object] Font object
191
+ def list_instances(font)
192
+ fvar = font.table("fvar")
193
+ unless fvar
194
+ puts "Not a variable font - no named instances available."
195
+ return
196
+ end
197
+
198
+ instances = fvar.instances
199
+ if instances.empty?
200
+ puts "No named instances defined in font."
201
+ return
202
+ end
203
+
204
+ puts "Available named instances:"
205
+ puts
206
+
207
+ instances.each_with_index do |instance, index|
208
+ name_id = instance[:subfamily_name_id]
209
+ puts " [#{index}] Instance #{name_id}"
210
+ puts " Coordinates:"
211
+ instance[:coordinates].each_with_index do |value, axis_index|
212
+ next if axis_index >= fvar.axes.length
213
+
214
+ axis = fvar.axes[axis_index]
215
+ puts " #{axis.axis_tag}: #{value}"
216
+ end
217
+ puts
218
+ end
219
+ end
220
+
221
+ # Extract axis coordinates from options
222
+ #
223
+ # @param options [Hash] Command options
224
+ # @return [Hash] Coordinates hash
225
+ def extract_coordinates(options)
226
+ coords = {}
227
+
228
+ # Check for common axis options
229
+ coords["wght"] = options[:wght].to_f if options[:wght]
230
+ coords["wdth"] = options[:wdth].to_f if options[:wdth]
231
+ coords["slnt"] = options[:slnt].to_f if options[:slnt]
232
+ coords["ital"] = options[:ital].to_f if options[:ital]
233
+ coords["opsz"] = options[:opsz].to_f if options[:opsz]
234
+
235
+ # Allow arbitrary axis coordinates via --axis-TAG=value
236
+ options.each do |key, value|
237
+ key_str = key.to_s
238
+ if key_str.start_with?("axis_")
239
+ axis_tag = key_str.sub("axis_", "")
240
+ coords[axis_tag] = value.to_f
241
+ end
242
+ end
243
+
244
+ coords
245
+ end
246
+
247
+ # Determine output path
248
+ #
249
+ # @param input_path [String] Input file path
250
+ # @param options [Hash] Command options
251
+ # @return [String] Output path
252
+ def determine_output_path(input_path, options)
253
+ return options[:output] if options[:output]
254
+
255
+ # Generate default output name
256
+ base = File.basename(input_path, ".*")
257
+ ext = options[:to] || File.extname(input_path)[1..]
258
+ dir = File.dirname(input_path)
259
+
260
+ "#{dir}/#{base}-instance.#{ext}"
261
+ end
262
+
263
+ # Detect source format from font
264
+ #
265
+ # @param font [Object] Font object
266
+ # @return [Symbol] Source format (:ttf or :otf)
267
+ def detect_source_format(font)
268
+ font.has_table?("CFF ") || font.has_table?("CFF2") ? :otf : :ttf
269
+ end
270
+
271
+ # Load font from file
272
+ #
273
+ # @param path [String] Font file path
274
+ # @return [Object] Font object
275
+ def load_font(path)
276
+ unless File.exist?(path)
277
+ raise ArgumentError, "Font file not found: #{path}"
278
+ end
279
+
280
+ FontLoader.load(path)
281
+ rescue StandardError => e
282
+ raise ArgumentError, "Failed to load font: #{e.message}"
283
+ end
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../font_loader"
4
+ require_relative "../models/collection_list_info"
5
+ require_relative "../models/font_summary"
6
+ require_relative "../tables/name"
7
+ require_relative "../error"
8
+
9
+ module Fontisan
10
+ module Commands
11
+ # Command to list contents of font files (collections or individual fonts).
12
+ #
13
+ # This command provides a universal "ls" interface that auto-detects
14
+ # whether the input is a collection (TTC/OTC) or individual font (TTF/OTF)
15
+ # and returns the appropriate listing:
16
+ # - For collections: Lists all fonts in the collection
17
+ # - For individual fonts: Shows a quick summary
18
+ #
19
+ # @example List fonts in collection
20
+ # command = LsCommand.new("fonts.ttc")
21
+ # list = command.run
22
+ # puts "Contains #{list.num_fonts} fonts"
23
+ #
24
+ # @example Get font summary
25
+ # command = LsCommand.new("font.ttf")
26
+ # summary = command.run
27
+ # puts "#{summary.family_name} - #{summary.num_glyphs} glyphs"
28
+ class LsCommand
29
+ # Initialize ls command
30
+ #
31
+ # @param file_path [String] Path to font or collection file
32
+ # @param options [Hash] Command options
33
+ # @option options [Integer] :font_index Index for TTC/OTC (unused for ls)
34
+ def initialize(file_path, options = {})
35
+ @file_path = file_path
36
+ @options = options
37
+ end
38
+
39
+ # Execute the ls command
40
+ #
41
+ # Auto-detects file type and returns appropriate model:
42
+ # - CollectionListInfo for TTC/OTC files
43
+ # - FontSummary for TTF/OTF files
44
+ #
45
+ # @return [CollectionListInfo, FontSummary] List or summary
46
+ # @raise [Errno::ENOENT] if file does not exist
47
+ # @raise [Error] for loading or processing failures
48
+ def run
49
+ if FontLoader.collection?(@file_path)
50
+ list_collection
51
+ else
52
+ font_summary
53
+ end
54
+ rescue Errno::ENOENT
55
+ raise
56
+ rescue StandardError => e
57
+ raise Error, "Failed to list file contents: #{e.message}"
58
+ end
59
+
60
+ private
61
+
62
+ # List fonts in a collection
63
+ #
64
+ # @return [CollectionListInfo] List of fonts with metadata
65
+ def list_collection
66
+ collection = FontLoader.load_collection(@file_path)
67
+
68
+ File.open(@file_path, "rb") do |io|
69
+ list = collection.list_fonts(io)
70
+ list.collection_path = @file_path
71
+ list
72
+ end
73
+ end
74
+
75
+ # Create summary for individual font
76
+ #
77
+ # @return [FontSummary] Quick font summary
78
+ def font_summary
79
+ font = FontLoader.load(@file_path)
80
+
81
+ # Extract basic info
82
+ name_table = font.table("name")
83
+ post_table = font.table("post")
84
+
85
+ family_name = name_table&.english_name(Tables::Name::FAMILY) || "Unknown"
86
+ subfamily_name = name_table&.english_name(Tables::Name::SUBFAMILY) || "Regular"
87
+
88
+ # Determine font format
89
+ sfnt = font.header.sfnt_version
90
+ font_format = case sfnt
91
+ when 0x00010000, 0x74727565 # 0x74727565 = 'true'
92
+ "TrueType"
93
+ when 0x4F54544F # 'OTTO'
94
+ "OpenType"
95
+ else
96
+ "Unknown"
97
+ end
98
+
99
+ num_glyphs = post_table&.glyph_names&.length || 0
100
+ num_tables = font.table_names.length
101
+
102
+ Models::FontSummary.new(
103
+ font_path: @file_path,
104
+ family_name: family_name,
105
+ subfamily_name: subfamily_name,
106
+ font_format: font_format,
107
+ num_glyphs: num_glyphs,
108
+ num_tables: num_tables,
109
+ )
110
+ end
111
+ end
112
+ end
113
+ end