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.
@@ -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