ruby_spriter 0.6.7 → 0.7.0.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +3 -3
  3. data/CHANGELOG.md +1035 -405
  4. data/Gemfile +17 -17
  5. data/LICENSE +21 -21
  6. data/README.md +183 -902
  7. data/bin/ruby_spriter +20 -20
  8. data/lib/ruby_spriter/background_sampler.rb +140 -0
  9. data/lib/ruby_spriter/batch_processor.rb +268 -212
  10. data/lib/ruby_spriter/cell_cleanup_config.rb +21 -0
  11. data/lib/ruby_spriter/cell_cleanup_gimp_script.rb +123 -0
  12. data/lib/ruby_spriter/cell_cleanup_processor.rb +230 -0
  13. data/lib/ruby_spriter/cli.rb +676 -612
  14. data/lib/ruby_spriter/compression_manager.rb +101 -101
  15. data/lib/ruby_spriter/consolidator.rb +179 -179
  16. data/lib/ruby_spriter/dependency_checker.rb +224 -174
  17. data/lib/ruby_spriter/ghost_edge_cleaner.rb +164 -0
  18. data/lib/ruby_spriter/gimp_processor.rb +1188 -667
  19. data/lib/ruby_spriter/metadata_manager.rb +117 -116
  20. data/lib/ruby_spriter/platform.rb +137 -82
  21. data/lib/ruby_spriter/processor.rb +1230 -886
  22. data/lib/ruby_spriter/smoke_detector.rb +223 -0
  23. data/lib/ruby_spriter/threshold_stepper.rb +227 -0
  24. data/lib/ruby_spriter/utils/file_helper.rb +82 -82
  25. data/lib/ruby_spriter/utils/image_helper.rb +16 -0
  26. data/lib/ruby_spriter/utils/output_formatter.rb +65 -65
  27. data/lib/ruby_spriter/utils/path_helper.rb +59 -59
  28. data/lib/ruby_spriter/utils/spritesheet_splitter.rb +86 -86
  29. data/lib/ruby_spriter/version.rb +6 -7
  30. data/lib/ruby_spriter/video_processor.rb +357 -139
  31. data/lib/ruby_spriter.rb +38 -34
  32. data/ruby_spriter.gemspec +44 -42
  33. data/spec/fixtures/centered_sprite_with_inner_bg.png +0 -0
  34. data/spec/fixtures/centered_sprite_with_inner_bg_inner_removed.png +0 -0
  35. data/spec/fixtures/centered_sprite_with_inner_bg_threshold_stepped.png +0 -0
  36. data/spec/fixtures/complex_background_sprite.png +0 -0
  37. data/spec/fixtures/death - bloodborne rekconing.mp4 +0 -0
  38. data/spec/fixtures/death - bloodborne rekconing_spritesheet-nobg-global.png +0 -0
  39. data/spec/fixtures/death - bloodborne rekconing_spritesheet.png +0 -0
  40. data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185027_431-nobg-global.png +0 -0
  41. data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185027_431.png +0 -0
  42. data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185125_738-nobg-global.png +0 -0
  43. data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185125_738.png +0 -0
  44. data/spec/fixtures/has_inner_bg.png +0 -0
  45. data/spec/fixtures/has_small_inner_bg.png +0 -0
  46. data/spec/fixtures/smoke_effect_sprite.png +0 -0
  47. data/spec/fixtures/spritesheet_with_metadata.png +0 -0
  48. data/spec/fixtures/test_sprite.png +0 -0
  49. data/spec/fixtures/test_sprite_smoke_processed.png +0 -0
  50. data/spec/fixtures/test_video_spritesheet.png +0 -0
  51. data/spec/fixtures/transparent_bg_sprite.png +0 -0
  52. data/spec/fixtures/walk_north_sprite-sheet.png +0 -0
  53. data/spec/integration/no_fuzzy_mode_spec.rb +100 -0
  54. data/spec/ruby_spriter/batch_processor_spec.rb +509 -200
  55. data/spec/ruby_spriter/cli_spec.rb +2026 -1892
  56. data/spec/ruby_spriter/compression_manager_spec.rb +157 -157
  57. data/spec/ruby_spriter/consolidator_spec.rb +538 -538
  58. data/spec/ruby_spriter/gimp_processor_spec.rb +523 -425
  59. data/spec/ruby_spriter/platform_spec.rb +92 -82
  60. data/spec/ruby_spriter/processor_spec.rb +911 -735
  61. data/spec/ruby_spriter/utils/file_helper_spec.rb +150 -150
  62. data/spec/ruby_spriter/utils/path_helper_spec.rb +78 -78
  63. data/spec/ruby_spriter/utils/spritesheet_splitter_spec.rb +104 -104
  64. data/spec/ruby_spriter/video_processor_spec.rb +346 -29
  65. data/spec/spec_helper.rb +41 -41
  66. data/spec/tmp/cli_test_output.png +0 -0
  67. data/spec/tmp/cli_test_output_20251105_114647_395.png +0 -0
  68. data/spec/tmp/combined_test.png +0 -0
  69. data/spec/tmp/compat_test.png +0 -0
  70. data/spec/tmp/compat_test_20251030_174233_361.png +0 -0
  71. data/spec/tmp/final_all_features.png +0 -0
  72. data/spec/tmp/final_test_all_features.png +0 -0
  73. data/spec/tmp/full_pipeline_test.png +0 -0
  74. data/spec/tmp/inner_test.png +0 -0
  75. data/spec/tmp/integration_test.png +0 -0
  76. data/spec/tmp/validation_test.png +0 -0
  77. data/spec/unit/background_sampler_spec.rb +132 -0
  78. data/spec/unit/cell_cleanup_config_spec.rb +32 -0
  79. data/spec/unit/cell_cleanup_gimp_script_spec.rb +59 -0
  80. data/spec/unit/cell_cleanup_processor_spec.rb +261 -0
  81. data/spec/unit/ghost_edge_cleaner_spec.rb +223 -0
  82. data/spec/unit/gimp_processor_single_point_selection_spec.rb +81 -0
  83. data/spec/unit/smoke_detector_spec.rb +246 -0
  84. data/spec/unit/threshold_stepper_spec.rb +195 -0
  85. metadata +56 -10
