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.
- checksums.yaml +4 -4
- data/.rspec +3 -3
- data/CHANGELOG.md +1035 -405
- data/Gemfile +17 -17
- data/LICENSE +21 -21
- data/README.md +183 -902
- data/bin/ruby_spriter +20 -20
- data/lib/ruby_spriter/background_sampler.rb +140 -0
- data/lib/ruby_spriter/batch_processor.rb +268 -212
- data/lib/ruby_spriter/cell_cleanup_config.rb +21 -0
- data/lib/ruby_spriter/cell_cleanup_gimp_script.rb +123 -0
- data/lib/ruby_spriter/cell_cleanup_processor.rb +230 -0
- data/lib/ruby_spriter/cli.rb +676 -612
- data/lib/ruby_spriter/compression_manager.rb +101 -101
- data/lib/ruby_spriter/consolidator.rb +179 -179
- data/lib/ruby_spriter/dependency_checker.rb +224 -174
- data/lib/ruby_spriter/ghost_edge_cleaner.rb +164 -0
- data/lib/ruby_spriter/gimp_processor.rb +1188 -667
- data/lib/ruby_spriter/metadata_manager.rb +117 -116
- data/lib/ruby_spriter/platform.rb +137 -82
- data/lib/ruby_spriter/processor.rb +1230 -886
- data/lib/ruby_spriter/smoke_detector.rb +223 -0
- data/lib/ruby_spriter/threshold_stepper.rb +227 -0
- data/lib/ruby_spriter/utils/file_helper.rb +82 -82
- data/lib/ruby_spriter/utils/image_helper.rb +16 -0
- data/lib/ruby_spriter/utils/output_formatter.rb +65 -65
- data/lib/ruby_spriter/utils/path_helper.rb +59 -59
- data/lib/ruby_spriter/utils/spritesheet_splitter.rb +86 -86
- data/lib/ruby_spriter/version.rb +6 -7
- data/lib/ruby_spriter/video_processor.rb +357 -139
- data/lib/ruby_spriter.rb +38 -34
- data/ruby_spriter.gemspec +44 -42
- data/spec/fixtures/centered_sprite_with_inner_bg.png +0 -0
- data/spec/fixtures/centered_sprite_with_inner_bg_inner_removed.png +0 -0
- data/spec/fixtures/centered_sprite_with_inner_bg_threshold_stepped.png +0 -0
- data/spec/fixtures/complex_background_sprite.png +0 -0
- data/spec/fixtures/death - bloodborne rekconing.mp4 +0 -0
- data/spec/fixtures/death - bloodborne rekconing_spritesheet-nobg-global.png +0 -0
- data/spec/fixtures/death - bloodborne rekconing_spritesheet.png +0 -0
- data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185027_431-nobg-global.png +0 -0
- data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185027_431.png +0 -0
- data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185125_738-nobg-global.png +0 -0
- data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185125_738.png +0 -0
- data/spec/fixtures/has_inner_bg.png +0 -0
- data/spec/fixtures/has_small_inner_bg.png +0 -0
- data/spec/fixtures/smoke_effect_sprite.png +0 -0
- data/spec/fixtures/spritesheet_with_metadata.png +0 -0
- data/spec/fixtures/test_sprite.png +0 -0
- data/spec/fixtures/test_sprite_smoke_processed.png +0 -0
- data/spec/fixtures/test_video_spritesheet.png +0 -0
- data/spec/fixtures/transparent_bg_sprite.png +0 -0
- data/spec/fixtures/walk_north_sprite-sheet.png +0 -0
- data/spec/integration/no_fuzzy_mode_spec.rb +100 -0
- data/spec/ruby_spriter/batch_processor_spec.rb +509 -200
- data/spec/ruby_spriter/cli_spec.rb +2026 -1892
- data/spec/ruby_spriter/compression_manager_spec.rb +157 -157
- data/spec/ruby_spriter/consolidator_spec.rb +538 -538
- data/spec/ruby_spriter/gimp_processor_spec.rb +523 -425
- data/spec/ruby_spriter/platform_spec.rb +92 -82
- data/spec/ruby_spriter/processor_spec.rb +911 -735
- data/spec/ruby_spriter/utils/file_helper_spec.rb +150 -150
- data/spec/ruby_spriter/utils/path_helper_spec.rb +78 -78
- data/spec/ruby_spriter/utils/spritesheet_splitter_spec.rb +104 -104
- data/spec/ruby_spriter/video_processor_spec.rb +346 -29
- data/spec/spec_helper.rb +41 -41
- data/spec/tmp/cli_test_output.png +0 -0
- data/spec/tmp/cli_test_output_20251105_114647_395.png +0 -0
- data/spec/tmp/combined_test.png +0 -0
- data/spec/tmp/compat_test.png +0 -0
- data/spec/tmp/compat_test_20251030_174233_361.png +0 -0
- data/spec/tmp/final_all_features.png +0 -0
- data/spec/tmp/final_test_all_features.png +0 -0
- data/spec/tmp/full_pipeline_test.png +0 -0
- data/spec/tmp/inner_test.png +0 -0
- data/spec/tmp/integration_test.png +0 -0
- data/spec/tmp/validation_test.png +0 -0
- data/spec/unit/background_sampler_spec.rb +132 -0
- data/spec/unit/cell_cleanup_config_spec.rb +32 -0
- data/spec/unit/cell_cleanup_gimp_script_spec.rb +59 -0
- data/spec/unit/cell_cleanup_processor_spec.rb +261 -0
- data/spec/unit/ghost_edge_cleaner_spec.rb +223 -0
- data/spec/unit/gimp_processor_single_point_selection_spec.rb +81 -0
- data/spec/unit/smoke_detector_spec.rb +246 -0
- data/spec/unit/threshold_stepper_spec.rb +195 -0
- 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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
'
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
26
|
-
require_relative 'ruby_spriter/
|
|
27
|
-
require_relative 'ruby_spriter/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
require_relative 'ruby_spriter/
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
require_relative 'ruby_spriter/
|
|
34
|
-
require_relative 'ruby_spriter/
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
spec.
|
|
20
|
-
|
|
21
|
-
spec.
|
|
22
|
-
|
|
23
|
-
spec.metadata['
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
spec.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
#
|
|
42
|
-
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|