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,200 +1,509 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'spec_helper'
|
|
4
|
-
|
|
5
|
-
RSpec.describe RubySpriter::BatchProcessor do
|
|
6
|
-
let(:test_dir) { 'E:/test/videos' }
|
|
7
|
-
let(:output_dir) { 'E:/test/output' }
|
|
8
|
-
let(:video1) { File.join(test_dir, 'video1.mp4') }
|
|
9
|
-
let(:video2) { File.join(test_dir, 'video2.mp4') }
|
|
10
|
-
let(:video3) { File.join(test_dir, 'video3.mp4') }
|
|
11
|
-
|
|
12
|
-
let(:options) do
|
|
13
|
-
{
|
|
14
|
-
batch: true,
|
|
15
|
-
dir: test_dir,
|
|
16
|
-
columns: 4,
|
|
17
|
-
frame_count: 16,
|
|
18
|
-
max_width: 320,
|
|
19
|
-
overwrite: false,
|
|
20
|
-
debug: false
|
|
21
|
-
}
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
before do
|
|
25
|
-
# Mock file system
|
|
26
|
-
allow(Dir).to receive(:exist?).and_return(true)
|
|
27
|
-
allow(File).to receive(:exist?).and_return(true)
|
|
28
|
-
allow(File).to receive(:directory?).with(test_dir).and_return(true)
|
|
29
|
-
allow(Dir).to receive(:glob).with(File.join(test_dir, '*.mp4')).and_return([video1, video2, video3])
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
describe '#initialize' do
|
|
33
|
-
it 'raises error if directory does not exist' do
|
|
34
|
-
allow(File).to receive(:directory?).with(test_dir).and_return(false)
|
|
35
|
-
|
|
36
|
-
expect {
|
|
37
|
-
described_class.new(options)
|
|
38
|
-
}.to raise_error(RubySpriter::ValidationError, /Directory not found/)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
it 'initializes with valid options' do
|
|
42
|
-
processor = described_class.new(options)
|
|
43
|
-
expect(processor.options).to eq(options)
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
describe '#find_videos' do
|
|
48
|
-
it 'finds all MP4 files in directory' do
|
|
49
|
-
processor = described_class.new(options)
|
|
50
|
-
videos = processor.find_videos
|
|
51
|
-
|
|
52
|
-
expect(videos).to eq([video1, video2, video3])
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
it 'raises error if no videos found' do
|
|
56
|
-
allow(Dir).to receive(:glob).with(File.join(test_dir, '*.mp4')).and_return([])
|
|
57
|
-
|
|
58
|
-
processor = described_class.new(options)
|
|
59
|
-
|
|
60
|
-
expect {
|
|
61
|
-
processor.find_videos
|
|
62
|
-
}.to raise_error(RubySpriter::ValidationError, /No MP4 files found/)
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
describe '#process' do
|
|
67
|
-
let(:video_processor_mock) { instance_double(RubySpriter::VideoProcessor) }
|
|
68
|
-
|
|
69
|
-
before do
|
|
70
|
-
allow(RubySpriter::VideoProcessor).to receive(:new).and_return(video_processor_mock)
|
|
71
|
-
allow(video_processor_mock).to receive(:create_spritesheet).and_return(
|
|
72
|
-
{ output_file: 'output.png', columns: 4, rows: 4, frames: 16 }
|
|
73
|
-
)
|
|
74
|
-
allow(File).to receive(:exist?).and_return(false) # No existing files
|
|
75
|
-
allow(RubySpriter::Utils::FileHelper).to receive(:spritesheet_filename) do |video|
|
|
76
|
-
video.gsub('.mp4', '_spritesheet.png')
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
it 'processes all videos in directory' do
|
|
81
|
-
processor = described_class.new(options)
|
|
82
|
-
|
|
83
|
-
expect(video_processor_mock).to receive(:create_spritesheet).exactly(3).times
|
|
84
|
-
|
|
85
|
-
results = processor.process
|
|
86
|
-
|
|
87
|
-
expect(results[:processed_count]).to eq(3)
|
|
88
|
-
expect(results[:outputs].length).to eq(3)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
it 'outputs to same directory by default' do
|
|
92
|
-
processor = described_class.new(options)
|
|
93
|
-
|
|
94
|
-
expected_output1 = File.join(test_dir, 'video1_spritesheet.png')
|
|
95
|
-
expect(video_processor_mock).to receive(:create_spritesheet).with(video1, expected_output1)
|
|
96
|
-
|
|
97
|
-
processor.process
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
it 'outputs to specified outputdir when provided' do
|
|
101
|
-
options_with_output = options.merge(outputdir: output_dir)
|
|
102
|
-
processor = described_class.new(options_with_output)
|
|
103
|
-
|
|
104
|
-
allow(File).to receive(:directory?).with(output_dir).and_return(true)
|
|
105
|
-
|
|
106
|
-
expected_output1 = File.join(output_dir, 'video1_spritesheet.png')
|
|
107
|
-
expect(video_processor_mock).to receive(:create_spritesheet).with(video1, expected_output1)
|
|
108
|
-
|
|
109
|
-
processor.process
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
it 'creates output directory if it does not exist' do
|
|
113
|
-
options_with_output = options.merge(outputdir: output_dir)
|
|
114
|
-
processor = described_class.new(options_with_output)
|
|
115
|
-
|
|
116
|
-
allow(File).to receive(:directory?).with(output_dir).and_return(false)
|
|
117
|
-
expect(FileUtils).to receive(:mkdir_p).with(output_dir)
|
|
118
|
-
|
|
119
|
-
processor.process
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
it 'enforces unique filenames unless overwrite is true' do
|
|
123
|
-
allow(File).to receive(:exist?).with(/video1_spritesheet\.png/).and_return(true)
|
|
124
|
-
allow(RubySpriter::Utils::FileHelper).to receive(:ensure_unique_output).and_call_original
|
|
125
|
-
allow(RubySpriter::Utils::FileHelper).to receive(:unique_filename).and_return('video1_spritesheet_20251024_123456_789.png')
|
|
126
|
-
|
|
127
|
-
processor = described_class.new(options)
|
|
128
|
-
|
|
129
|
-
expect(RubySpriter::Utils::FileHelper).to receive(:ensure_unique_output).at_least(:once)
|
|
130
|
-
|
|
131
|
-
processor.process
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
it 'continues processing after error in one video' do
|
|
135
|
-
processor = described_class.new(options)
|
|
136
|
-
|
|
137
|
-
allow(video_processor_mock).to receive(:create_spritesheet).and_raise(RubySpriter::ProcessingError, 'Test error')
|
|
138
|
-
|
|
139
|
-
results = processor.process
|
|
140
|
-
|
|
141
|
-
expect(results[:processed_count]).to eq(0)
|
|
142
|
-
expect(results[:errors].length).to eq(3)
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
describe '
|
|
147
|
-
let(:
|
|
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
|
-
allow(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RubySpriter::BatchProcessor do
|
|
6
|
+
let(:test_dir) { 'E:/test/videos' }
|
|
7
|
+
let(:output_dir) { 'E:/test/output' }
|
|
8
|
+
let(:video1) { File.join(test_dir, 'video1.mp4') }
|
|
9
|
+
let(:video2) { File.join(test_dir, 'video2.mp4') }
|
|
10
|
+
let(:video3) { File.join(test_dir, 'video3.mp4') }
|
|
11
|
+
|
|
12
|
+
let(:options) do
|
|
13
|
+
{
|
|
14
|
+
batch: true,
|
|
15
|
+
dir: test_dir,
|
|
16
|
+
columns: 4,
|
|
17
|
+
frame_count: 16,
|
|
18
|
+
max_width: 320,
|
|
19
|
+
overwrite: false,
|
|
20
|
+
debug: false
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
before do
|
|
25
|
+
# Mock file system
|
|
26
|
+
allow(Dir).to receive(:exist?).and_return(true)
|
|
27
|
+
allow(File).to receive(:exist?).and_return(true)
|
|
28
|
+
allow(File).to receive(:directory?).with(test_dir).and_return(true)
|
|
29
|
+
allow(Dir).to receive(:glob).with(File.join(test_dir, '*.mp4')).and_return([video1, video2, video3])
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe '#initialize' do
|
|
33
|
+
it 'raises error if directory does not exist' do
|
|
34
|
+
allow(File).to receive(:directory?).with(test_dir).and_return(false)
|
|
35
|
+
|
|
36
|
+
expect {
|
|
37
|
+
described_class.new(options)
|
|
38
|
+
}.to raise_error(RubySpriter::ValidationError, /Directory not found/)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'initializes with valid options' do
|
|
42
|
+
processor = described_class.new(options)
|
|
43
|
+
expect(processor.options).to eq(options)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe '#find_videos' do
|
|
48
|
+
it 'finds all MP4 files in directory' do
|
|
49
|
+
processor = described_class.new(options)
|
|
50
|
+
videos = processor.find_videos
|
|
51
|
+
|
|
52
|
+
expect(videos).to eq([video1, video2, video3])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'raises error if no videos found' do
|
|
56
|
+
allow(Dir).to receive(:glob).with(File.join(test_dir, '*.mp4')).and_return([])
|
|
57
|
+
|
|
58
|
+
processor = described_class.new(options)
|
|
59
|
+
|
|
60
|
+
expect {
|
|
61
|
+
processor.find_videos
|
|
62
|
+
}.to raise_error(RubySpriter::ValidationError, /No MP4 files found/)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe '#process' do
|
|
67
|
+
let(:video_processor_mock) { instance_double(RubySpriter::VideoProcessor) }
|
|
68
|
+
|
|
69
|
+
before do
|
|
70
|
+
allow(RubySpriter::VideoProcessor).to receive(:new).and_return(video_processor_mock)
|
|
71
|
+
allow(video_processor_mock).to receive(:create_spritesheet).and_return(
|
|
72
|
+
{ output_file: 'output.png', columns: 4, rows: 4, frames: 16 }
|
|
73
|
+
)
|
|
74
|
+
allow(File).to receive(:exist?).and_return(false) # No existing files
|
|
75
|
+
allow(RubySpriter::Utils::FileHelper).to receive(:spritesheet_filename) do |video|
|
|
76
|
+
video.gsub('.mp4', '_spritesheet.png')
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'processes all videos in directory' do
|
|
81
|
+
processor = described_class.new(options)
|
|
82
|
+
|
|
83
|
+
expect(video_processor_mock).to receive(:create_spritesheet).exactly(3).times
|
|
84
|
+
|
|
85
|
+
results = processor.process
|
|
86
|
+
|
|
87
|
+
expect(results[:processed_count]).to eq(3)
|
|
88
|
+
expect(results[:outputs].length).to eq(3)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'outputs to same directory by default' do
|
|
92
|
+
processor = described_class.new(options)
|
|
93
|
+
|
|
94
|
+
expected_output1 = File.join(test_dir, 'video1_spritesheet.png')
|
|
95
|
+
expect(video_processor_mock).to receive(:create_spritesheet).with(video1, expected_output1)
|
|
96
|
+
|
|
97
|
+
processor.process
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'outputs to specified outputdir when provided' do
|
|
101
|
+
options_with_output = options.merge(outputdir: output_dir)
|
|
102
|
+
processor = described_class.new(options_with_output)
|
|
103
|
+
|
|
104
|
+
allow(File).to receive(:directory?).with(output_dir).and_return(true)
|
|
105
|
+
|
|
106
|
+
expected_output1 = File.join(output_dir, 'video1_spritesheet.png')
|
|
107
|
+
expect(video_processor_mock).to receive(:create_spritesheet).with(video1, expected_output1)
|
|
108
|
+
|
|
109
|
+
processor.process
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'creates output directory if it does not exist' do
|
|
113
|
+
options_with_output = options.merge(outputdir: output_dir)
|
|
114
|
+
processor = described_class.new(options_with_output)
|
|
115
|
+
|
|
116
|
+
allow(File).to receive(:directory?).with(output_dir).and_return(false)
|
|
117
|
+
expect(FileUtils).to receive(:mkdir_p).with(output_dir)
|
|
118
|
+
|
|
119
|
+
processor.process
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'enforces unique filenames unless overwrite is true' do
|
|
123
|
+
allow(File).to receive(:exist?).with(/video1_spritesheet\.png/).and_return(true)
|
|
124
|
+
allow(RubySpriter::Utils::FileHelper).to receive(:ensure_unique_output).and_call_original
|
|
125
|
+
allow(RubySpriter::Utils::FileHelper).to receive(:unique_filename).and_return('video1_spritesheet_20251024_123456_789.png')
|
|
126
|
+
|
|
127
|
+
processor = described_class.new(options)
|
|
128
|
+
|
|
129
|
+
expect(RubySpriter::Utils::FileHelper).to receive(:ensure_unique_output).at_least(:once)
|
|
130
|
+
|
|
131
|
+
processor.process
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'continues processing after error in one video' do
|
|
135
|
+
processor = described_class.new(options)
|
|
136
|
+
|
|
137
|
+
allow(video_processor_mock).to receive(:create_spritesheet).and_raise(RubySpriter::ProcessingError, 'Test error')
|
|
138
|
+
|
|
139
|
+
results = processor.process
|
|
140
|
+
|
|
141
|
+
expect(results[:processed_count]).to eq(0)
|
|
142
|
+
expect(results[:errors].length).to eq(3)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
describe 'batch processing with --by-frame flag' do
|
|
147
|
+
let(:options) do
|
|
148
|
+
{
|
|
149
|
+
batch: true,
|
|
150
|
+
dir: test_dir,
|
|
151
|
+
remove_bg: true,
|
|
152
|
+
by_frame: true,
|
|
153
|
+
frame_count: 4,
|
|
154
|
+
columns: 2
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
let(:batch_processor) { described_class.new(options) }
|
|
159
|
+
|
|
160
|
+
before do
|
|
161
|
+
# Mock directory with video files
|
|
162
|
+
allow(Dir).to receive(:glob).with(File.join(test_dir, '*.mp4')).and_return(['test_videos/video1.mp4', 'test_videos/video2.mp4'])
|
|
163
|
+
allow(File).to receive(:directory?).and_return(true)
|
|
164
|
+
|
|
165
|
+
# Mock file validation
|
|
166
|
+
allow(RubySpriter::Utils::FileHelper).to receive(:validate_readable!)
|
|
167
|
+
|
|
168
|
+
# Mock DependencyChecker for GIMP path
|
|
169
|
+
dependency_checker = instance_double(RubySpriter::DependencyChecker)
|
|
170
|
+
allow(RubySpriter::DependencyChecker).to receive(:new).and_return(dependency_checker)
|
|
171
|
+
allow(dependency_checker).to receive(:check_all).and_return({
|
|
172
|
+
ffmpeg: { available: true },
|
|
173
|
+
ffprobe: { available: true },
|
|
174
|
+
imagemagick: { available: true },
|
|
175
|
+
gimp: { available: true }
|
|
176
|
+
})
|
|
177
|
+
allow(dependency_checker).to receive(:gimp_path).and_return('/usr/bin/gimp')
|
|
178
|
+
allow(dependency_checker).to receive(:gimp_version).and_return({ major: 3, minor: 0 })
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it 'passes by_frame flag to each video processor' do
|
|
182
|
+
video_processor1 = instance_double(RubySpriter::VideoProcessor)
|
|
183
|
+
video_processor2 = instance_double(RubySpriter::VideoProcessor)
|
|
184
|
+
|
|
185
|
+
# VideoProcessor.new is called 2 times total (1 per video)
|
|
186
|
+
# After refactoring: uses cached @gimp_path, so only one instantiation per video
|
|
187
|
+
call_count = 0
|
|
188
|
+
expect(RubySpriter::VideoProcessor).to receive(:new).exactly(2).times do |passed_options|
|
|
189
|
+
call_count += 1
|
|
190
|
+
|
|
191
|
+
# Every call should have by_frame, remove_bg, and gimp_path (cached)
|
|
192
|
+
expect(passed_options[:by_frame]).to be true
|
|
193
|
+
expect(passed_options[:remove_bg]).to be true
|
|
194
|
+
expect(passed_options[:gimp_path]).to eq('/usr/bin/gimp')
|
|
195
|
+
|
|
196
|
+
call_count == 1 ? video_processor1 : video_processor2
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Mock process_with_background_removal to return proper result
|
|
200
|
+
allow(video_processor1).to receive(:process_with_background_removal).and_return({
|
|
201
|
+
output_file: 'output1.png',
|
|
202
|
+
columns: 2,
|
|
203
|
+
frames: 4,
|
|
204
|
+
processing_mode: 'by-frame'
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
allow(video_processor2).to receive(:process_with_background_removal).and_return({
|
|
208
|
+
output_file: 'output2.png',
|
|
209
|
+
columns: 2,
|
|
210
|
+
frames: 4,
|
|
211
|
+
processing_mode: 'by-frame'
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
batch_processor.process
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it 'reports frame-by-frame processing mode in batch summary' do
|
|
218
|
+
video_processor = instance_double(RubySpriter::VideoProcessor)
|
|
219
|
+
allow(RubySpriter::VideoProcessor).to receive(:new).and_return(video_processor)
|
|
220
|
+
|
|
221
|
+
# Mock process_with_background_removal
|
|
222
|
+
allow(video_processor).to receive(:process_with_background_removal).and_return({
|
|
223
|
+
output_file: 'output.png',
|
|
224
|
+
columns: 2,
|
|
225
|
+
frames: 4,
|
|
226
|
+
processing_mode: 'by-frame'
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
result = batch_processor.process
|
|
230
|
+
|
|
231
|
+
expect(result[:processed_count]).to eq(2)
|
|
232
|
+
expect(result[:errors].length).to eq(0)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
describe '#consolidate_results' do
|
|
237
|
+
let(:spritesheet1) { File.join(test_dir, 'video1_spritesheet.png') }
|
|
238
|
+
let(:spritesheet2) { File.join(test_dir, 'video2_spritesheet.png') }
|
|
239
|
+
let(:spritesheet3) { File.join(test_dir, 'video3_spritesheet.png') }
|
|
240
|
+
let(:consolidator_mock) { instance_double(RubySpriter::Consolidator) }
|
|
241
|
+
|
|
242
|
+
before do
|
|
243
|
+
allow(RubySpriter::Consolidator).to receive(:new).and_return(consolidator_mock)
|
|
244
|
+
allow(consolidator_mock).to receive(:consolidate).and_return(
|
|
245
|
+
{ output_file: 'consolidated.png', columns: 4, rows: 12, frames: 48 }
|
|
246
|
+
)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
it 'consolidates all spritesheets when batch_consolidate is true' do
|
|
250
|
+
options_with_consolidate = options.merge(batch_consolidate: true)
|
|
251
|
+
processor = described_class.new(options_with_consolidate)
|
|
252
|
+
|
|
253
|
+
outputs = [spritesheet1, spritesheet2, spritesheet3]
|
|
254
|
+
|
|
255
|
+
expect(consolidator_mock).to receive(:consolidate) do |files, output|
|
|
256
|
+
expect(files).to eq(outputs)
|
|
257
|
+
expect(output).to include('batch_consolidated_spritesheet')
|
|
258
|
+
expect(output).to end_with('.png')
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
processor.consolidate_results(outputs)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
it 'uses outputdir for consolidated file if specified' do
|
|
265
|
+
options_with_consolidate = options.merge(batch_consolidate: true, outputdir: output_dir)
|
|
266
|
+
processor = described_class.new(options_with_consolidate)
|
|
267
|
+
|
|
268
|
+
allow(File).to receive(:directory?).with(output_dir).and_return(true)
|
|
269
|
+
|
|
270
|
+
outputs = [spritesheet1, spritesheet2, spritesheet3]
|
|
271
|
+
|
|
272
|
+
expect(consolidator_mock).to receive(:consolidate) do |files, output|
|
|
273
|
+
expect(output).to start_with(output_dir)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
processor.consolidate_results(outputs)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
it 'does not consolidate when batch_consolidate is false' do
|
|
280
|
+
processor = described_class.new(options)
|
|
281
|
+
|
|
282
|
+
outputs = [spritesheet1, spritesheet2, spritesheet3]
|
|
283
|
+
|
|
284
|
+
expect(consolidator_mock).not_to receive(:consolidate)
|
|
285
|
+
|
|
286
|
+
result = processor.consolidate_results(outputs)
|
|
287
|
+
expect(result).to be_nil
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
describe '#using_frame_by_frame_background_removal?' do
|
|
292
|
+
context 'when both by_frame and remove_bg are true' do
|
|
293
|
+
it 'returns true' do
|
|
294
|
+
options_with_flags = options.merge(by_frame: true, remove_bg: true)
|
|
295
|
+
processor = described_class.new(options_with_flags)
|
|
296
|
+
|
|
297
|
+
expect(processor.send(:using_frame_by_frame_background_removal?)).to be true
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
context 'when only by_frame is true' do
|
|
302
|
+
it 'returns false' do
|
|
303
|
+
options_with_by_frame = options.merge(by_frame: true, remove_bg: false)
|
|
304
|
+
processor = described_class.new(options_with_by_frame)
|
|
305
|
+
|
|
306
|
+
expect(processor.send(:using_frame_by_frame_background_removal?)).to be false
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
context 'when only remove_bg is true' do
|
|
311
|
+
it 'returns false' do
|
|
312
|
+
options_with_remove_bg = options.merge(by_frame: false, remove_bg: true)
|
|
313
|
+
processor = described_class.new(options_with_remove_bg)
|
|
314
|
+
|
|
315
|
+
expect(processor.send(:using_frame_by_frame_background_removal?)).to be false
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
context 'when neither flag is set' do
|
|
320
|
+
it 'returns false' do
|
|
321
|
+
options_without_flags = options.merge(by_frame: false, remove_bg: false)
|
|
322
|
+
processor = described_class.new(options_without_flags)
|
|
323
|
+
|
|
324
|
+
expect(processor.send(:using_frame_by_frame_background_removal?)).to be false
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
describe '#normalize_video_result_format' do
|
|
330
|
+
it 'normalizes result hash to standard format' do
|
|
331
|
+
input_result = {
|
|
332
|
+
output_file: 'test_output.png',
|
|
333
|
+
columns: 4,
|
|
334
|
+
frames: 16,
|
|
335
|
+
processing_mode: 'by-frame',
|
|
336
|
+
extra_data: 'should be removed'
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
expected_result = {
|
|
340
|
+
output_file: 'test_output.png',
|
|
341
|
+
columns: 4,
|
|
342
|
+
rows: 4,
|
|
343
|
+
frames: 16
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
processor = described_class.new(options)
|
|
347
|
+
normalized = processor.send(:normalize_video_result_format, input_result)
|
|
348
|
+
|
|
349
|
+
expect(normalized).to eq(expected_result)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
it 'calculates rows with ceiling division' do
|
|
353
|
+
input_result = {
|
|
354
|
+
output_file: 'test.png',
|
|
355
|
+
columns: 4,
|
|
356
|
+
frames: 15 # 15 / 4 = 3.75, should ceil to 4
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
processor = described_class.new(options)
|
|
360
|
+
normalized = processor.send(:normalize_video_result_format, input_result)
|
|
361
|
+
|
|
362
|
+
expect(normalized[:rows]).to eq(4)
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
describe 'dependency checking' do
|
|
367
|
+
let(:dependency_checker) { instance_double(RubySpriter::DependencyChecker) }
|
|
368
|
+
|
|
369
|
+
before do
|
|
370
|
+
allow(RubySpriter::DependencyChecker).to receive(:new).and_return(dependency_checker)
|
|
371
|
+
allow(dependency_checker).to receive(:check_all).and_return({
|
|
372
|
+
ffmpeg: { available: true },
|
|
373
|
+
ffprobe: { available: true },
|
|
374
|
+
imagemagick: { available: true },
|
|
375
|
+
gimp: { available: true }
|
|
376
|
+
})
|
|
377
|
+
allow(dependency_checker).to receive(:gimp_path).and_return('/usr/bin/gimp')
|
|
378
|
+
allow(dependency_checker).to receive(:gimp_version).and_return({ major: 3, minor: 0 })
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
context 'when GIMP is needed for processing' do
|
|
382
|
+
let(:options_needing_gimp) { options.merge(by_frame: true, remove_bg: true) }
|
|
383
|
+
|
|
384
|
+
it 'checks dependencies during initialization' do
|
|
385
|
+
expect(RubySpriter::DependencyChecker).to receive(:new).with(verbose: false).once
|
|
386
|
+
expect(dependency_checker).to receive(:check_all).once
|
|
387
|
+
|
|
388
|
+
described_class.new(options_needing_gimp)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
it 'stores gimp_path as instance variable' do
|
|
392
|
+
processor = described_class.new(options_needing_gimp)
|
|
393
|
+
|
|
394
|
+
expect(processor.instance_variable_get(:@gimp_path)).to eq('/usr/bin/gimp')
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
it 'stores gimp_version as instance variable' do
|
|
398
|
+
processor = described_class.new(options_needing_gimp)
|
|
399
|
+
|
|
400
|
+
expect(processor.instance_variable_get(:@gimp_version)).to eq({ major: 3, minor: 0 })
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
context 'when GIMP is not needed' do
|
|
405
|
+
let(:options_without_gimp) { options.merge(by_frame: false, remove_bg: false) }
|
|
406
|
+
|
|
407
|
+
it 'does not check dependencies during initialization' do
|
|
408
|
+
expect(RubySpriter::DependencyChecker).not_to receive(:new)
|
|
409
|
+
|
|
410
|
+
described_class.new(options_without_gimp)
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
describe '#process_video with cached dependencies' do
|
|
416
|
+
let(:options_with_by_frame) { options.merge(by_frame: true, remove_bg: true) }
|
|
417
|
+
let(:video_file) { File.join(test_dir, 'test.mp4') }
|
|
418
|
+
let(:output_file) { File.join(test_dir, 'test_spritesheet.png') }
|
|
419
|
+
let(:video_processor) { instance_double(RubySpriter::VideoProcessor) }
|
|
420
|
+
let(:dependency_checker) { instance_double(RubySpriter::DependencyChecker) }
|
|
421
|
+
|
|
422
|
+
before do
|
|
423
|
+
# Mock DependencyChecker for initialization only
|
|
424
|
+
allow(RubySpriter::DependencyChecker).to receive(:new).and_return(dependency_checker)
|
|
425
|
+
allow(dependency_checker).to receive(:check_all).and_return({
|
|
426
|
+
ffmpeg: { available: true },
|
|
427
|
+
gimp: { available: true }
|
|
428
|
+
})
|
|
429
|
+
allow(dependency_checker).to receive(:gimp_path).and_return('/usr/bin/gimp')
|
|
430
|
+
allow(dependency_checker).to receive(:gimp_version).and_return({ major: 3, minor: 0 })
|
|
431
|
+
|
|
432
|
+
# Mock VideoProcessor
|
|
433
|
+
allow(RubySpriter::VideoProcessor).to receive(:new).and_return(video_processor)
|
|
434
|
+
allow(video_processor).to receive(:process_with_background_removal).and_return({
|
|
435
|
+
output_file: output_file,
|
|
436
|
+
columns: 4,
|
|
437
|
+
frames: 16
|
|
438
|
+
})
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
it 'does not create additional DependencyChecker instances during processing' do
|
|
442
|
+
processor = described_class.new(options_with_by_frame)
|
|
443
|
+
|
|
444
|
+
# DependencyChecker should only be called once during initialization
|
|
445
|
+
# Not again during process_video
|
|
446
|
+
expect(RubySpriter::DependencyChecker).not_to receive(:new)
|
|
447
|
+
|
|
448
|
+
processor.send(:process_video, video_file, output_file)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
it 'passes cached gimp_path to VideoProcessor via options' do
|
|
452
|
+
processor = described_class.new(options_with_by_frame)
|
|
453
|
+
|
|
454
|
+
expect(RubySpriter::VideoProcessor).to receive(:new) do |passed_options|
|
|
455
|
+
expect(passed_options[:gimp_path]).to eq('/usr/bin/gimp')
|
|
456
|
+
video_processor
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
processor.send(:process_video, video_file, output_file)
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
describe '#process_with_gimp with cached dependencies' do
|
|
464
|
+
let(:options_with_gimp) { options.merge(scale_percent: 50) }
|
|
465
|
+
let(:input_file) { File.join(test_dir, 'input.png') }
|
|
466
|
+
let(:output_file) { File.join(test_dir, 'output.png') }
|
|
467
|
+
let(:gimp_processor) { instance_double(RubySpriter::GimpProcessor) }
|
|
468
|
+
let(:dependency_checker) { instance_double(RubySpriter::DependencyChecker) }
|
|
469
|
+
|
|
470
|
+
before do
|
|
471
|
+
# Mock DependencyChecker for initialization only
|
|
472
|
+
allow(RubySpriter::DependencyChecker).to receive(:new).and_return(dependency_checker)
|
|
473
|
+
allow(dependency_checker).to receive(:check_all).and_return({
|
|
474
|
+
ffmpeg: { available: true },
|
|
475
|
+
gimp: { available: true }
|
|
476
|
+
})
|
|
477
|
+
allow(dependency_checker).to receive(:gimp_path).and_return('/usr/bin/gimp')
|
|
478
|
+
allow(dependency_checker).to receive(:gimp_version).and_return({ major: 3, minor: 0 })
|
|
479
|
+
|
|
480
|
+
# Mock GimpProcessor
|
|
481
|
+
allow(RubySpriter::GimpProcessor).to receive(:new).and_return(gimp_processor)
|
|
482
|
+
allow(gimp_processor).to receive(:process).and_return(output_file)
|
|
483
|
+
|
|
484
|
+
# Mock file operations
|
|
485
|
+
allow(File).to receive(:exist?).and_return(true)
|
|
486
|
+
allow(File).to receive(:delete)
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
it 'does not create additional DependencyChecker instances' do
|
|
490
|
+
processor = described_class.new(options_with_gimp)
|
|
491
|
+
|
|
492
|
+
# DependencyChecker should only be called once during initialization
|
|
493
|
+
expect(RubySpriter::DependencyChecker).not_to receive(:new)
|
|
494
|
+
|
|
495
|
+
processor.send(:process_with_gimp, input_file, { columns: 4, rows: 4 })
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
it 'uses cached gimp_path when creating GimpProcessor' do
|
|
499
|
+
processor = described_class.new(options_with_gimp)
|
|
500
|
+
|
|
501
|
+
expect(RubySpriter::GimpProcessor).to receive(:new).with(
|
|
502
|
+
'/usr/bin/gimp',
|
|
503
|
+
hash_including(gimp_version: { major: 3, minor: 0 })
|
|
504
|
+
).and_return(gimp_processor)
|
|
505
|
+
|
|
506
|
+
processor.send(:process_with_gimp, input_file, { columns: 4, rows: 4 })
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
end
|