@@ -1,101 +1,101 @@
1
- # frozen_string_literal: true
2
-
3
- require 'open3'
4
- require 'fileutils'
5
-
6
- module RubySpriter
7
- # Manages PNG compression with metadata preservation
8
- class CompressionManager
9
- # Compress PNG file using ImageMagick with maximum compression
10
- # @param input_file [String] Source PNG file
11
- # @param output_file [String] Destination PNG file
12
- # @param debug [Boolean] Enable debug output
13
- def self.compress(input_file, output_file, debug: false)
14
- Utils::FileHelper.validate_readable!(input_file)
15
-
16
- cmd = build_compression_command(input_file, output_file)
17
-
18
- if debug
19
- Utils::OutputFormatter.indent("DEBUG: Compression command: #{cmd}")
20
- end
21
-
22
- stdout, stderr, status = Open3.capture3(cmd)
23
-
24
- unless status.success?
25
- raise ProcessingError, "Failed to compress PNG: #{stderr}"
26
- end
27
-
28
- Utils::FileHelper.validate_exists!(output_file)
29
- end
30
-
31
- # Compress PNG file while preserving embedded metadata
32
- # @param input_file [String] Source PNG file
33
- # @param output_file [String] Destination PNG file
34
- # @param debug [Boolean] Enable debug output
35
- def self.compress_with_metadata(input_file, output_file, debug: false)
36
- # Read metadata before compression
37
- metadata = MetadataManager.read(input_file)
38
-
39
- # Compress the file
40
- temp_file = output_file.gsub('.png', '_compress_temp.png')
41
- compress(input_file, temp_file, debug: debug)
42
-
43
- # Re-embed metadata if it existed
44
- if metadata
45
- MetadataManager.embed(
46
- temp_file,
47
- output_file,
48
- columns: metadata[:columns],
49
- rows: metadata[:rows],
50
- frames: metadata[:frames],
51
- debug: debug
52
- )
53
-
54
- # Clean up temp file
55
- FileUtils.rm_f(temp_file) if File.exist?(temp_file)
56
- else
57
- # No metadata, just move temp to output
58
- FileUtils.mv(temp_file, output_file)
59
- end
60
- end
61
-
62
- # Get compression statistics
63
- # @param original_file [String] Original file path
64
- # @param compressed_file [String] Compressed file path
65
- # @return [Hash] Statistics including sizes and reduction percentage
66
- def self.compression_stats(original_file, compressed_file)
67
- original_size = File.size(original_file)
68
- compressed_size = File.size(compressed_file)
69
- saved_bytes = original_size - compressed_size
70
- reduction_percent = (saved_bytes.to_f / original_size * 100.0)
71
-
72
- {
73
- original_size: original_size,
74
- compressed_size: compressed_size,
75
- saved_bytes: saved_bytes,
76
- reduction_percent: reduction_percent
77
- }
78
- end
79
-
80
- private_class_method def self.build_compression_command(input_file, output_file)
81
- magick_cmd = Platform.imagemagick_convert_cmd
82
-
83
- # Use maximum PNG compression settings:
84
- # - compression-level=9: Maximum zlib compression
85
- # - compression-filter=5: Paeth filter (best for most images)
86
- # - compression-strategy=1: Filtered strategy
87
- # - quality=95: High quality
88
- # - strip: Remove all metadata (we'll re-add it later)
89
- [
90
- magick_cmd,
91
- Utils::PathHelper.quote_path(input_file),
92
- '-strip',
93
- '-define', 'png:compression-level=9',
94
- '-define', 'png:compression-filter=5',
95
- '-define', 'png:compression-strategy=1',
96
- '-quality', '95',
97
- Utils::PathHelper.quote_path(output_file)
98
- ].join(' ')
99
- end
100
- end
101
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'fileutils'
5
+
6
+ module RubySpriter
7
+ # Manages PNG compression with metadata preservation
8
+ class CompressionManager
9
+ # Compress PNG file using ImageMagick with maximum compression
10
+ # @param input_file [String] Source PNG file
11
+ # @param output_file [String] Destination PNG file
12
+ # @param debug [Boolean] Enable debug output
13
+ def self.compress(input_file, output_file, debug: false)
14
+ Utils::FileHelper.validate_readable!(input_file)
15
+
16
+ cmd = build_compression_command(input_file, output_file)
17
+
18
+ if debug
19
+ Utils::OutputFormatter.indent("DEBUG: Compression command: #{cmd}")
20
+ end
21
+
22
+ stdout, stderr, status = Open3.capture3(cmd)
23
+
24
+ unless status.success?
25
+ raise ProcessingError, "Failed to compress PNG: #{stderr}"
26
+ end
27
+
28
+ Utils::FileHelper.validate_exists!(output_file)
29
+ end
30
+
31
+ # Compress PNG file while preserving embedded metadata
32
+ # @param input_file [String] Source PNG file
33
+ # @param output_file [String] Destination PNG file
34
+ # @param debug [Boolean] Enable debug output
35
+ def self.compress_with_metadata(input_file, output_file, debug: false)
36
+ # Read metadata before compression
37
+ metadata = MetadataManager.read(input_file)
38
+
39
+ # Compress the file
40
+ temp_file = output_file.gsub('.png', '_compress_temp.png')
41
+ compress(input_file, temp_file, debug: debug)
42
+
43
+ # Re-embed metadata if it existed
44
+ if metadata
45
+ MetadataManager.embed(
46
+ temp_file,
47
+ output_file,
48
+ columns: metadata[:columns],
49
+ rows: metadata[:rows],
50
+ frames: metadata[:frames],
51
+ debug: debug
52
+ )
53
+
54
+ # Clean up temp file
55
+ FileUtils.rm_f(temp_file) if File.exist?(temp_file)
56
+ else
57
+ # No metadata, just move temp to output
58
+ FileUtils.mv(temp_file, output_file)
59
+ end
60
+ end
61
+
62
+ # Get compression statistics
63
+ # @param original_file [String] Original file path
64
+ # @param compressed_file [String] Compressed file path
65
+ # @return [Hash] Statistics including sizes and reduction percentage
66
+ def self.compression_stats(original_file, compressed_file)
67
+ original_size = File.size(original_file)
68
+ compressed_size = File.size(compressed_file)
69
+ saved_bytes = original_size - compressed_size
70
+ reduction_percent = (saved_bytes.to_f / original_size * 100.0)
71
+
72
+ {
73
+ original_size: original_size,
74
+ compressed_size: compressed_size,
75
+ saved_bytes: saved_bytes,
76
+ reduction_percent: reduction_percent
77
+ }
78
+ end
79
+
80
+ private_class_method def self.build_compression_command(input_file, output_file)
81
+ magick_cmd = Platform.imagemagick_convert_cmd
82
+
83
+ # Use maximum PNG compression settings:
84
+ # - compression-level=9: Maximum zlib compression
85
+ # - compression-filter=5: Paeth filter (best for most images)
86
+ # - compression-strategy=1: Filtered strategy
87
+ # - quality=95: High quality
88
+ # - strip: Remove all metadata (we'll re-add it later)
89
+ [
90
+ magick_cmd,
91
+ Utils::PathHelper.quote_path(input_file),
92
+ '-strip',
93
+ '-define', 'png:compression-level=9',
94
+ '-define', 'png:compression-filter=5',
95
+ '-define', 'png:compression-strategy=1',
96
+ '-quality', '95',
97
+ Utils::PathHelper.quote_path(output_file)
98
+ ].join(' ')
99
+ end
100
+ end
101
+ end
@@ -1,179 +1,179 @@
1
- # frozen_string_literal: true
2
-
3
- require 'open3'
4
-
5
- module RubySpriter
6
- # Consolidates multiple spritesheets vertically
7
- class Consolidator
8
- attr_reader :options
9
-
10
- def initialize(options = {})
11
- @options = options
12
- end
13
-
14
- # Consolidate multiple spritesheets into one
15
- # @param files [Array<String>] Array of spritesheet file paths
16
- # @param output_file [String] Output consolidated file path
17
- # @return [Hash] Processing results
18
- def consolidate(files, output_file)
19
- validate_files!(files)
20
-
21
- Utils::OutputFormatter.header("Consolidating Spritesheets")
22
-
23
- metadata_list = read_all_metadata(files)
24
- validate_compatibility!(metadata_list) if options[:validate_columns]
25
-
26
- create_consolidated_image(files, output_file)
27
-
28
- total_frames = metadata_list.sum { |m| m[:frames] }
29
- columns = metadata_list.first[:columns]
30
- rows = (total_frames.to_f / columns).ceil
31
-
32
- # Embed consolidated metadata
33
- temp_file = output_file.sub('.png', '_temp.png')
34
- File.rename(output_file, temp_file)
35
-
36
- MetadataManager.embed(
37
- temp_file,
38
- output_file,
39
- columns: columns,
40
- rows: rows,
41
- frames: total_frames,
42
- debug: options[:debug]
43
- )
44
-
45
- File.delete(temp_file) if File.exist?(temp_file)
46
-
47
- file_size = File.size(output_file)
48
-
49
- # Display results with Godot instructions
50
- display_consolidation_results(output_file, file_size, files, metadata_list, columns, rows, total_frames)
51
-
52
- {
53
- output_file: output_file,
54
- columns: columns,
55
- rows: rows,
56
- frames: total_frames,
57
- size: file_size
58
- }
59
- end
60
-
61
- # Find all PNG files with spritesheet metadata in a directory
62
- # @param directory [String] Directory path to scan
63
- # @return [Array<String>] Sorted array of spritesheet file paths
64
- def find_spritesheets_in_directory(directory)
65
- # Validate directory exists
66
- unless File.directory?(directory)
67
- raise ValidationError, "Directory not found: #{directory}"
68
- end
69
-
70
- # Find all PNG files
71
- pattern = File.join(directory, '*.png')
72
- png_files = Dir.glob(pattern)
73
-
74
- # Filter to only files with metadata
75
- spritesheets = png_files.select do |file|
76
- metadata = MetadataManager.read(file)
77
- !metadata.nil?
78
- end
79
-
80
- # Validate we found at least one
81
- if spritesheets.empty?
82
- raise ValidationError, "No PNG files with spritesheet metadata found in directory: #{directory}"
83
- end
84
-
85
- # Validate we have at least 2
86
- if spritesheets.length < 2
87
- raise ValidationError, "Found only #{spritesheets.length} spritesheet, need at least 2 for consolidation"
88
- end
89
-
90
- # Sort alphabetically by filename
91
- spritesheets.sort
92
- end
93
-
94
- private
95
-
96
- def validate_files!(files)
97
- raise ValidationError, "Need at least 2 files to consolidate" if files.length < 2
98
-
99
- files.each { |file| Utils::FileHelper.validate_readable!(file) }
100
- end
101
-
102
- def read_all_metadata(files)
103
- metadata_list = files.map do |file|
104
- metadata = MetadataManager.read(file)
105
-
106
- unless metadata
107
- raise ValidationError, "File missing metadata: #{file}\nAll files must be Ruby Spriter spritesheets."
108
- end
109
-
110
- metadata
111
- end
112
-
113
- metadata_list
114
- end
115
-
116
- def validate_compatibility!(metadata_list)
117
- columns = metadata_list.first[:columns]
118
-
119
- incompatible = metadata_list.find { |m| m[:columns] != columns }
120
-
121
- if incompatible
122
- raise ValidationError,
123
- "Column count mismatch: Expected #{columns}, found #{incompatible[:columns]}\n" \
124
- "Use --no-validate-columns to force consolidation."
125
- end
126
- end
127
-
128
- def create_consolidated_image(files, output_file)
129
- Utils::OutputFormatter.indent("Stacking spritesheets vertically...")
130
-
131
- # Use ImageMagick to stack images vertically
132
- magick_cmd = Platform.imagemagick_convert_cmd
133
-
134
- cmd = [
135
- magick_cmd,
136
- *files.map { |f| Utils::PathHelper.quote_path(f) },
137
- '-append',
138
- Utils::PathHelper.quote_path(output_file)
139
- ].join(' ')
140
-
141
- if options[:debug]
142
- Utils::OutputFormatter.indent("DEBUG: ImageMagick command: #{cmd}")
143
- end
144
-
145
- stdout, stderr, status = Open3.capture3(cmd)
146
-
147
- unless status.success?
148
- raise ProcessingError, "ImageMagick consolidation failed: #{stderr}"
149
- end
150
-
151
- Utils::FileHelper.validate_exists!(output_file)
152
- end
153
-
154
- def display_consolidation_results(output_file, file_size, files, metadata_list, columns, rows, total_frames)
155
- Utils::OutputFormatter.success("Consolidated spritesheet created")
156
- Utils::OutputFormatter.indent("Output: #{output_file}")
157
- Utils::OutputFormatter.indent("Size: #{Utils::FileHelper.format_size(file_size)}")
158
- Utils::OutputFormatter.note("Combined #{files.length} spritesheets (#{total_frames} total frames)")
159
-
160
- puts "\n Grid Layout:"
161
- Utils::OutputFormatter.indent("Columns: #{columns}")
162
- Utils::OutputFormatter.indent("Rows: #{rows}")
163
- Utils::OutputFormatter.indent("Total Frames: #{total_frames}")
164
-
165
- puts "\n 📊 Godot AnimatedSprite2D Settings:"
166
- Utils::OutputFormatter.indent("HFrames = #{columns}")
167
- Utils::OutputFormatter.indent("VFrames = #{rows}")
168
-
169
- puts "\n 📋 Source Breakdown:"
170
- metadata_list.each_with_index do |meta, index|
171
- file_basename = File.basename(files[index])
172
- Utils::OutputFormatter.indent("#{index + 1}. #{file_basename}")
173
- Utils::OutputFormatter.indent(" └─ #{meta[:columns]}×#{meta[:rows]} grid (#{meta[:frames]} frames)")
174
- end
175
-
176
- puts ""
177
- end
178
- end
179
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module RubySpriter
6
+ # Consolidates multiple spritesheets vertically
7
+ class Consolidator
8
+ attr_reader :options
9
+
10
+ def initialize(options = {})
11
+ @options = options
12
+ end
13
+
14
+ # Consolidate multiple spritesheets into one
15
+ # @param files [Array<String>] Array of spritesheet file paths
16
+ # @param output_file [String] Output consolidated file path
17
+ # @return [Hash] Processing results
18
+ def consolidate(files, output_file)
19
+ validate_files!(files)
20
+
21
+ Utils::OutputFormatter.header("Consolidating Spritesheets")
22
+
23
+ metadata_list = read_all_metadata(files)
24
+ validate_compatibility!(metadata_list) if options[:validate_columns]
25
+
26
+ create_consolidated_image(files, output_file)
27
+
28
+ total_frames = metadata_list.sum { |m| m[:frames] }
29
+ columns = metadata_list.first[:columns]
30
+ rows = (total_frames.to_f / columns).ceil
31
+
32
+ # Embed consolidated metadata
33
+ temp_file = output_file.sub('.png', '_temp.png')
34
+ File.rename(output_file, temp_file)
35
+
36
+ MetadataManager.embed(
37
+ temp_file,
38
+ output_file,
39
+ columns: columns,
40
+ rows: rows,
41
+ frames: total_frames,
42
+ debug: options[:debug]
43
+ )
44
+
45
+ File.delete(temp_file) if File.exist?(temp_file)
46
+
47
+ file_size = File.size(output_file)
48
+
49
+ # Display results with Godot instructions
50
+ display_consolidation_results(output_file, file_size, files, metadata_list, columns, rows, total_frames)
51
+
52
+ {
53
+ output_file: output_file,
54
+ columns: columns,
55
+ rows: rows,
56
+ frames: total_frames,
57
+ size: file_size
58
+ }
59
+ end
60
+
61
+ # Find all PNG files with spritesheet metadata in a directory
62
+ # @param directory [String] Directory path to scan
63
+ # @return [Array<String>] Sorted array of spritesheet file paths
64
+ def find_spritesheets_in_directory(directory)
65
+ # Validate directory exists
66
+ unless File.directory?(directory)
67
+ raise ValidationError, "Directory not found: #{directory}"
68
+ end
69
+
70
+ # Find all PNG files
71
+ pattern = File.join(directory, '*.png')
72
+ png_files = Dir.glob(pattern)
73
+
74
+ # Filter to only files with metadata
75
+ spritesheets = png_files.select do |file|
76
+ metadata = MetadataManager.read(file)
77
+ !metadata.nil?
78
+ end
79
+
80
+ # Validate we found at least one
81
+ if spritesheets.empty?
82
+ raise ValidationError, "No PNG files with spritesheet metadata found in directory: #{directory}"
83
+ end
84
+
85
+ # Validate we have at least 2
86
+ if spritesheets.length < 2
87
+ raise ValidationError, "Found only #{spritesheets.length} spritesheet, need at least 2 for consolidation"
88
+ end
89
+
90
+ # Sort alphabetically by filename
91
+ spritesheets.sort
92
+ end
93
+
94
+ private
95
+
96
+ def validate_files!(files)
97
+ raise ValidationError, "Need at least 2 files to consolidate" if files.length < 2
98
+
99
+ files.each { |file| Utils::FileHelper.validate_readable!(file) }
100
+ end
101
+
102
+ def read_all_metadata(files)
103
+ metadata_list = files.map do |file|
104
+ metadata = MetadataManager.read(file)
105
+
106
+ unless metadata
107
+ raise ValidationError, "File missing metadata: #{file}\nAll files must be Ruby Spriter spritesheets."
108
+ end
109
+
110
+ metadata
111
+ end
112
+
113
+ metadata_list
114
+ end
115
+
116
+ def validate_compatibility!(metadata_list)
117
+ columns = metadata_list.first[:columns]
118
+
119
+ incompatible = metadata_list.find { |m| m[:columns] != columns }
120
+
121
+ if incompatible
122
+ raise ValidationError,
123
+ "Column count mismatch: Expected #{columns}, found #{incompatible[:columns]}\n" \
124
+ "Use --no-validate-columns to force consolidation."
125
+ end
126
+ end
127
+
128
+ def create_consolidated_image(files, output_file)
129
+ Utils::OutputFormatter.indent("Stacking spritesheets vertically...")
130
+
131
+ # Use ImageMagick to stack images vertically
132
+ magick_cmd = Platform.imagemagick_convert_cmd
133
+
134
+ cmd = [
135
+ magick_cmd,
136
+ *files.map { |f| Utils::PathHelper.quote_path(f) },
137
+ '-append',
138
+ Utils::PathHelper.quote_path(output_file)
139
+ ].join(' ')
140
+
141
+ if options[:debug]
142
+ Utils::OutputFormatter.indent("DEBUG: ImageMagick command: #{cmd}")
143
+ end
144
+
145
+ stdout, stderr, status = Open3.capture3(cmd)
146
+
147
+ unless status.success?
148
+ raise ProcessingError, "ImageMagick consolidation failed: #{stderr}"
149
+ end
150
+
151
+ Utils::FileHelper.validate_exists!(output_file)
152
+ end
153
+
154
+ def display_consolidation_results(output_file, file_size, files, metadata_list, columns, rows, total_frames)
155
+ Utils::OutputFormatter.success("Consolidated spritesheet created")
156
+ Utils::OutputFormatter.indent("Output: #{output_file}")
157
+ Utils::OutputFormatter.indent("Size: #{Utils::FileHelper.format_size(file_size)}")
158
+ Utils::OutputFormatter.note("Combined #{files.length} spritesheets (#{total_frames} total frames)")
159
+
160
+ puts "\n Grid Layout:"
161
+ Utils::OutputFormatter.indent("Columns: #{columns}")
162
+ Utils::OutputFormatter.indent("Rows: #{rows}")
163
+ Utils::OutputFormatter.indent("Total Frames: #{total_frames}")
164
+
165
+ puts "\n 📊 Godot AnimatedSprite2D Settings:"
166
+ Utils::OutputFormatter.indent("HFrames = #{columns}")
167
+ Utils::OutputFormatter.indent("VFrames = #{rows}")
168
+
169
+ puts "\n 📋 Source Breakdown:"
170
+ metadata_list.each_with_index do |meta, index|
171
+ file_basename = File.basename(files[index])
172
+ Utils::OutputFormatter.indent("#{index + 1}. #{file_basename}")
173
+ Utils::OutputFormatter.indent(" └─ #{meta[:columns]}×#{meta[:rows]} grid (#{meta[:frames]} frames)")
174
+ end
175
+
176
+ puts ""
177
+ end
178
+ end
179
+ end