fontisan 0.1.0 → 0.2.0

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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +529 -65
  3. data/Gemfile +1 -0
  4. data/LICENSE +5 -1
  5. data/README.adoc +1301 -275
  6. data/Rakefile +27 -2
  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 +309 -0
  12. data/lib/fontisan/collection/builder.rb +260 -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 +241 -0
  16. data/lib/fontisan/collection/writer.rb +306 -0
  17. data/lib/fontisan/commands/base_command.rb +8 -1
  18. data/lib/fontisan/commands/convert_command.rb +291 -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 +295 -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 +178 -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 +69 -0
  37. data/lib/fontisan/converters/conversion_strategy.rb +96 -0
  38. data/lib/fontisan/converters/format_converter.rb +259 -0
  39. data/lib/fontisan/converters/outline_converter.rb +936 -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 +121 -12
  57. data/lib/fontisan/font_writer.rb +301 -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 +177 -0
  61. data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
  62. data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
  63. data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
  64. data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
  65. data/lib/fontisan/loading_modes.rb +113 -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 +233 -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 +296 -10
  88. data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
  89. data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
  90. data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
  91. data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
  92. data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
  93. data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
  94. data/lib/fontisan/outline_extractor.rb +423 -0
  95. data/lib/fontisan/subset/builder.rb +268 -0
  96. data/lib/fontisan/subset/glyph_mapping.rb +215 -0
  97. data/lib/fontisan/subset/options.rb +142 -0
  98. data/lib/fontisan/subset/profile.rb +152 -0
  99. data/lib/fontisan/subset/table_subsetter.rb +461 -0
  100. data/lib/fontisan/svg/font_face_generator.rb +278 -0
  101. data/lib/fontisan/svg/font_generator.rb +264 -0
  102. data/lib/fontisan/svg/glyph_generator.rb +168 -0
  103. data/lib/fontisan/svg/view_box_calculator.rb +137 -0
  104. data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
  105. data/lib/fontisan/tables/cff/charset.rb +282 -0
  106. data/lib/fontisan/tables/cff/charstring.rb +905 -0
  107. data/lib/fontisan/tables/cff/charstring_builder.rb +322 -0
  108. data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
  109. data/lib/fontisan/tables/cff/dict.rb +351 -0
  110. data/lib/fontisan/tables/cff/dict_builder.rb +242 -0
  111. data/lib/fontisan/tables/cff/encoding.rb +274 -0
  112. data/lib/fontisan/tables/cff/header.rb +102 -0
  113. data/lib/fontisan/tables/cff/index.rb +237 -0
  114. data/lib/fontisan/tables/cff/index_builder.rb +170 -0
  115. data/lib/fontisan/tables/cff/private_dict.rb +284 -0
  116. data/lib/fontisan/tables/cff/top_dict.rb +236 -0
  117. data/lib/fontisan/tables/cff.rb +487 -0
  118. data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
  119. data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
  120. data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
  121. data/lib/fontisan/tables/cff2.rb +341 -0
  122. data/lib/fontisan/tables/cvar.rb +242 -0
  123. data/lib/fontisan/tables/fvar.rb +2 -2
  124. data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
  125. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
  126. data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
  127. data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
  128. data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
  129. data/lib/fontisan/tables/glyf.rb +235 -0
  130. data/lib/fontisan/tables/gvar.rb +270 -0
  131. data/lib/fontisan/tables/hhea.rb +124 -0
  132. data/lib/fontisan/tables/hmtx.rb +287 -0
  133. data/lib/fontisan/tables/hvar.rb +191 -0
  134. data/lib/fontisan/tables/loca.rb +322 -0
  135. data/lib/fontisan/tables/maxp.rb +192 -0
  136. data/lib/fontisan/tables/mvar.rb +185 -0
  137. data/lib/fontisan/tables/name.rb +99 -30
  138. data/lib/fontisan/tables/variation_common.rb +346 -0
  139. data/lib/fontisan/tables/vvar.rb +234 -0
  140. data/lib/fontisan/true_type_collection.rb +156 -2
  141. data/lib/fontisan/true_type_font.rb +297 -11
  142. data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
  143. data/lib/fontisan/utilities/checksum_calculator.rb +18 -0
  144. data/lib/fontisan/utils/thread_pool.rb +134 -0
  145. data/lib/fontisan/validation/checksum_validator.rb +170 -0
  146. data/lib/fontisan/validation/consistency_validator.rb +197 -0
  147. data/lib/fontisan/validation/structure_validator.rb +198 -0
  148. data/lib/fontisan/validation/table_validator.rb +158 -0
  149. data/lib/fontisan/validation/validator.rb +152 -0
  150. data/lib/fontisan/variable/axis_normalizer.rb +215 -0
  151. data/lib/fontisan/variable/delta_applicator.rb +313 -0
  152. data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
  153. data/lib/fontisan/variable/instancer.rb +344 -0
  154. data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
  155. data/lib/fontisan/variable/region_matcher.rb +208 -0
  156. data/lib/fontisan/variable/static_font_builder.rb +213 -0
  157. data/lib/fontisan/variable/table_updater.rb +219 -0
  158. data/lib/fontisan/variation/blend_applier.rb +199 -0
  159. data/lib/fontisan/variation/cache.rb +298 -0
  160. data/lib/fontisan/variation/cache_key_builder.rb +162 -0
  161. data/lib/fontisan/variation/converter.rb +268 -0
  162. data/lib/fontisan/variation/data_extractor.rb +86 -0
  163. data/lib/fontisan/variation/delta_applier.rb +266 -0
  164. data/lib/fontisan/variation/delta_parser.rb +228 -0
  165. data/lib/fontisan/variation/inspector.rb +275 -0
  166. data/lib/fontisan/variation/instance_generator.rb +273 -0
  167. data/lib/fontisan/variation/interpolator.rb +231 -0
  168. data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
  169. data/lib/fontisan/variation/optimizer.rb +418 -0
  170. data/lib/fontisan/variation/parallel_generator.rb +150 -0
  171. data/lib/fontisan/variation/region_matcher.rb +221 -0
  172. data/lib/fontisan/variation/subsetter.rb +463 -0
  173. data/lib/fontisan/variation/table_accessor.rb +105 -0
  174. data/lib/fontisan/variation/validator.rb +345 -0
  175. data/lib/fontisan/variation/variation_context.rb +211 -0
  176. data/lib/fontisan/version.rb +1 -1
  177. data/lib/fontisan/woff2/directory.rb +257 -0
  178. data/lib/fontisan/woff2/header.rb +101 -0
  179. data/lib/fontisan/woff2/table_transformer.rb +163 -0
  180. data/lib/fontisan/woff2_font.rb +712 -0
  181. data/lib/fontisan/woff_font.rb +483 -0
  182. data/lib/fontisan.rb +120 -0
  183. data/scripts/compare_stack_aware.rb +187 -0
  184. data/scripts/measure_optimization.rb +141 -0
  185. metadata +205 -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,295 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require_relative "base_command"
