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.
- checksums.yaml +4 -4
- data/.rspec +3 -3
- data/CHANGELOG.md +1035 -524
- data/Gemfile +17 -17
- data/LICENSE +21 -21
- data/README.md +183 -950
- data/bin/ruby_spriter +20 -20
- data/lib/ruby_spriter/background_sampler.rb +140 -0
- data/lib/ruby_spriter/batch_processor.rb +268 -214
- 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 -224
- data/lib/ruby_spriter/ghost_edge_cleaner.rb +164 -0
- data/lib/ruby_spriter/gimp_processor.rb +1188 -1058
- data/lib/ruby_spriter/metadata_manager.rb +117 -116
- data/lib/ruby_spriter/platform.rb +137 -137
- data/lib/ruby_spriter/processor.rb +1230 -891
- 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 -92
- 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
data/bin/ruby_spriter
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
# Add lib directory to load path
|
|
5
|
-
lib = File.expand_path('../lib', __dir__)
|
|
6
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
7
|
-
|
|
8
|
-
require 'ruby_spriter'
|
|
9
|
-
|
|
10
|
-
# Run the CLI
|
|
11
|
-
begin
|
|
12
|
-
RubySpriter::CLI.start(ARGV)
|
|
13
|
-
rescue Interrupt
|
|
14
|
-
puts "\n\n⚠️ Process interrupted by user"
|
|
15
|
-
exit 130
|
|
16
|
-
rescue StandardError => e
|
|
17
|
-
puts "\n❌ ERROR: #{e.message}"
|
|
18
|
-
puts e.backtrace.join("\n") if ENV['DEBUG']
|
|
19
|
-
exit 1
|
|
20
|
-
end
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Add lib directory to load path
|
|
5
|
+
lib = File.expand_path('../lib', __dir__)
|
|
6
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
7
|
+
|
|
8
|
+
require 'ruby_spriter'
|
|
9
|
+
|
|
10
|
+
# Run the CLI
|
|
11
|
+
begin
|
|
12
|
+
RubySpriter::CLI.start(ARGV)
|
|
13
|
+
rescue Interrupt
|
|
14
|
+
puts "\n\n⚠️ Process interrupted by user"
|
|
15
|
+
exit 130
|
|
16
|
+
rescue StandardError => e
|
|
17
|
+
puts "\n❌ ERROR: #{e.message}"
|
|
18
|
+
puts e.backtrace.join("\n") if ENV['DEBUG']
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module RubySpriter
|
|
6
|
+
# BackgroundSampler collects unique background colors from interior image regions
|
|
7
|
+
#
|
|
8
|
+
# Sampling Strategy:
|
|
9
|
+
# - Starts at (sample_offset, sample_offset) to avoid edge compression artifacts
|
|
10
|
+
# - Samples horizontally across the image with calculated intervals
|
|
11
|
+
# - Moves to next row if not enough unique colors found
|
|
12
|
+
# - Uses pixel cache for fast lookups (loads all pixels once)
|
|
13
|
+
class BackgroundSampler
|
|
14
|
+
attr_reader :image_path, :sample_offset, :sample_count, :max_rows
|
|
15
|
+
|
|
16
|
+
def initialize(image_path, sample_offset = 5, sample_count = 10, max_rows = 20)
|
|
17
|
+
@image_path = image_path
|
|
18
|
+
@sample_offset = sample_offset
|
|
19
|
+
@sample_count = sample_count
|
|
20
|
+
@max_rows = max_rows
|
|
21
|
+
@image_width = nil
|
|
22
|
+
@image_height = nil
|
|
23
|
+
@pixel_cache = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Collect unique background colors by sampling interior regions
|
|
27
|
+
def collect_unique_colors
|
|
28
|
+
load_image_dimensions
|
|
29
|
+
load_pixel_cache
|
|
30
|
+
|
|
31
|
+
# Input validation
|
|
32
|
+
if @sample_count < 2
|
|
33
|
+
raise ValidationError, "sample_count must be at least 2"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
usable_width = @image_width - (2 * @sample_offset)
|
|
37
|
+
if usable_width <= 0
|
|
38
|
+
raise ValidationError, "sample_offset (#{@sample_offset}) too large for image width (#{@image_width})"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
unique_colors = []
|
|
42
|
+
y = @sample_offset
|
|
43
|
+
rows_sampled = 0
|
|
44
|
+
|
|
45
|
+
while unique_colors.length < @sample_count && rows_sampled < @max_rows
|
|
46
|
+
# Calculate interval across usable width (excluding offset margins on both sides)
|
|
47
|
+
interval = (@image_width - 2 * @sample_offset).to_f / (@sample_count - 1)
|
|
48
|
+
|
|
49
|
+
# Sample across the width at current y position
|
|
50
|
+
@sample_count.times do |i|
|
|
51
|
+
x = @sample_offset + (i * interval).round
|
|
52
|
+
|
|
53
|
+
# Ensure x is within bounds
|
|
54
|
+
x = x.clamp(@sample_offset, @image_width - @sample_offset - 1)
|
|
55
|
+
|
|
56
|
+
color = sample_pixel(x, y)
|
|
57
|
+
|
|
58
|
+
if color && !color_exists?(unique_colors, color)
|
|
59
|
+
unique_colors << color
|
|
60
|
+
break if unique_colors.length >= @sample_count
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Move to next row
|
|
65
|
+
y += 1
|
|
66
|
+
rows_sampled += 1
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
unique_colors
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def load_image_dimensions
|
|
75
|
+
identify_cmd = Platform.imagemagick_identify_cmd
|
|
76
|
+
cmd = "#{identify_cmd} -format \"%w %h\" #{Utils::PathHelper.quote_path(@image_path)}"
|
|
77
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
78
|
+
|
|
79
|
+
unless status.success?
|
|
80
|
+
raise ProcessingError, "Failed to get image dimensions: #{stderr}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
@image_width, @image_height = stdout.strip.split.map(&:to_i)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def load_pixel_cache
|
|
87
|
+
return if @pixel_cache
|
|
88
|
+
|
|
89
|
+
convert_cmd = Platform.imagemagick_convert_cmd
|
|
90
|
+
cmd = "#{convert_cmd} #{Utils::PathHelper.quote_path(@image_path)} txt:-"
|
|
91
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
92
|
+
|
|
93
|
+
unless status.success?
|
|
94
|
+
raise ProcessingError, "Failed to load pixel cache: #{stderr}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
@pixel_cache = {}
|
|
98
|
+
|
|
99
|
+
stdout.each_line do |line|
|
|
100
|
+
next if line.start_with?('#')
|
|
101
|
+
|
|
102
|
+
if line =~ /^(\d+),(\d+):\s+\((\d+),(\d+),(\d+)/
|
|
103
|
+
x = $1.to_i
|
|
104
|
+
y = $2.to_i
|
|
105
|
+
r = $3.to_i
|
|
106
|
+
g = $4.to_i
|
|
107
|
+
b = $5.to_i
|
|
108
|
+
|
|
109
|
+
@pixel_cache[[x, y]] = { r: r, g: g, b: b }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def sample_pixel(x, y)
|
|
115
|
+
# Fallback to direct ImageMagick call if cache not loaded (for testing)
|
|
116
|
+
return @pixel_cache[[x, y]] if @pixel_cache
|
|
117
|
+
|
|
118
|
+
convert_cmd = Platform.imagemagick_convert_cmd
|
|
119
|
+
cmd = "#{convert_cmd} #{Utils::PathHelper.quote_path(@image_path)} -format \"%[pixel:p{#{x},#{y}}]\" info:"
|
|
120
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
121
|
+
|
|
122
|
+
return nil unless status.success?
|
|
123
|
+
|
|
124
|
+
# Parse output like "srgb(255,255,255)", "srgba(255,255,255,1.0)", or "gray(255)"
|
|
125
|
+
if stdout =~ /srgba?\((\d+),(\d+),(\d+)/
|
|
126
|
+
{ r: $1.to_i, g: $2.to_i, b: $3.to_i }
|
|
127
|
+
elsif stdout =~ /gray\((\d+)\)/
|
|
128
|
+
# Convert grayscale to RGB
|
|
129
|
+
gray_value = $1.to_i
|
|
130
|
+
{ r: gray_value, g: gray_value, b: gray_value }
|
|
131
|
+
else
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def color_exists?(colors, new_color)
|
|
137
|
+
colors.any? { |c| c[:r] == new_color[:r] && c[:g] == new_color[:g] && c[:b] == new_color[:b] }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -1,214 +1,268 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'fileutils'
|
|
4
|
-
|
|
5
|
-
module RubySpriter
|
|
6
|
-
# Processes multiple videos in batch mode
|
|
7
|
-
class BatchProcessor
|
|
8
|
-
attr_reader :options
|
|
9
|
-
|
|
10
|
-
def initialize(options = {})
|
|
11
|
-
@options = options
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module RubySpriter
|
|
6
|
+
# Processes multiple videos in batch mode
|
|
7
|
+
class BatchProcessor
|
|
8
|
+
attr_reader :options
|
|
9
|
+
|
|
10
|
+
def initialize(options = {})
|
|
11
|
+
@options = options
|
|
12
|
+
@gimp_path = nil
|
|
13
|
+
@gimp_version = nil
|
|
14
|
+
validate_directory!
|
|
15
|
+
setup_dependencies if needs_dependency_setup?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Find all MP4 files in the directory
|
|
19
|
+
# @return [Array<String>] List of MP4 file paths
|
|
20
|
+
def find_videos
|
|
21
|
+
pattern = File.join(options[:dir], '*.mp4')
|
|
22
|
+
videos = Dir.glob(pattern)
|
|
23
|
+
|
|
24
|
+
if videos.empty?
|
|
25
|
+
raise ValidationError, "No MP4 files found in directory: #{options[:dir]}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if options[:debug]
|
|
29
|
+
Utils::OutputFormatter.note("Found #{videos.length} video(s) to process")
|
|
30
|
+
videos.each { |v| Utils::OutputFormatter.indent(File.basename(v)) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
videos
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Process all videos in the directory
|
|
37
|
+
# @return [Hash] Processing results with outputs and errors
|
|
38
|
+
def process
|
|
39
|
+
videos = find_videos
|
|
40
|
+
output_dir = determine_output_directory
|
|
41
|
+
ensure_output_directory_exists(output_dir)
|
|
42
|
+
|
|
43
|
+
results = {
|
|
44
|
+
processed_count: 0,
|
|
45
|
+
outputs: [],
|
|
46
|
+
errors: []
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Utils::OutputFormatter.header("Batch Processing: #{videos.length} video(s)")
|
|
50
|
+
|
|
51
|
+
videos.each_with_index do |video, index|
|
|
52
|
+
begin
|
|
53
|
+
puts "\nProcessing [#{index + 1}/#{videos.length}]: #{File.basename(video)}"
|
|
54
|
+
|
|
55
|
+
output_file = determine_output_file(video, output_dir)
|
|
56
|
+
output_file = Utils::FileHelper.ensure_unique_output(output_file, overwrite: options[:overwrite])
|
|
57
|
+
|
|
58
|
+
# Process video
|
|
59
|
+
video_result = process_video(video, output_file)
|
|
60
|
+
|
|
61
|
+
# Apply max compression if requested
|
|
62
|
+
if options[:max_compress] && video_result[:output_file]
|
|
63
|
+
video_result[:output_file] = apply_compression(video_result[:output_file])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
results[:outputs] << video_result[:output_file]
|
|
67
|
+
results[:processed_count] += 1
|
|
68
|
+
|
|
69
|
+
Utils::OutputFormatter.success("Output: #{File.basename(video_result[:output_file])}")
|
|
70
|
+
rescue StandardError => e
|
|
71
|
+
error_msg = "#{File.basename(video)}: #{e.message}"
|
|
72
|
+
results[:errors] << error_msg
|
|
73
|
+
Utils::OutputFormatter.error(error_msg)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Consolidate if requested
|
|
78
|
+
if options[:batch_consolidate] && results[:outputs].any?
|
|
79
|
+
consolidated = consolidate_results(results[:outputs])
|
|
80
|
+
results[:consolidated] = consolidated if consolidated
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
display_summary(results)
|
|
84
|
+
|
|
85
|
+
results
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Consolidate all resulting spritesheets
|
|
89
|
+
# @param outputs [Array<String>] List of spritesheet file paths
|
|
90
|
+
# @return [Hash, nil] Consolidation result or nil if not requested
|
|
91
|
+
def consolidate_results(outputs)
|
|
92
|
+
return nil unless options[:batch_consolidate]
|
|
93
|
+
return nil if outputs.empty?
|
|
94
|
+
|
|
95
|
+
Utils::OutputFormatter.header("Consolidating #{outputs.length} spritesheets")
|
|
96
|
+
|
|
97
|
+
output_dir = options[:outputdir] || options[:dir]
|
|
98
|
+
consolidated_file = File.join(output_dir, 'batch_consolidated_spritesheet.png')
|
|
99
|
+
consolidated_file = Utils::FileHelper.ensure_unique_output(consolidated_file, overwrite: options[:overwrite])
|
|
100
|
+
|
|
101
|
+
consolidator = Consolidator.new(options)
|
|
102
|
+
result = consolidator.consolidate(outputs, consolidated_file)
|
|
103
|
+
|
|
104
|
+
Utils::OutputFormatter.success("Consolidated output: #{File.basename(consolidated_file)}")
|
|
105
|
+
|
|
106
|
+
result
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def validate_directory!
|
|
112
|
+
dir = options[:dir]
|
|
113
|
+
|
|
114
|
+
raise ValidationError, "Must specify --dir for batch processing" unless dir
|
|
115
|
+
raise ValidationError, "Directory not found: #{dir}" unless File.directory?(dir)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Check if using frame-by-frame background removal mode
|
|
119
|
+
# @return [Boolean] true if both --by-frame and --remove-bg flags are set
|
|
120
|
+
def using_frame_by_frame_background_removal?
|
|
121
|
+
options[:by_frame] && options[:remove_bg]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Normalize video processing result to standard format
|
|
125
|
+
# @param result [Hash] Result from process_with_background_removal
|
|
126
|
+
# @return [Hash] Normalized result with :output_file, :columns, :rows, :frames
|
|
127
|
+
def normalize_video_result_format(result)
|
|
128
|
+
{
|
|
129
|
+
output_file: result[:output_file],
|
|
130
|
+
columns: result[:columns],
|
|
131
|
+
rows: (result[:frames].to_f / result[:columns]).ceil,
|
|
132
|
+
frames: result[:frames]
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Check if we need to setup dependencies (GIMP required)
|
|
137
|
+
# @return [Boolean] true if any operation requires GIMP
|
|
138
|
+
def needs_dependency_setup?
|
|
139
|
+
options[:by_frame] || options[:scale_percent] || options[:remove_bg] || options[:sharpen]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Setup dependencies by checking for required tools
|
|
143
|
+
# Caches GIMP path and version as instance variables
|
|
144
|
+
def setup_dependencies
|
|
145
|
+
checker = DependencyChecker.new(verbose: false)
|
|
146
|
+
results = checker.check_all
|
|
147
|
+
@gimp_path = checker.gimp_path
|
|
148
|
+
@gimp_version = checker.gimp_version
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def determine_output_directory
|
|
152
|
+
options[:outputdir] || options[:dir]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def ensure_output_directory_exists(dir)
|
|
156
|
+
return if File.directory?(dir)
|
|
157
|
+
|
|
158
|
+
if options[:debug]
|
|
159
|
+
Utils::OutputFormatter.note("Creating output directory: #{dir}")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
FileUtils.mkdir_p(dir)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def determine_output_file(video_file, output_dir)
|
|
166
|
+
basename = File.basename(video_file, '.*')
|
|
167
|
+
File.join(output_dir, "#{basename}_spritesheet.png")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def process_video(video_file, output_file)
|
|
171
|
+
# Check if we need frame-by-frame background removal
|
|
172
|
+
if using_frame_by_frame_background_removal?
|
|
173
|
+
# Frame-by-frame processing with background removal
|
|
174
|
+
unless @gimp_path
|
|
175
|
+
raise DependencyError, "GIMP not found but required for --by-frame processing"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Pass gimp_path through options
|
|
179
|
+
video_options = options.merge(gimp_path: @gimp_path)
|
|
180
|
+
video_processor = VideoProcessor.new(video_options)
|
|
181
|
+
|
|
182
|
+
result = video_processor.process_with_background_removal(
|
|
183
|
+
video_file,
|
|
184
|
+
output_file,
|
|
185
|
+
video_options
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Normalize result format to match create_spritesheet
|
|
189
|
+
result = normalize_video_result_format(result)
|
|
190
|
+
|
|
191
|
+
working_file = result[:output_file]
|
|
192
|
+
else
|
|
193
|
+
# Standard video processing
|
|
194
|
+
video_processor = VideoProcessor.new(options)
|
|
195
|
+
result = video_processor.create_spritesheet(video_file, output_file)
|
|
196
|
+
working_file = result[:output_file]
|
|
197
|
+
|
|
198
|
+
# Apply GIMP processing if requested (only for non-by-frame mode)
|
|
199
|
+
if needs_gimp_processing?
|
|
200
|
+
working_file = process_with_gimp(working_file, result)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Update result with final file
|
|
205
|
+
result[:output_file] = working_file
|
|
206
|
+
result
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def needs_gimp_processing?
|
|
210
|
+
options[:scale_percent] || options[:remove_bg] || options[:sharpen]
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def process_with_gimp(input_file, video_result)
|
|
214
|
+
# Use cached GIMP path and version from initialization
|
|
215
|
+
unless @gimp_path
|
|
216
|
+
raise DependencyError, "GIMP not found but required for processing"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
gimp_options = options.merge(gimp_version: @gimp_version)
|
|
220
|
+
gimp_processor = GimpProcessor.new(@gimp_path, gimp_options)
|
|
221
|
+
output_file = gimp_processor.process(input_file)
|
|
222
|
+
|
|
223
|
+
# Clean up intermediate file if different
|
|
224
|
+
if output_file != input_file && File.exist?(input_file)
|
|
225
|
+
File.delete(input_file) unless options[:keep_temp]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
output_file
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def apply_compression(file)
|
|
232
|
+
Utils::OutputFormatter.indent("Applying maximum compression...")
|
|
233
|
+
|
|
234
|
+
temp_file = file.gsub('.png', '_temp.png')
|
|
235
|
+
CompressionManager.compress_with_metadata(file, temp_file)
|
|
236
|
+
|
|
237
|
+
# Show compression stats
|
|
238
|
+
if options[:debug]
|
|
239
|
+
stats = CompressionManager.compression_stats(file, temp_file)
|
|
240
|
+
Utils::OutputFormatter.indent("Saved #{Utils::FileHelper.format_size(stats[:saved_bytes])} (#{stats[:reduction_percent].round(1)}% reduction)")
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Replace original with compressed
|
|
244
|
+
FileUtils.mv(temp_file, file)
|
|
245
|
+
|
|
246
|
+
file
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def display_summary(results)
|
|
250
|
+
Utils::OutputFormatter.header("Batch Processing Summary")
|
|
251
|
+
|
|
252
|
+
puts "Total videos: #{results[:processed_count] + results[:errors].length}"
|
|
253
|
+
Utils::OutputFormatter.success("Successfully processed: #{results[:processed_count]}")
|
|
254
|
+
|
|
255
|
+
if results[:errors].any?
|
|
256
|
+
Utils::OutputFormatter.error("Failed: #{results[:errors].length}")
|
|
257
|
+
results[:errors].each do |error|
|
|
258
|
+
Utils::OutputFormatter.indent("- #{error}")
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
if results[:consolidated]
|
|
263
|
+
puts "\nConsolidated spritesheet:"
|
|
264
|
+
Utils::OutputFormatter.indent(results[:consolidated][:output_file])
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|