ruby_spriter 0.6.7.1 → 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 -524
  4. data/Gemfile +17 -17
  5. data/LICENSE +21 -21
  6. data/README.md +183 -950
  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 -214
  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 -224
  17. data/lib/ruby_spriter/ghost_edge_cleaner.rb +164 -0
  18. data/lib/ruby_spriter/gimp_processor.rb +1188 -1058
  19. data/lib/ruby_spriter/metadata_manager.rb +117 -116
  20. data/lib/ruby_spriter/platform.rb +137 -137
  21. data/lib/ruby_spriter/processor.rb +1230 -891
  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 -92
  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,139 +1,357 @@
1
- # frozen_string_literal: true
2
-
3
- require 'open3'
4
-
5
- module RubySpriter
6
- # Processes video files with FFmpeg
7
- class VideoProcessor
8
- attr_reader :options
9
-
10
- def initialize(options = {})
11
- @options = options
12
- end
13
-
14
- # Create spritesheet from video file
15
- # @param video_file [String] Path to video file
16
- # @param output_file [String] Path to output spritesheet
17
- # @return [Hash] Processing results
18
- def create_spritesheet(video_file, output_file)
19
- Utils::FileHelper.validate_readable!(video_file)
20
-
21
- Utils::OutputFormatter.header("Video Analysis")
22
- duration = get_duration(video_file)
23
- Utils::OutputFormatter.indent("Duration: #{duration.round(2)} seconds\n")
24
-
25
- columns = options[:columns] || 4
26
- frame_count = options[:frame_count] || 16
27
- rows = (frame_count.to_f / columns).ceil
28
-
29
- Utils::OutputFormatter.header("Creating Spritesheet")
30
-
31
- temp_file = output_file.sub('.png', '_temp.png')
32
-
33
- create_with_ffmpeg(video_file, temp_file, duration, columns, rows, frame_count)
34
-
35
- # Embed metadata
36
- MetadataManager.embed(
37
- temp_file,
38
- output_file,
39
- columns: columns,
40
- rows: rows,
41
- frames: frame_count,
42
- debug: options[:debug]
43
- )
44
-
45
- # Clean up temp file
46
- File.delete(temp_file) if File.exist?(temp_file)
47
-
48
- file_size = File.size(output_file)
49
-
50
- # Display results with Godot instructions
51
- display_spritesheet_results(output_file, file_size, columns, rows, frame_count)
52
-
53
- {
54
- output_file: output_file,
55
- columns: columns,
56
- rows: rows,
57
- frames: frame_count,
58
- size: file_size
59
- }
60
- end
61
-
62
- # Get video duration in seconds
63
- # @param video_file [String] Path to video file
64
- # @return [Float] Duration in seconds
65
- def get_duration(video_file)
66
- cmd = [
67
- 'ffprobe',
68
- '-v', 'error',
69
- '-show_entries', 'format=duration',
70
- '-of', 'default=noprint_wrappers=1:nokey=1',
71
- Utils::PathHelper.quote_path(video_file)
72
- ].join(' ')
73
-
74
- stdout, stderr, status = Open3.capture3(cmd)
75
-
76
- unless status.success?
77
- raise ProcessingError, "Could not determine video duration: #{stderr}"
78
- end
79
-
80
- stdout.strip.to_f
81
- end
82
-
83
- private
84
-
85
- def create_with_ffmpeg(video_file, output_file, duration, columns, rows, frame_count)
86
- fps = (frame_count / duration.to_f).round(6)
87
- max_width = options[:max_width] || 320
88
-
89
- Utils::OutputFormatter.indent("Layout: #{columns}×#{rows} grid (#{frame_count} frames)")
90
- Utils::OutputFormatter.indent("Frame rate: #{fps} fps")
91
- Utils::OutputFormatter.indent("Max frame width: #{max_width}px")
92
- Utils::OutputFormatter.indent("Building spritesheet...")
93
-
94
- filter_complex = [
95
- "fps=#{fps}",
96
- "scale=#{max_width}:-1:flags=lanczos",
97
- "tile=#{columns}x#{rows}"
98
- ].join(',')
99
-
100
- cmd = build_ffmpeg_command(video_file, output_file, filter_complex)
101
-
102
- if options[:debug]
103
- Utils::OutputFormatter.indent("DEBUG: ffmpeg command:")
104
- Utils::OutputFormatter.indent(cmd)
105
- end
106
-
107
- stdout, stderr, status = Open3.capture3(cmd)
108
-
109
- unless status.success?
110
- raise ProcessingError, "FFmpeg failed: #{stderr}"
111
- end
112
-
113
- Utils::FileHelper.validate_exists!(output_file)
114
- end
115
-
116
- def build_ffmpeg_command(video_file, output_file, filter_complex)
117
- [
118
- 'ffmpeg',
119
- '-i', Utils::PathHelper.quote_path(video_file),
120
- '-filter_complex', Utils::PathHelper.quote_arg(filter_complex),
121
- '-frames:v', '1',
122
- '-y',
123
- Utils::PathHelper.quote_path(output_file),
124
- '-hide_banner',
125
- options[:debug] ? '-loglevel info' : '-loglevel error'
126
- ].join(' ')
127
- end
128
-
129
- def display_spritesheet_results(output_file, file_size, columns, rows, frame_count)
130
- Utils::OutputFormatter.success("Spritesheet created: #{output_file}")
131
- Utils::OutputFormatter.indent("Size: #{Utils::FileHelper.format_size(file_size)}")
132
- Utils::OutputFormatter.note("Metadata embedded: #{columns}×#{rows} grid (#{frame_count} frames)")
133
-
134
- puts "\n 📊 Godot AnimatedSprite2D Settings:"
135
- Utils::OutputFormatter.indent("HFrames = #{columns}")
136
- Utils::OutputFormatter.indent("VFrames = #{rows}\n")
137
- end
138
- end
139
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module RubySpriter
6
+ # Processes video files with FFmpeg
7
+ class VideoProcessor
8
+ attr_reader :options
9
+
10
+ # Filename suffix for background-removed frames
11
+ NO_BACKGROUND_SUFFIX = '_nobg'
12
+
13
+ def initialize(options = {})
14
+ @options = options
15
+ end
16
+
17
+ # Create spritesheet from video file
18
+ # @param video_file [String] Path to video file
19
+ # @param output_file [String] Path to output spritesheet
20
+ # @return [Hash] Processing results
21
+ def create_spritesheet(video_file, output_file)
22
+ Utils::FileHelper.validate_readable!(video_file)
23
+
24
+ Utils::OutputFormatter.header("Video Analysis")
25
+ duration = get_duration(video_file)
26
+ Utils::OutputFormatter.indent("Duration: #{duration.round(2)} seconds\n")
27
+
28
+ columns = options[:columns] || 4
29
+ frame_count = options[:frame_count] || 16
30
+ rows = (frame_count.to_f / columns).ceil
31
+
32
+ Utils::OutputFormatter.header("Creating Spritesheet")
33
+
34
+ temp_file = output_file.sub('.png', '_temp.png')
35
+
36
+ create_with_ffmpeg(video_file, temp_file, duration, columns, rows, frame_count)
37
+
38
+ # Embed metadata
39
+ MetadataManager.embed(
40
+ temp_file,
41
+ output_file,
42
+ columns: columns,
43
+ rows: rows,
44
+ frames: frame_count,
45
+ debug: options[:debug]
46
+ )
47
+
48
+ # Clean up temp file
49
+ File.delete(temp_file) if File.exist?(temp_file)
50
+
51
+ file_size = File.size(output_file)
52
+
53
+ # Display results with Godot instructions
54
+ display_spritesheet_results(output_file, file_size, columns, rows, frame_count)
55
+
56
+ {
57
+ output_file: output_file,
58
+ columns: columns,
59
+ rows: rows,
60
+ frames: frame_count,
61
+ size: file_size
62
+ }
63
+ end
64
+
65
+ # Get video duration in seconds
66
+ # @param video_file [String] Path to video file
67
+ # @return [Float] Duration in seconds
68
+ def get_duration(video_file)
69
+ cmd = [
70
+ 'ffprobe',
71
+ '-v', 'error',
72
+ '-show_entries', 'format=duration',
73
+ '-of', 'default=noprint_wrappers=1:nokey=1',
74
+ Utils::PathHelper.quote_path(video_file)
75
+ ].join(' ')
76
+
77
+ stdout, stderr, status = Open3.capture3(cmd)
78
+
79
+ unless status.success?
80
+ raise ProcessingError, "Could not determine video duration: #{stderr}"
81
+ end
82
+
83
+ stdout.strip.to_f
84
+ end
85
+
86
+ # Process video frames with background removal
87
+ # @param video_path [String] Path to input video file
88
+ # @param output_path [String] Path to output spritesheet
89
+ # @param options [Hash] Processing options
90
+ # @option options [Boolean] :by_frame Process each frame individually
91
+ # @option options [String] :gimp_path Path to GIMP executable
92
+ # @option options [Integer] :columns Number of columns in spritesheet
93
+ # @option options [Integer] :frames Number of frames to extract
94
+ # @option options [Boolean] :keep_temp Keep temporary files
95
+ # @option options [Boolean] :debug Enable debug output
96
+ # @return [Hash] Processing results with :output_file, :columns, :frames, :processing_mode
97
+ def process_with_background_removal(video_path, output_path, options)
98
+ temp_dir = Dir.mktmpdir('ruby_spriter_')
99
+
100
+ begin
101
+ # Extract frames from video
102
+ frame_files = extract_frames(video_path, temp_dir, options)
103
+
104
+ if options[:by_frame]
105
+ # Frame-by-frame processing
106
+ process_frames_individually(frame_files, temp_dir, options)
107
+
108
+ # Assemble spritesheet from processed frames
109
+ processed_frames = frame_files.map { |f| no_background_filename(f) }
110
+ assemble_spritesheet_from_frames(processed_frames, output_path, options.merge(temp_dir: temp_dir))
111
+ else
112
+ # Standard processing: assemble first, then process spritesheet
113
+ assemble_spritesheet_from_frames(frame_files, output_path, options.merge(temp_dir: temp_dir))
114
+
115
+ # Apply background removal to entire spritesheet
116
+ process_image_with_gimp(output_path, output_path, options)
117
+ end
118
+
119
+ # Cell-based cleanup (if --cleanup-cells flag present)
120
+ if options[:cleanup_cells]
121
+ require_relative 'cell_cleanup_processor'
122
+ cell_processor = CellCleanupProcessor.new(options)
123
+ stats = cell_processor.cleanup_cells(output_path, options)
124
+ end
125
+
126
+ # Calculate rows for metadata
127
+ columns = options[:columns] || 4
128
+ frames = options[:frames] || 16
129
+ rows = (frames.to_f / columns).ceil
130
+
131
+ # Embed metadata using temp file pattern (like create_spritesheet does)
132
+ temp_file = output_path.sub('.png', '_temp.png')
133
+ FileUtils.mv(output_path, temp_file)
134
+
135
+ RubySpriter::MetadataManager.embed(
136
+ temp_file,
137
+ output_path,
138
+ columns: columns,
139
+ rows: rows,
140
+ frames: frames,
141
+ debug: options[:debug]
142
+ )
143
+
144
+ # Clean up temp file
145
+ File.delete(temp_file) if File.exist?(temp_file)
146
+
147
+ # Return processing results
148
+ {
149
+ output_file: output_path,
150
+ columns: options[:columns],
151
+ frames: options[:frames],
152
+ processing_mode: options[:by_frame] ? 'by-frame' : 'standard'
153
+ }
154
+
155
+ ensure
156
+ # Cleanup temp directory unless --keep-temp or --debug
157
+ FileUtils.rm_rf(temp_dir) unless options[:keep_temp] || options[:debug]
158
+ end
159
+ end
160
+
161
+ private
162
+
163
+ # Process an image with GIMP and move to expected output location
164
+ # @param input_path [String] Path to input image
165
+ # @param expected_output_path [String] Expected output path
166
+ # @param options [Hash] Processing options including :gimp_path
167
+ # @return [String] Path to processed file
168
+ def process_image_with_gimp(input_path, expected_output_path, options)
169
+ gimp_path = options[:gimp_path]
170
+ gimp_processor = RubySpriter::GimpProcessor.new(gimp_path, options)
171
+ processed_file = gimp_processor.process(input_path)
172
+
173
+ # Move file to expected location if different
174
+ FileUtils.mv(processed_file, expected_output_path) if processed_file != expected_output_path
175
+
176
+ expected_output_path
177
+ end
178
+
179
+ # Generate filename with no-background suffix
180
+ # @param filename [String] Original filename
181
+ # @return [String] Filename with _nobg suffix
182
+ def no_background_filename(filename)
183
+ filename.sub('.png', "#{NO_BACKGROUND_SUFFIX}.png")
184
+ end
185
+
186
+ # Extract individual frames from video file
187
+ # @param video_path [String] Path to input video file
188
+ # @param temp_dir [String] Directory to store extracted frames
189
+ # @param options [Hash] Processing options
190
+ # @option options [Integer] :frames Number of frames to extract (default: 16)
191
+ # @option options [Integer] :max_width Maximum frame width (default: 320)
192
+ # @option options [Boolean] :debug Enable debug output
193
+ # @return [Array<String>] Array of frame filenames (basenames only, not full paths)
194
+ def extract_frames(video_path, temp_dir, options)
195
+ frame_count = options[:frames] || 16
196
+ max_width = options[:max_width] || 320
197
+
198
+ # Get video duration to calculate FPS
199
+ duration = get_duration(video_path)
200
+ fps = (frame_count / duration.to_f).round(6)
201
+
202
+ # Output pattern for frames
203
+ output_pattern = File.join(temp_dir, 'frame_%03d.png')
204
+
205
+ # Build FFmpeg command
206
+ cmd = [
207
+ 'ffmpeg',
208
+ '-i', Utils::PathHelper.quote_path(video_path),
209
+ '-vf', "fps=#{fps},scale=#{max_width}:-1:flags=lanczos",
210
+ '-frames:v', frame_count.to_s,
211
+ Utils::PathHelper.quote_path(output_pattern),
212
+ '-hide_banner',
213
+ options[:debug] ? '-loglevel info' : '-loglevel error'
214
+ ].join(' ')
215
+
216
+ if options[:debug]
217
+ Utils::OutputFormatter.indent("DEBUG: Extracting frames with command:")
218
+ Utils::OutputFormatter.indent(cmd)
219
+ end
220
+
221
+ # Execute FFmpeg
222
+ stdout, stderr, status = Open3.capture3(cmd)
223
+
224
+ unless status.success?
225
+ raise ProcessingError, "Failed to extract frames: #{stderr}"
226
+ end
227
+
228
+ # Return array of frame filenames (basenames only)
229
+ (1..frame_count).map { |i| format('frame_%03d.png', i) }
230
+ end
231
+
232
+
233
+ # Assemble individual frames into a spritesheet
234
+ # @param frame_files [Array<String>] Array of frame filenames (basenames)
235
+ # @param output_path [String] Path to output spritesheet
236
+ # @param options [Hash] Processing options
237
+ # @option options [Integer] :columns Number of columns in grid (default: 4)
238
+ # @option options [String] :temp_dir Temporary directory containing frames
239
+ # @option options [Boolean] :debug Enable debug output
240
+ # @return [void]
241
+ def assemble_spritesheet_from_frames(frame_files, output_path, options)
242
+ columns = options[:columns] || 4
243
+ frame_count = frame_files.length
244
+ rows = (frame_count.to_f / columns).ceil
245
+ temp_dir = options[:temp_dir]
246
+
247
+ # Detect the pattern from the first filename
248
+ # Examples: frame_001.png → frame_%03d.png
249
+ # frame_001_nobg.png → frame_%03d_nobg.png
250
+ first_file = frame_files.first
251
+ pattern = if first_file.include?('_nobg.png')
252
+ 'frame_%03d_nobg.png'
253
+ else
254
+ 'frame_%03d.png'
255
+ end
256
+
257
+ # Input pattern for frames
258
+ input_pattern = File.join(temp_dir, pattern)
259
+
260
+ # Build FFmpeg command
261
+ cmd = [
262
+ 'ffmpeg',
263
+ '-i', Utils::PathHelper.quote_path(input_pattern),
264
+ '-filter_complex', "tile=#{columns}x#{rows}",
265
+ '-frames:v', '1',
266
+ '-y',
267
+ Utils::PathHelper.quote_path(output_path),
268
+ '-hide_banner',
269
+ options[:debug] ? '-loglevel info' : '-loglevel error'
270
+ ].join(' ')
271
+
272
+ if options[:debug]
273
+ Utils::OutputFormatter.indent("DEBUG: Assembling spritesheet with command:")
274
+ Utils::OutputFormatter.indent(cmd)
275
+ end
276
+
277
+ # Execute FFmpeg
278
+ stdout, stderr, status = Open3.capture3(cmd)
279
+
280
+ unless status.success?
281
+ raise ProcessingError, "Failed to assemble spritesheet: #{stderr}"
282
+ end
283
+
284
+ Utils::FileHelper.validate_exists!(output_path)
285
+ end
286
+
287
+ def process_frames_individually(frame_files, temp_dir, options)
288
+ total_frames = frame_files.length
289
+
290
+ frame_files.each_with_index do |frame_file, index|
291
+ frame_number = index + 1
292
+ puts "Processing frame #{frame_number}/#{total_frames}..."
293
+
294
+ # Input and output paths
295
+ input_path = File.join(temp_dir, frame_file)
296
+ output_path = File.join(temp_dir, no_background_filename(frame_file))
297
+
298
+ # Process the frame with GIMP
299
+ process_image_with_gimp(input_path, output_path, options)
300
+ end
301
+ end
302
+
303
+ def create_with_ffmpeg(video_file, output_file, duration, columns, rows, frame_count)
304
+ fps = (frame_count / duration.to_f).round(6)
305
+ max_width = options[:max_width] || 320
306
+
307
+ Utils::OutputFormatter.indent("Layout: #{columns}×#{rows} grid (#{frame_count} frames)")
308
+ Utils::OutputFormatter.indent("Frame rate: #{fps} fps")
309
+ Utils::OutputFormatter.indent("Max frame width: #{max_width}px")
310
+ Utils::OutputFormatter.indent("Building spritesheet...")
311
+
312
+ filter_complex = [
313
+ "fps=#{fps}",
314
+ "scale=#{max_width}:-1:flags=lanczos",
315
+ "tile=#{columns}x#{rows}"
316
+ ].join(',')
317
+
318
+ cmd = build_ffmpeg_command(video_file, output_file, filter_complex)
319
+
320
+ if options[:debug]
321
+ Utils::OutputFormatter.indent("DEBUG: ffmpeg command:")
322
+ Utils::OutputFormatter.indent(cmd)
323
+ end
324
+
325
+ stdout, stderr, status = Open3.capture3(cmd)
326
+
327
+ unless status.success?
328
+ raise ProcessingError, "FFmpeg failed: #{stderr}"
329
+ end
330
+
331
+ Utils::FileHelper.validate_exists!(output_file)
332
+ end
333
+
334
+ def build_ffmpeg_command(video_file, output_file, filter_complex)
335
+ [
336
+ 'ffmpeg',
337
+ '-i', Utils::PathHelper.quote_path(video_file),
338
+ '-filter_complex', Utils::PathHelper.quote_arg(filter_complex),
339
+ '-frames:v', '1',
340
+ '-y',
341
+ Utils::PathHelper.quote_path(output_file),
342
+ '-hide_banner',
343
+ options[:debug] ? '-loglevel info' : '-loglevel error'
344
+ ].join(' ')
345
+ end
346
+
347
+ def display_spritesheet_results(output_file, file_size, columns, rows, frame_count)
348
+ Utils::OutputFormatter.success("Spritesheet created: #{output_file}")
349
+ Utils::OutputFormatter.indent("Size: #{Utils::FileHelper.format_size(file_size)}")
350
+ Utils::OutputFormatter.note("Metadata embedded: #{columns}×#{rows} grid (#{frame_count} frames)")
351
+
352
+ puts "\n 📊 Godot AnimatedSprite2D Settings:"
353
+ Utils::OutputFormatter.indent("HFrames = #{columns}")
354
+ Utils::OutputFormatter.indent("VFrames = #{rows}\n")
355
+ end
356
+ end
357
+ end
data/lib/ruby_spriter.rb CHANGED
@@ -1,34 +1,38 @@
1
- # frozen_string_literal: true
2
-
3
- # Main module for Ruby Spriter
4
- module RubySpriter
5
- class Error < StandardError; end
6
- class DependencyError < Error; end
7
- class ProcessingError < Error; end
8
- class ValidationError < Error; end
9
- end
10
-
11
- # Load version first
12
- require_relative 'ruby_spriter/version'
13
-
14
- # Load utilities (no dependencies)
15
- require_relative 'ruby_spriter/utils/path_helper'
16
- require_relative 'ruby_spriter/utils/file_helper'
17
- require_relative 'ruby_spriter/utils/output_formatter'
18
- require_relative 'ruby_spriter/utils/spritesheet_splitter'
19
-
20
- # Load core components
21
- require_relative 'ruby_spriter/platform'
22
- require_relative 'ruby_spriter/dependency_checker'
23
- require_relative 'ruby_spriter/metadata_manager'
24
-
25
- # Load processors
26
- require_relative 'ruby_spriter/video_processor'
27
- require_relative 'ruby_spriter/gimp_processor'
28
- require_relative 'ruby_spriter/consolidator'
29
- require_relative 'ruby_spriter/compression_manager'
30
- require_relative 'ruby_spriter/batch_processor'
31
-
32
- # Load orchestration
33
- require_relative 'ruby_spriter/processor'
34
- require_relative 'ruby_spriter/cli'
1
+ # frozen_string_literal: true
2
+
3
+ # Main module for Ruby Spriter
4
+ module RubySpriter
5
+ class Error < StandardError; end
6
+ class DependencyError < Error; end
7
+ class ProcessingError < Error; end
8
+ class ValidationError < Error; end
9
+ end
10
+
11
+ # Load version first
12
+ require_relative 'ruby_spriter/version'
13
+
14
+ # Load utilities (no dependencies)
15
+ require_relative 'ruby_spriter/utils/path_helper'
16
+ require_relative 'ruby_spriter/utils/file_helper'
17
+ require_relative 'ruby_spriter/utils/output_formatter'
18
+ require_relative 'ruby_spriter/utils/spritesheet_splitter'
19
+
20
+ # Load core components
21
+ require_relative 'ruby_spriter/platform'
22
+ require_relative 'ruby_spriter/dependency_checker'
23
+ require_relative 'ruby_spriter/metadata_manager'
24
+ require_relative 'ruby_spriter/background_sampler'
25
+ require_relative 'ruby_spriter/threshold_stepper'
26
+ require_relative 'ruby_spriter/ghost_edge_cleaner'
27
+ require_relative 'ruby_spriter/smoke_detector'
28
+
29
+ # Load processors
30
+ require_relative 'ruby_spriter/video_processor'
31
+ require_relative 'ruby_spriter/gimp_processor'
32
+ require_relative 'ruby_spriter/consolidator'
33
+ require_relative 'ruby_spriter/compression_manager'
34
+ require_relative 'ruby_spriter/batch_processor'
35
+
36
+ # Load orchestration
37
+ require_relative 'ruby_spriter/processor'
38
+ require_relative 'ruby_spriter/cli'
data/ruby_spriter.gemspec CHANGED
@@ -1,42 +1,44 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'lib/ruby_spriter/version'
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = 'ruby_spriter'
7
- spec.version = RubySpriter::VERSION
8
- spec.authors = ['scooter-indie']
9
- spec.email = ['scooter-indie@users.noreply.github.com']
10
-
11
- spec.summary = 'MP4 to Spritesheet converter with GIMP image processing'
12
- spec.description = <<~DESC
13
- Ruby Spriter is a cross-platform tool for creating spritesheets from video files
14
- and processing them with GIMP. Features include background removal, scaling,
15
- consolidation, and metadata management.
16
- DESC
17
- spec.homepage = 'https://github.com/scooter-indie/ruby-spriter'
18
- spec.license = 'MIT'
19
- spec.required_ruby_version = '>= 2.7.0'
20
-
21
- spec.metadata['homepage_uri'] = spec.homepage
22
- spec.metadata['source_code_uri'] = 'https://github.com/scooter-indie/ruby-spriter'
23
- spec.metadata['changelog_uri'] = 'https://github.com/scooter-indie/ruby-spriter/blob/main/CHANGELOG.md'
24
-
25
- # Specify which files should be added to the gem when it is released.
26
- spec.files = Dir.glob('{bin,lib,spec}/**/*') + %w[
27
- README.md
28
- CHANGELOG.md
29
- LICENSE
30
- Gemfile
31
- ruby_spriter.gemspec
32
- .rspec
33
- ]
34
-
35
- spec.bindir = 'bin'
36
- spec.executables = ['ruby_spriter']
37
- spec.require_paths = ['lib']
38
-
39
- # Runtime dependencies (none - uses only standard library + external tools)
40
-
41
- # Development dependencies are in Gemfile
42
- end
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/ruby_spriter/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'ruby_spriter'
7
+ spec.version = RubySpriter::VERSION
8
+ spec.authors = ['scooter-indie']
9
+ spec.email = ['scooter-indie@users.noreply.github.com']
10
+
11
+ spec.summary = 'Professional MP4 to Spritesheet converter with advanced GIMP image processing'
12
+ spec.description = <<~DESC
13
+ Ruby Spriter is a cross-platform tool for creating professional spritesheets from video files
14
+ with advanced GIMP image processing. Features include edge-based and inner background removal,
15
+ multi-threshold processing, ghost edge prevention, smoke detection, scaling with multiple
16
+ interpolation methods, sharpening, batch processing, spritesheet consolidation, frame extraction,
17
+ and comprehensive metadata management. Designed for game development workflows with Godot Engine.
18
+ DESC
19
+ spec.homepage = 'https://github.com/scooter-indie/ruby-spriter'
20
+ spec.license = 'MIT'
21
+ spec.required_ruby_version = '>= 2.7.0'
22
+
23
+ spec.metadata['homepage_uri'] = spec.homepage
24
+ spec.metadata['source_code_uri'] = 'https://github.com/scooter-indie/ruby-spriter'
25
+ spec.metadata['changelog_uri'] = 'https://github.com/scooter-indie/ruby-spriter/blob/main/CHANGELOG.md'
26
+
27
+ # Specify which files should be added to the gem when it is released.
28
+ spec.files = Dir.glob('{bin,lib,spec}/**/*') + %w[
29
+ README.md
30
+ CHANGELOG.md
31
+ LICENSE
32
+ Gemfile
33
+ ruby_spriter.gemspec
34
+ .rspec
35
+ ]
36
+
37
+ spec.bindir = 'bin'
38
+ spec.executables = ['ruby_spriter']
39
+ spec.require_paths = ['lib']
40
+
41
+ # Runtime dependencies (none - uses only standard library + external tools)
42
+
43
+ # Development dependencies are in Gemfile
44
+ end