5
+ require_relative "../variable/instancer"
6
+ require_relative "../variation/validator"
7
+ require_relative "../variation/parallel_generator"
8
+ require_relative "../converters/format_converter"
9
+ require_relative "../error"
10
+
11
+ module Fontisan
12
+ module Commands
13
+ # CLI command for generating static font instances from variable fonts
14
+ #
15
+ # Provides command-line interface for:
16
+ # - Instancing at specific coordinates
17
+ # - Using named instances
18
+ # - Converting output format during instancing
19
+ # - Listing available instances
20
+ # - Validation before generation
21
+ # - Dry-run mode for previewing
22
+ # - Progress tracking
23
+ # - Parallel batch generation
24
+ #
25
+ # @example Instance at coordinates
26
+ # fontisan instance variable.ttf --wght=700 --output=bold.ttf
27
+ #
28
+ # @example Instance with validation
29
+ # fontisan instance variable.ttf --wght=700 --validate --output=bold.ttf
30
+ #
31
+ # @example Dry-run to preview
32
+ # fontisan instance variable.ttf --wght=700 --dry-run
33
+ #
34
+ # @example Instance with progress
35
+ # fontisan instance variable.ttf --wght=700 --progress --output=bold.ttf
36
+ class InstanceCommand < BaseCommand
37
+ # Instance a variable font at specified coordinates
38
+ #
39
+ # @param input_path [String] Path to variable font file
40
+ def execute(input_path, options = {})
41
+ # Load variable font
42
+ font = load_font(input_path)
43
+
44
+ # Validate font if requested
45
+ validate_font(font) if options[:validate]
46
+
47
+ # Create instancer
48
+ instancer = Variable::Instancer.new(font)
49
+
50
+ # Handle list-instances option
51
+ if options[:list_instances]
52
+ list_instances(instancer)
53
+ return
54
+ end
55
+
56
+ # Handle dry-run mode
57
+ if options[:dry_run]
58
+ preview_instance(instancer, options)
59
+ return
60
+ end
61
+
62
+ # Determine output path
63
+ output_path = determine_output_path(input_path, options)
64
+
65
+ # Generate instance
66
+ if options[:named_instance]
67
+ instance_named(instancer, options[:named_instance], output_path,
68
+ options)
69
+ else
70
+ instance_coords(instancer, extract_coordinates(options), output_path,
71
+ options)
72
+ end
73
+
74
+ puts "Static font instance written to: #{output_path}"
75
+ rescue VariationError => e
76
+ $stderr.puts "Variation Error: #{e.detailed_message}"
77
+ exit 1
78
+ rescue StandardError => e
79
+ $stderr.puts "Error: #{e.message}"
80
+ $stderr.puts e.backtrace.first(5).join("\n") if options[:verbose]
81
+ exit 1
82
+ end
83
+
84
+ private
85
+
86
+ # Validate font before generating instance
87
+ #
88
+ # @param font [Object] Font object
89
+ def validate_font(font)
90
+ puts "Validating font..." if @options[:verbose]
91
+
92
+ validator = Variation::Validator.new(font)
93
+ errors = validator.validate
94
+
95
+ if errors.any?
96
+ $stderr.puts "Validation errors found:"
97
+ errors.each do |error|
98
+ $stderr.puts " - #{error}"
99
+ end
100
+ exit 1
101
+ end
102
+
103
+ puts "Font validation passed" if @options[:verbose]
104
+ end
105
+
106
+ # Preview instance without generating
107
+ #
108
+ # @param instancer [Variable::Instancer] Instancer object
109
+ # @param options [Hash] Command options
110
+ def preview_instance(instancer, options)
111
+ coords = extract_coordinates(options)
112
+
113
+ if coords.empty?
114
+ raise ArgumentError,
115
+ "No coordinates specified. Use --wght=700, --wdth=100, etc."
116
+ end
117
+
118
+ puts "Dry-run mode: Preview of instance generation"
119
+ puts
120
+ puts "Coordinates:"
121
+ coords.each do |axis, value|
122
+ puts " #{axis}: #{value}"
123
+ end
124
+ puts
125
+ puts "Output would be written to: #{determine_output_path(@input_path, options)}"
126
+ puts
127
+ puts "Use without --dry-run to actually generate the instance."
128
+ end
129
+
130
+ # Instance at specific coordinates
131
+ #
132
+ # @param instancer [Variable::Instancer] Instancer object
133
+ # @param coords [Hash] User coordinates
134
+ # @param output_path [String] Output file path
135
+ # @param options [Hash] Command options
136
+ def instance_coords(instancer, coords, output_path, options)
137
+ if coords.empty?
138
+ raise ArgumentError,
139
+ "No coordinates specified. Use --wght=700, --wdth=100, etc."
140
+ end
141
+
142
+ # Show progress if requested
143
+ print "Generating instance..." if options[:progress]
144
+
145
+ # Generate instance
146
+ binary = instancer.instance(coords)
147
+
148
+ puts " done" if options[:progress]
149
+
150
+ # Convert format if requested
151
+ if options[:to]
152
+ print "Converting format..." if options[:progress]
153
+ binary = convert_format(binary, options)
154
+ puts " done" if options[:progress]
155
+ end
156
+
157
+ # Write to file
158
+ print "Writing output..." if options[:progress]
159
+ File.binwrite(output_path, binary)
160
+ puts " done" if options[:progress]
161
+ end
162
+
163
+ # Instance using named instance
164
+ #
165
+ # @param instancer [Variable::Instancer] Instancer object
166
+ # @param instance_name [String] Named instance name
167
+ # @param output_path [String] Output file path
168
+ # @param options [Hash] Command options
169
+ def instance_named(instancer, instance_name, output_path, options)
170
+ # Generate instance
171
+ binary = instancer.instance_named(instance_name)
172
+
173
+ # Convert format if requested
174
+ binary = convert_format(binary, options) if options[:to]
175
+
176
+ # Write to file
177
+ File.binwrite(output_path, binary)
178
+ end
179
+
180
+ # List available named instances
181
+ #
182
+ # @param instancer [Variable::Instancer] Instancer object
183
+ def list_instances(instancer)
184
+ instances = instancer.named_instances
185
+
186
+ if instances.empty?
187
+ puts "No named instances defined in font."
188
+ return
189
+ end
190
+
191
+ puts "Available named instances:"
192
+ puts
193
+
194
+ instances.each do |instance|
195
+ puts " #{instance[:name]}"
196
+ puts " Coordinates:"
197
+ instance[:coordinates].each do |axis, value|
198
+ puts " #{axis}: #{value}"
199
+ end
200
+ puts
201
+ end
202
+ end
203
+
204
+ # Extract axis coordinates from options
205
+ #
206
+ # @param options [Hash] Command options
207
+ # @return [Hash] Coordinates hash
208
+ def extract_coordinates(options)
209
+ coords = {}
210
+
211
+ # Check for common axis options
212
+ coords["wght"] = options[:wght].to_f if options[:wght]
213
+ coords["wdth"] = options[:wdth].to_f if options[:wdth]
214
+ coords["slnt"] = options[:slnt].to_f if options[:slnt]
215
+ coords["ital"] = options[:ital].to_f if options[:ital]
216
+ coords["opsz"] = options[:opsz].to_f if options[:opsz]
217
+
218
+ # Allow arbitrary axis coordinates via --axis-TAG=value
219
+ options.each do |key, value|
220
+ key_str = key.to_s
221
+ if key_str.start_with?("axis_")
222
+ axis_tag = key_str.sub("axis_", "")
223
+ coords[axis_tag] = value.to_f
224
+ end
225
+ end
226
+
227
+ coords
228
+ end
229
+
230
+ # Determine output path
231
+ #
232
+ # @param input_path [String] Input file path
233
+ # @param options [Hash] Command options
234
+ # @return [String] Output path
235
+ def determine_output_path(input_path, options)
236
+ return options[:output] if options[:output]
237
+
238
+ # Generate default output name
239
+ base = File.basename(input_path, ".*")
240
+ ext = options[:to] || File.extname(input_path)[1..]
241
+ dir = File.dirname(input_path)
242
+
243
+ "#{dir}/#{base}-instance.#{ext}"
244
+ end
245
+
246
+ # Convert format using FormatConverter
247
+ #
248
+ # @param binary [String] Font binary
249
+ # @param options [Hash] Command options
250
+ # @return [String] Converted binary
251
+ def convert_format(binary, options)
252
+ target_format = options[:to].to_sym
253
+
254
+ # Load font from binary
255
+ require "tempfile"
256
+ Tempfile.create(["instance", ".ttf"]) do |temp_file|
257
+ temp_file.binmode
258
+ temp_file.write(binary)
259
+ temp_file.flush
260
+
261
+ font = FontLoader.load(temp_file.path)
262
+ converter = Converters::FormatConverter.new
263
+
264
+ result = converter.convert(font, target_format)
265
+
266
+ case target_format
267
+ when :woff, :woff2
268
+ result[:font_data]
269
+ when :svg
270
+ result[:svg_xml]
271
+ else
272
+ binary
273
+ end
274
+ end
275
+ rescue StandardError => e
276
+ warn "Format conversion failed: #{e.message}"
277
+ binary
278
+ end
279
+
280
+ # Load font from file
281
+ #
282
+ # @param path [String] Font file path
283
+ # @return [Object] Font object
284
+ def load_font(path)
285
+ unless File.exist?(path)
286
+ raise ArgumentError, "Font file not found: #{path}"
287
+ end
288
+
289
+ FontLoader.load(path)
290
+ rescue StandardError => e
291
+ raise ArgumentError, "Failed to load font: #{e.message}"
292
+ end
293
+ end
294
+ end
295
+ 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