ruby_spriter 0.6.5 → 0.6.7
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/CHANGELOG.md +188 -0
- data/README.md +374 -33
- data/lib/ruby_spriter/batch_processor.rb +212 -0
- data/lib/ruby_spriter/cli.rb +369 -6
- data/lib/ruby_spriter/compression_manager.rb +101 -0
- data/lib/ruby_spriter/consolidator.rb +33 -0
- data/lib/ruby_spriter/gimp_processor.rb +6 -3
- data/lib/ruby_spriter/processor.rb +661 -26
- data/lib/ruby_spriter/utils/file_helper.rb +25 -0
- data/lib/ruby_spriter/utils/spritesheet_splitter.rb +86 -0
- data/lib/ruby_spriter/version.rb +2 -2
- data/lib/ruby_spriter/video_processor.rb +7 -7
- data/lib/ruby_spriter.rb +3 -0
- data/spec/ruby_spriter/batch_processor_spec.rb +200 -0
- data/spec/ruby_spriter/cli_spec.rb +750 -0
- data/spec/ruby_spriter/compression_manager_spec.rb +157 -0
- data/spec/ruby_spriter/consolidator_spec.rb +163 -0
- data/spec/ruby_spriter/processor_spec.rb +735 -0
- data/spec/ruby_spriter/utils/file_helper_spec.rb +80 -1
- data/spec/ruby_spriter/utils/spritesheet_splitter_spec.rb +104 -0
- data/spec/ruby_spriter/video_processor_spec.rb +29 -0
- metadata +8 -2
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RubySpriter::Processor do
|
|
6
|
+
describe 'default options' do
|
|
7
|
+
it 'sets default max_width to 320' do
|
|
8
|
+
processor = described_class.new
|
|
9
|
+
expect(processor.options[:max_width]).to eq(320)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe 'numeric option validation' do
|
|
14
|
+
describe '--frames validation' do
|
|
15
|
+
it 'raises error when frame_count is below minimum (0)' do
|
|
16
|
+
expect {
|
|
17
|
+
described_class.new(frame_count: 0)
|
|
18
|
+
}.to raise_error(RubySpriter::ValidationError, /frame_count must be between 1 and 10000/)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'raises error when frame_count is above maximum (10001)' do
|
|
22
|
+
expect {
|
|
23
|
+
described_class.new(frame_count: 10001)
|
|
24
|
+
}.to raise_error(RubySpriter::ValidationError, /frame_count must be between 1 and 10000/)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe '--columns validation' do
|
|
29
|
+
it 'raises error when columns is below minimum (0)' do
|
|
30
|
+
expect {
|
|
31
|
+
described_class.new(columns: 0)
|
|
32
|
+
}.to raise_error(RubySpriter::ValidationError, /columns must be between 1 and 100/)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'raises error when columns is above maximum (101)' do
|
|
36
|
+
expect {
|
|
37
|
+
described_class.new(columns: 101)
|
|
38
|
+
}.to raise_error(RubySpriter::ValidationError, /columns must be between 1 and 100/)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '--width validation' do
|
|
43
|
+
it 'raises error when max_width is below minimum (0)' do
|
|
44
|
+
expect {
|
|
45
|
+
described_class.new(max_width: 0)
|
|
46
|
+
}.to raise_error(RubySpriter::ValidationError, /max_width must be between 1 and 1920/)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'raises error when max_width is above maximum (1921)' do
|
|
50
|
+
expect {
|
|
51
|
+
described_class.new(max_width: 1921)
|
|
52
|
+
}.to raise_error(RubySpriter::ValidationError, /max_width must be between 1 and 1920/)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe '--scale validation' do
|
|
57
|
+
it 'raises error when scale_percent is below minimum (0)' do
|
|
58
|
+
expect {
|
|
59
|
+
described_class.new(scale_percent: 0)
|
|
60
|
+
}.to raise_error(RubySpriter::ValidationError, /scale_percent must be between 1 and 500/)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'raises error when scale_percent is above maximum (501)' do
|
|
64
|
+
expect {
|
|
65
|
+
described_class.new(scale_percent: 501)
|
|
66
|
+
}.to raise_error(RubySpriter::ValidationError, /scale_percent must be between 1 and 500/)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe '--grow validation' do
|
|
71
|
+
it 'raises error when grow_selection is below minimum (-1)' do
|
|
72
|
+
expect {
|
|
73
|
+
described_class.new(grow_selection: -1)
|
|
74
|
+
}.to raise_error(RubySpriter::ValidationError, /grow_selection must be between 0 and 100/)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'raises error when grow_selection is above maximum (101)' do
|
|
78
|
+
expect {
|
|
79
|
+
described_class.new(grow_selection: 101)
|
|
80
|
+
}.to raise_error(RubySpriter::ValidationError, /grow_selection must be between 0 and 100/)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe '--sharpen-radius validation' do
|
|
85
|
+
it 'raises error when sharpen_radius is below minimum (0.0)' do
|
|
86
|
+
expect {
|
|
87
|
+
described_class.new(sharpen_radius: 0.0)
|
|
88
|
+
}.to raise_error(RubySpriter::ValidationError, /sharpen_radius must be between 0.1 and 100.0/)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'raises error when sharpen_radius is above maximum (100.1)' do
|
|
92
|
+
expect {
|
|
93
|
+
described_class.new(sharpen_radius: 100.1)
|
|
94
|
+
}.to raise_error(RubySpriter::ValidationError, /sharpen_radius must be between 0.1 and 100.0/)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
describe '--sharpen-gain validation' do
|
|
99
|
+
it 'raises error when sharpen_gain is below minimum (-0.1)' do
|
|
100
|
+
expect {
|
|
101
|
+
described_class.new(sharpen_gain: -0.1)
|
|
102
|
+
}.to raise_error(RubySpriter::ValidationError, /sharpen_gain must be between 0.0 and 10.0/)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'raises error when sharpen_gain is above maximum (10.1)' do
|
|
106
|
+
expect {
|
|
107
|
+
described_class.new(sharpen_gain: 10.1)
|
|
108
|
+
}.to raise_error(RubySpriter::ValidationError, /sharpen_gain must be between 0.0 and 10.0/)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
describe '--sharpen-threshold validation' do
|
|
113
|
+
it 'raises error when sharpen_threshold is below minimum (-0.1)' do
|
|
114
|
+
expect {
|
|
115
|
+
described_class.new(sharpen_threshold: -0.1)
|
|
116
|
+
}.to raise_error(RubySpriter::ValidationError, /sharpen_threshold must be between 0.0 and 1.0/)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'raises error when sharpen_threshold is above maximum (1.1)' do
|
|
120
|
+
expect {
|
|
121
|
+
described_class.new(sharpen_threshold: 1.1)
|
|
122
|
+
}.to raise_error(RubySpriter::ValidationError, /sharpen_threshold must be between 0.0 and 1.0/)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
describe '--threshold validation' do
|
|
127
|
+
it 'raises error when bg_threshold is below minimum (-0.1)' do
|
|
128
|
+
expect {
|
|
129
|
+
described_class.new(bg_threshold: -0.1)
|
|
130
|
+
}.to raise_error(RubySpriter::ValidationError, /bg_threshold must be between 0.0 and 100.0/)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'raises error when bg_threshold is above maximum (100.1)' do
|
|
134
|
+
expect {
|
|
135
|
+
described_class.new(bg_threshold: 100.1)
|
|
136
|
+
}.to raise_error(RubySpriter::ValidationError, /bg_threshold must be between 0.0 and 100.0/)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
describe '--split validation' do
|
|
142
|
+
describe 'format validation' do
|
|
143
|
+
it 'raises error for invalid split format (missing colon)' do
|
|
144
|
+
expect {
|
|
145
|
+
described_class.new(image: 'test.png', split: '44')
|
|
146
|
+
}.to raise_error(RubySpriter::ValidationError, /Invalid --split format/)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'raises error for invalid split format (non-numeric rows)' do
|
|
150
|
+
expect {
|
|
151
|
+
described_class.new(image: 'test.png', split: 'a:4')
|
|
152
|
+
}.to raise_error(RubySpriter::ValidationError, /Invalid --split format/)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it 'raises error for invalid split format (non-numeric columns)' do
|
|
156
|
+
expect {
|
|
157
|
+
described_class.new(image: 'test.png', split: '4:b')
|
|
158
|
+
}.to raise_error(RubySpriter::ValidationError, /Invalid --split format/)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
describe 'range validation' do
|
|
163
|
+
it 'raises error when rows is below minimum (0)' do
|
|
164
|
+
expect {
|
|
165
|
+
described_class.new(image: 'test.png', split: '0:4')
|
|
166
|
+
}.to raise_error(RubySpriter::ValidationError, /rows must be between 1 and 99/)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it 'raises error when rows is above maximum (100)' do
|
|
170
|
+
expect {
|
|
171
|
+
described_class.new(image: 'test.png', split: '100:4')
|
|
172
|
+
}.to raise_error(RubySpriter::ValidationError, /rows must be between 1 and 99/)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it 'raises error when columns is below minimum (0)' do
|
|
176
|
+
expect {
|
|
177
|
+
described_class.new(image: 'test.png', split: '4:0')
|
|
178
|
+
}.to raise_error(RubySpriter::ValidationError, /columns must be between 1 and 99/)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it 'raises error when columns is above maximum (100)' do
|
|
182
|
+
expect {
|
|
183
|
+
described_class.new(image: 'test.png', split: '4:100')
|
|
184
|
+
}.to raise_error(RubySpriter::ValidationError, /columns must be between 1 and 99/)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it 'raises error when total frames >= 1000' do
|
|
188
|
+
expect {
|
|
189
|
+
described_class.new(image: 'test.png', split: '32:32')
|
|
190
|
+
}.to raise_error(RubySpriter::ValidationError, /Total frames \(1024\) must be less than 1000/)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it 'raises error when total frames equals 1000' do
|
|
194
|
+
expect {
|
|
195
|
+
described_class.new(image: 'test.png', split: '20:50')
|
|
196
|
+
}.to raise_error(RubySpriter::ValidationError, /Total frames \(1000\) must be less than 1000/)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it 'allows maximum valid frames (999)' do
|
|
200
|
+
expect {
|
|
201
|
+
described_class.new(image: 'test.png', split: '27:37')
|
|
202
|
+
}.not_to raise_error
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
describe '--extract validation' do
|
|
208
|
+
let(:temp_image) { File.join(Dir.mktmpdir, 'test.png') }
|
|
209
|
+
|
|
210
|
+
before do
|
|
211
|
+
FileUtils.touch(temp_image)
|
|
212
|
+
|
|
213
|
+
allow_any_instance_of(RubySpriter::DependencyChecker).to receive(:check_all).and_return({
|
|
214
|
+
ffmpeg: { available: true },
|
|
215
|
+
ffprobe: { available: true },
|
|
216
|
+
imagemagick: { available: true },
|
|
217
|
+
gimp: { available: true }
|
|
218
|
+
})
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
after do
|
|
222
|
+
FileUtils.rm_f(temp_image)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
describe 'format validation' do
|
|
226
|
+
it 'raises error for invalid extract format (non-numeric)' do
|
|
227
|
+
expect {
|
|
228
|
+
described_class.new(image: temp_image, extract: '1,a,3')
|
|
229
|
+
}.to raise_error(RubySpriter::ValidationError, /Invalid --extract format/)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it 'raises error for invalid extract format (empty)' do
|
|
233
|
+
expect {
|
|
234
|
+
described_class.new(image: temp_image, extract: '')
|
|
235
|
+
}.to raise_error(RubySpriter::ValidationError, /Invalid --extract format/)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it 'raises error for invalid extract format (spaces)' do
|
|
239
|
+
expect {
|
|
240
|
+
described_class.new(image: temp_image, extract: '1, 2, 3')
|
|
241
|
+
}.to raise_error(RubySpriter::ValidationError, /Invalid --extract format/)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it 'accepts valid comma-separated frame numbers' do
|
|
245
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(temp_image).and_return({
|
|
246
|
+
columns: 4,
|
|
247
|
+
rows: 4,
|
|
248
|
+
frames: 16
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
expect {
|
|
252
|
+
described_class.new(image: temp_image, extract: '1,2,4,5,8')
|
|
253
|
+
}.not_to raise_error
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
describe 'minimum frames validation' do
|
|
258
|
+
it 'raises error when less than 2 frames requested' do
|
|
259
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(temp_image).and_return({
|
|
260
|
+
columns: 4,
|
|
261
|
+
rows: 4,
|
|
262
|
+
frames: 16
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
expect {
|
|
266
|
+
described_class.new(image: temp_image, extract: '1')
|
|
267
|
+
}.to raise_error(RubySpriter::ValidationError, /--extract requires at least 2 frames/)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
it 'allows exactly 2 frames' do
|
|
271
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(temp_image).and_return({
|
|
272
|
+
columns: 4,
|
|
273
|
+
rows: 4,
|
|
274
|
+
frames: 16
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
expect {
|
|
278
|
+
described_class.new(image: temp_image, extract: '1,2')
|
|
279
|
+
}.not_to raise_error
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
describe 'frame number validation' do
|
|
284
|
+
it 'raises error when frame number is 0' do
|
|
285
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(temp_image).and_return({
|
|
286
|
+
columns: 4,
|
|
287
|
+
rows: 4,
|
|
288
|
+
frames: 16
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
expect {
|
|
292
|
+
described_class.new(image: temp_image, extract: '0,1,2')
|
|
293
|
+
}.to raise_error(RubySpriter::ValidationError, /Frame numbers must be 1-indexed/)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
it 'raises error when frame number is negative' do
|
|
297
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(temp_image).and_return({
|
|
298
|
+
columns: 4,
|
|
299
|
+
rows: 4,
|
|
300
|
+
frames: 16
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
expect {
|
|
304
|
+
described_class.new(image: temp_image, extract: '1,-2,3')
|
|
305
|
+
}.to raise_error(RubySpriter::ValidationError, /Frame numbers must be 1-indexed/)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
it 'allows duplicate frame numbers' do
|
|
309
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(temp_image).and_return({
|
|
310
|
+
columns: 4,
|
|
311
|
+
rows: 4,
|
|
312
|
+
frames: 16
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
expect {
|
|
316
|
+
described_class.new(image: temp_image, extract: '1,1,2,2,3,3')
|
|
317
|
+
}.not_to raise_error
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
describe 'metadata requirement' do
|
|
322
|
+
it 'raises error when image has no metadata' do
|
|
323
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(temp_image).and_return(nil)
|
|
324
|
+
|
|
325
|
+
expect {
|
|
326
|
+
described_class.new(image: temp_image, extract: '1,2,3')
|
|
327
|
+
}.to raise_error(RubySpriter::ValidationError, /Image has no metadata.*Cannot extract frames/)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
describe 'out of bounds validation' do
|
|
332
|
+
before do
|
|
333
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(temp_image).and_return({
|
|
334
|
+
columns: 4,
|
|
335
|
+
rows: 4,
|
|
336
|
+
frames: 16
|
|
337
|
+
})
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
it 'raises error when frame number exceeds total frames' do
|
|
341
|
+
expect {
|
|
342
|
+
described_class.new(image: temp_image, extract: '1,2,17')
|
|
343
|
+
}.to raise_error(RubySpriter::ValidationError, /Frame 17 is out of bounds.*only has 16 frames/)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
it 'allows frame number equal to total frames' do
|
|
347
|
+
expect {
|
|
348
|
+
described_class.new(image: temp_image, extract: '1,2,16')
|
|
349
|
+
}.not_to raise_error
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
it 'raises error for multiple out of bounds frames' do
|
|
353
|
+
expect {
|
|
354
|
+
described_class.new(image: temp_image, extract: '1,20,25')
|
|
355
|
+
}.to raise_error(RubySpriter::ValidationError, /Frame 20 is out of bounds/)
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
describe '--columns default' do
|
|
360
|
+
before do
|
|
361
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(temp_image).and_return({
|
|
362
|
+
columns: 4,
|
|
363
|
+
rows: 4,
|
|
364
|
+
frames: 16
|
|
365
|
+
})
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
it 'defaults to 4 columns when not specified' do
|
|
369
|
+
processor = described_class.new(image: temp_image, extract: '1,2,3,4')
|
|
370
|
+
expect(processor.instance_variable_get(:@options)[:columns]).to eq(4)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
it 'uses specified columns when provided' do
|
|
374
|
+
processor = described_class.new(image: temp_image, extract: '1,2,3', columns: 3)
|
|
375
|
+
expect(processor.instance_variable_get(:@options)[:columns]).to eq(3)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
describe '--add-meta validation' do
|
|
381
|
+
let(:temp_image) { File.join(Dir.mktmpdir, 'test.png') }
|
|
382
|
+
|
|
383
|
+
before do
|
|
384
|
+
FileUtils.touch(temp_image)
|
|
385
|
+
|
|
386
|
+
allow_any_instance_of(RubySpriter::DependencyChecker).to receive(:check_all).and_return({
|
|
387
|
+
ffmpeg: { available: true },
|
|
388
|
+
ffprobe: { available: true },
|
|
389
|
+
imagemagick: { available: true },
|
|
390
|
+
gimp: { available: true }
|
|
391
|
+
})
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
after do
|
|
395
|
+
FileUtils.rm_f(temp_image)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
describe 'format validation' do
|
|
399
|
+
it 'raises error for invalid format (missing colon)' do
|
|
400
|
+
expect {
|
|
401
|
+
described_class.new(image: temp_image, add_meta: '44')
|
|
402
|
+
}.to raise_error(RubySpriter::ValidationError, /Invalid --add-meta format/)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
it 'raises error for invalid format (non-numeric rows)' do
|
|
406
|
+
expect {
|
|
407
|
+
described_class.new(image: temp_image, add_meta: 'a:4')
|
|
408
|
+
}.to raise_error(RubySpriter::ValidationError, /Invalid --add-meta format/)
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
it 'raises error for invalid format (non-numeric columns)' do
|
|
412
|
+
expect {
|
|
413
|
+
described_class.new(image: temp_image, add_meta: '4:b')
|
|
414
|
+
}.to raise_error(RubySpriter::ValidationError, /Invalid --add-meta format/)
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
it 'accepts valid R:C format' do
|
|
418
|
+
# Mock ImageMagick identify for dimension validation
|
|
419
|
+
allow(Open3).to receive(:capture3).with(/identify/).and_return(
|
|
420
|
+
["800x800\n", '', instance_double(Process::Status, success?: true)]
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(temp_image).and_return(nil)
|
|
424
|
+
|
|
425
|
+
expect {
|
|
426
|
+
described_class.new(image: temp_image, add_meta: '4:4')
|
|
427
|
+
}.not_to raise_error
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
describe 'range validation' do
|
|
432
|
+
it 'raises error when rows is below minimum (0)' do
|
|
433
|
+
expect {
|
|
434
|
+
described_class.new(image: temp_image, add_meta: '0:4')
|
|
435
|
+
}.to raise_error(RubySpriter::ValidationError, /rows must be between 1 and 99/)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
it 'raises error when columns is below minimum (0)' do
|
|
439
|
+
expect {
|
|
440
|
+
described_class.new(image: temp_image, add_meta: '4:0')
|
|
441
|
+
}.to raise_error(RubySpriter::ValidationError, /columns must be between 1 and 99/)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
it 'raises error when rows exceeds maximum (100)' do
|
|
445
|
+
expect {
|
|
446
|
+
described_class.new(image: temp_image, add_meta: '100:4')
|
|
447
|
+
}.to raise_error(RubySpriter::ValidationError, /rows must be between 1 and 99/)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
it 'raises error when total frames exceeds 999' do
|
|
451
|
+
expect {
|
|
452
|
+
described_class.new(image: temp_image, add_meta: '20:50')
|
|
453
|
+
}.to raise_error(RubySpriter::ValidationError, /Total frames \(1000\) must be less than 1000/)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
it 'allows maximum valid frames (999)' do
|
|
457
|
+
# Mock ImageMagick identify - 3700x2700 divides evenly by 27 rows x 37 columns (100x100 per frame)
|
|
458
|
+
allow(Open3).to receive(:capture3).with(/identify/).and_return(
|
|
459
|
+
["3700x2700\n", '', instance_double(Process::Status, success?: true)]
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(temp_image).and_return(nil)
|
|
463
|
+
|
|
464
|
+
expect {
|
|
465
|
+
described_class.new(image: temp_image, add_meta: '27:37')
|
|
466
|
+
}.not_to raise_error
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
describe 'existing metadata handling' do
|
|
471
|
+
it 'raises error when image already has metadata' do
|
|
472
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(temp_image).and_return({
|
|
473
|
+
columns: 4,
|
|
474
|
+
rows: 4,
|
|
475
|
+
frames: 16
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
expect {
|
|
479
|
+
described_class.new(image: temp_image, add_meta: '4:4')
|
|
480
|
+
}.to raise_error(RubySpriter::ValidationError, /Image already has spritesheet metadata.*Use --overwrite-meta/)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
it 'allows replacing metadata with --overwrite-meta' do
|
|
484
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(temp_image).and_return({
|
|
485
|
+
columns: 4,
|
|
486
|
+
rows: 4,
|
|
487
|
+
frames: 16
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
# Mock ImageMagick identify
|
|
491
|
+
allow(Open3).to receive(:capture3).with(/identify/).and_return(
|
|
492
|
+
["800x800\n", '', instance_double(Process::Status, success?: true)]
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
expect {
|
|
496
|
+
described_class.new(image: temp_image, add_meta: '8:8', overwrite_meta: true)
|
|
497
|
+
}.not_to raise_error
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
describe 'dimension validation' do
|
|
502
|
+
before do
|
|
503
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(temp_image).and_return(nil)
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
it 'raises error when image dimensions do not divide evenly' do
|
|
507
|
+
# Mock ImageMagick identify - 800x600 doesn't divide by 3x3
|
|
508
|
+
allow(Open3).to receive(:capture3).with(/identify/).and_return(
|
|
509
|
+
["800x600\n", '', instance_double(Process::Status, success?: true)]
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
expect {
|
|
513
|
+
described_class.new(image: temp_image, add_meta: '3:3')
|
|
514
|
+
}.to raise_error(RubySpriter::ValidationError, /Image dimensions \(800x600\) must divide evenly by grid \(3x3\)/)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
it 'allows dimensions that divide evenly' do
|
|
518
|
+
# Mock ImageMagick identify - 800x800 divides by 4x4
|
|
519
|
+
allow(Open3).to receive(:capture3).with(/identify/).and_return(
|
|
520
|
+
["800x800\n", '', instance_double(Process::Status, success?: true)]
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
expect {
|
|
524
|
+
described_class.new(image: temp_image, add_meta: '4:4')
|
|
525
|
+
}.not_to raise_error
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
describe 'frame count handling' do
|
|
530
|
+
before do
|
|
531
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(temp_image).and_return(nil)
|
|
532
|
+
allow(Open3).to receive(:capture3).with(/identify/).and_return(
|
|
533
|
+
["800x800\n", '', instance_double(Process::Status, success?: true)]
|
|
534
|
+
)
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
it 'calculates frame count from rows * columns by default' do
|
|
538
|
+
processor = described_class.new(image: temp_image, add_meta: '4:4')
|
|
539
|
+
# Frame count should be 16 (4x4)
|
|
540
|
+
# We'll verify this in the workflow implementation
|
|
541
|
+
expect(processor.instance_variable_get(:@options)[:add_meta]).to eq('4:4')
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
it 'allows custom frame count with --frames for partial grids' do
|
|
545
|
+
processor = described_class.new(image: temp_image, add_meta: '4:4', frame_count: 14)
|
|
546
|
+
expect(processor.instance_variable_get(:@options)[:frame_count]).to eq(14)
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
it 'raises error when custom frame count exceeds grid size' do
|
|
550
|
+
expect {
|
|
551
|
+
described_class.new(image: temp_image, add_meta: '4:4', frame_count: 20)
|
|
552
|
+
}.to raise_error(RubySpriter::ValidationError, /Frame count \(20\) exceeds grid size \(16\)/)
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
describe '--split metadata priority logic' do
|
|
558
|
+
let(:temp_dir) { Dir.mktmpdir('test_split_') }
|
|
559
|
+
let(:image_file) { File.join(temp_dir, 'spritesheet.png') }
|
|
560
|
+
|
|
561
|
+
before do
|
|
562
|
+
FileUtils.touch(image_file)
|
|
563
|
+
|
|
564
|
+
allow_any_instance_of(RubySpriter::DependencyChecker).to receive(:check_all).and_return({
|
|
565
|
+
ffmpeg: { available: true },
|
|
566
|
+
ffprobe: { available: true },
|
|
567
|
+
imagemagick: { available: true },
|
|
568
|
+
gimp: { available: true }
|
|
569
|
+
})
|
|
570
|
+
allow_any_instance_of(RubySpriter::DependencyChecker).to receive(:gimp_path).and_return('/usr/bin/gimp')
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
after do
|
|
574
|
+
FileUtils.rm_rf(temp_dir)
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
context 'when image has metadata' do
|
|
578
|
+
before do
|
|
579
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(image_file).and_return({
|
|
580
|
+
columns: 4,
|
|
581
|
+
rows: 4,
|
|
582
|
+
frames: 16
|
|
583
|
+
})
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
it 'uses metadata when --split not provided' do
|
|
587
|
+
processor = described_class.new(image: image_file, save_frames: true)
|
|
588
|
+
|
|
589
|
+
splitter = instance_double(RubySpriter::Utils::SpritesheetSplitter)
|
|
590
|
+
allow(RubySpriter::Utils::SpritesheetSplitter).to receive(:new).and_return(splitter)
|
|
591
|
+
expect(splitter).to receive(:split_into_frames).with(image_file, anything, 4, 4, 16)
|
|
592
|
+
|
|
593
|
+
processor.run
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
it 'warns and uses metadata when --split provided without --override-md' do
|
|
597
|
+
processor = described_class.new(image: image_file, split: '5:5')
|
|
598
|
+
|
|
599
|
+
splitter = instance_double(RubySpriter::Utils::SpritesheetSplitter)
|
|
600
|
+
allow(RubySpriter::Utils::SpritesheetSplitter).to receive(:new).and_return(splitter)
|
|
601
|
+
expect(splitter).to receive(:split_into_frames).with(image_file, anything, 4, 4, 16)
|
|
602
|
+
|
|
603
|
+
expect(RubySpriter::Utils::OutputFormatter).to receive(:note).with(/Image has metadata.*Your --split values will be ignored/)
|
|
604
|
+
|
|
605
|
+
processor.run
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
it 'uses --split when --override-md provided' do
|
|
609
|
+
processor = described_class.new(image: image_file, split: '5:5', override_md: true)
|
|
610
|
+
|
|
611
|
+
# Mock ImageMagick identify for dimension validation
|
|
612
|
+
allow(Open3).to receive(:capture3).with(/identify/).and_return(
|
|
613
|
+
["500x500\n", '', instance_double(Process::Status, success?: true)]
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
splitter = instance_double(RubySpriter::Utils::SpritesheetSplitter)
|
|
617
|
+
allow(RubySpriter::Utils::SpritesheetSplitter).to receive(:new).and_return(splitter)
|
|
618
|
+
expect(splitter).to receive(:split_into_frames).with(image_file, anything, 5, 5, 25)
|
|
619
|
+
|
|
620
|
+
processor.run
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
context 'when image has no metadata' do
|
|
625
|
+
before do
|
|
626
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(image_file).and_return(nil)
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
it 'uses --split values when provided' do
|
|
630
|
+
processor = described_class.new(image: image_file, split: '6:6')
|
|
631
|
+
|
|
632
|
+
# Mock ImageMagick identify for dimension validation
|
|
633
|
+
allow(Open3).to receive(:capture3).with(/identify/).and_return(
|
|
634
|
+
["600x600\n", '', instance_double(Process::Status, success?: true)]
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
splitter = instance_double(RubySpriter::Utils::SpritesheetSplitter)
|
|
638
|
+
allow(RubySpriter::Utils::SpritesheetSplitter).to receive(:new).and_return(splitter)
|
|
639
|
+
expect(splitter).to receive(:split_into_frames).with(image_file, anything, 6, 6, 36)
|
|
640
|
+
|
|
641
|
+
processor.run
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
it 'raises error when --split not provided' do
|
|
645
|
+
processor = described_class.new(image: image_file, save_frames: true)
|
|
646
|
+
|
|
647
|
+
expect {
|
|
648
|
+
processor.run
|
|
649
|
+
}.to raise_error(RubySpriter::ValidationError, /Image has no metadata.*Please provide --split/)
|
|
650
|
+
end
|
|
651
|
+
end
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
describe 'frame extraction with --save-frames' do
|
|
655
|
+
let(:temp_dir) { Dir.mktmpdir('test_') }
|
|
656
|
+
let(:video_file) { File.join(temp_dir, 'test.mp4') }
|
|
657
|
+
let(:spritesheet_file) { File.join(temp_dir, 'spritesheet.png') }
|
|
658
|
+
|
|
659
|
+
before do
|
|
660
|
+
FileUtils.touch(video_file)
|
|
661
|
+
FileUtils.touch(spritesheet_file)
|
|
662
|
+
|
|
663
|
+
allow_any_instance_of(RubySpriter::DependencyChecker).to receive(:check_all).and_return({
|
|
664
|
+
ffmpeg: { available: true },
|
|
665
|
+
ffprobe: { available: true },
|
|
666
|
+
imagemagick: { available: true },
|
|
667
|
+
gimp: { available: true }
|
|
668
|
+
})
|
|
669
|
+
allow_any_instance_of(RubySpriter::DependencyChecker).to receive(:gimp_path).and_return('/usr/bin/gimp')
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
after do
|
|
673
|
+
FileUtils.rm_rf(temp_dir)
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
it 'splits spritesheet into frames after video processing when save_frames is true' do
|
|
677
|
+
processor = described_class.new(video: video_file, save_frames: true)
|
|
678
|
+
|
|
679
|
+
# The output file will be generated from video filename
|
|
680
|
+
expected_output = File.join(temp_dir, 'test_spritesheet.png')
|
|
681
|
+
|
|
682
|
+
video_processor = instance_double(RubySpriter::VideoProcessor)
|
|
683
|
+
allow(RubySpriter::VideoProcessor).to receive(:new).and_return(video_processor)
|
|
684
|
+
allow(video_processor).to receive(:create_spritesheet).and_return({
|
|
685
|
+
output_file: expected_output,
|
|
686
|
+
columns: 4,
|
|
687
|
+
rows: 4,
|
|
688
|
+
frames: 16
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
splitter = instance_double(RubySpriter::Utils::SpritesheetSplitter)
|
|
692
|
+
allow(RubySpriter::Utils::SpritesheetSplitter).to receive(:new).and_return(splitter)
|
|
693
|
+
expect(splitter).to receive(:split_into_frames).with(expected_output, anything, 4, 4, 16)
|
|
694
|
+
|
|
695
|
+
processor.run
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
it 'splits spritesheet into frames after image processing when save_frames is true' do
|
|
699
|
+
processor = described_class.new(image: spritesheet_file, save_frames: true)
|
|
700
|
+
|
|
701
|
+
# Mock metadata reading
|
|
702
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(spritesheet_file).and_return({
|
|
703
|
+
columns: 4,
|
|
704
|
+
rows: 4,
|
|
705
|
+
frames: 16
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
splitter = instance_double(RubySpriter::Utils::SpritesheetSplitter)
|
|
709
|
+
allow(RubySpriter::Utils::SpritesheetSplitter).to receive(:new).and_return(splitter)
|
|
710
|
+
expect(splitter).to receive(:split_into_frames).with(spritesheet_file, anything, 4, 4, 16)
|
|
711
|
+
|
|
712
|
+
processor.run
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
it 'does not split frames when save_frames is false' do
|
|
716
|
+
processor = described_class.new(video: video_file, save_frames: false)
|
|
717
|
+
|
|
718
|
+
# The output file will be generated from video filename
|
|
719
|
+
expected_output = File.join(temp_dir, 'test_spritesheet.png')
|
|
720
|
+
|
|
721
|
+
video_processor = instance_double(RubySpriter::VideoProcessor)
|
|
722
|
+
allow(RubySpriter::VideoProcessor).to receive(:new).and_return(video_processor)
|
|
723
|
+
allow(video_processor).to receive(:create_spritesheet).and_return({
|
|
724
|
+
output_file: expected_output,
|
|
725
|
+
columns: 4,
|
|
726
|
+
rows: 4,
|
|
727
|
+
frames: 16
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
expect(RubySpriter::Utils::SpritesheetSplitter).not_to receive(:new)
|
|
731
|
+
|
|
732
|
+
processor.run
|
|
733
|
+
end
|
|
734
|
+
end
|
|
735
|
+
end
|