ruby_spriter 0.6.5 → 0.6.6

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,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'fileutils'
5
+
6
+ module RubySpriter
7
+ module Utils
8
+ # Splits a spritesheet into individual frame images
9
+ class SpritesheetSplitter
10
+ # Split spritesheet into individual frames
11
+ # @param spritesheet_file [String] Path to spritesheet PNG
12
+ # @param output_dir [String] Directory to save individual frames
13
+ # @param columns [Integer] Number of columns in grid
14
+ # @param rows [Integer] Number of rows in grid
15
+ # @param frames [Integer] Total number of frames to extract
16
+ def split_into_frames(spritesheet_file, output_dir, columns, rows, frames)
17
+ FileUtils.mkdir_p(output_dir)
18
+
19
+ OutputFormatter.header("Extracting Frames")
20
+ OutputFormatter.indent("Splitting spritesheet into #{frames} frames to disk...")
21
+ OutputFormatter.indent("Output directory: #{output_dir}")
22
+
23
+ # Get spritesheet dimensions
24
+ dimensions = get_image_dimensions(spritesheet_file)
25
+ tile_width = dimensions[:width] / columns
26
+ tile_height = dimensions[:height] / rows
27
+
28
+ # Extract each frame
29
+ spritesheet_basename = File.basename(spritesheet_file, '.*')
30
+
31
+ frames.times do |i|
32
+ frame_number = i + 1
33
+ row = i / columns
34
+ col = i % columns
35
+
36
+ x_offset = col * tile_width
37
+ y_offset = row * tile_height
38
+
39
+ frame_filename = "FR#{format('%03d', frame_number)}_#{spritesheet_basename}.png"
40
+ frame_path = File.join(output_dir, frame_filename)
41
+
42
+ extract_tile(spritesheet_file, frame_path, tile_width, tile_height, x_offset, y_offset)
43
+ end
44
+
45
+ OutputFormatter.indent("✅ Frames extracted successfully\n")
46
+ end
47
+
48
+ private
49
+
50
+ def get_image_dimensions(image_file)
51
+ cmd = [
52
+ 'magick',
53
+ 'identify',
54
+ '-format', '%wx%h',
55
+ PathHelper.quote_path(image_file)
56
+ ].join(' ')
57
+
58
+ stdout, stderr, status = Open3.capture3(cmd)
59
+
60
+ unless status.success?
61
+ raise ProcessingError, "Could not get image dimensions: #{stderr}"
62
+ end
63
+
64
+ width, height = stdout.strip.split('x').map(&:to_i)
65
+ { width: width, height: height }
66
+ end
67
+
68
+ def extract_tile(source_file, output_file, width, height, x_offset, y_offset)
69
+ cmd = [
70
+ 'magick',
71
+ 'convert',
72
+ PathHelper.quote_path(source_file),
73
+ '-crop', "#{width}x#{height}+#{x_offset}+#{y_offset}",
74
+ '+repage',
75
+ PathHelper.quote_path(output_file)
76
+ ].join(' ')
77
+
78
+ stdout, stderr, status = Open3.capture3(cmd)
79
+
80
+ unless status.success?
81
+ raise ProcessingError, "Could not extract frame: #{stderr}"
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubySpriter
4
- VERSION = '0.6.5'
4
+ VERSION = '0.6.6'
5
5
  VERSION_DATE = '2025-10-23'
6
6
  METADATA_VERSION = '0.6'
7
7
  end
@@ -27,26 +27,26 @@ module RubySpriter
27
27
  rows = (frame_count.to_f / columns).ceil
28
28
 
29
29
  Utils::OutputFormatter.header("Creating Spritesheet")
30
-
30
+
31
31
  temp_file = output_file.sub('.png', '_temp.png')
32
-
32
+
33
33
  create_with_ffmpeg(video_file, temp_file, duration, columns, rows, frame_count)
34
-
34
+
35
35
  # Embed metadata
36
36
  MetadataManager.embed(
37
- temp_file,
37
+ temp_file,
38
38
  output_file,
39
39
  columns: columns,
40
40
  rows: rows,
41
41
  frames: frame_count,
42
42
  debug: options[:debug]
43
43
  )
44
-
44
+
45
45
  # Clean up temp file
46
46
  File.delete(temp_file) if File.exist?(temp_file)
47
-
47
+
48
48
  file_size = File.size(output_file)
49
-
49
+
50
50
  # Display results with Godot instructions
51
51
  display_spritesheet_results(output_file, file_size, columns, rows, frame_count)
52
52
 
data/lib/ruby_spriter.rb CHANGED
@@ -15,6 +15,7 @@ require_relative 'ruby_spriter/version'
15
15
  require_relative 'ruby_spriter/utils/path_helper'
16
16
  require_relative 'ruby_spriter/utils/file_helper'
17
17
  require_relative 'ruby_spriter/utils/output_formatter'
18
+ require_relative 'ruby_spriter/utils/spritesheet_splitter'
18
19
 
19
20
  # Load core components
20
21
  require_relative 'ruby_spriter/platform'
@@ -155,6 +155,32 @@ RSpec.describe RubySpriter::CLI do
155
155
  end.to raise_error(SystemExit)
156
156
  end
157
157
  end
158
+
159
+ describe '--overwrite flag' do
160
+ it 'sets overwrite option to true' do
161
+ processor_double = instance_double(RubySpriter::Processor)
162
+ allow(processor_double).to receive(:run)
163
+
164
+ allow(RubySpriter::Processor).to receive(:new) do |options|
165
+ expect(options[:overwrite]).to eq(true)
166
+ processor_double
167
+ end
168
+
169
+ described_class.start(['--video', 'test.mp4', '--overwrite'])
170
+ end
171
+
172
+ it 'defaults to false when not specified' do
173
+ processor_double = instance_double(RubySpriter::Processor)
174
+ allow(processor_double).to receive(:run)
175
+
176
+ allow(RubySpriter::Processor).to receive(:new) do |options|
177
+ expect(options[:overwrite]).to be_nil
178
+ processor_double
179
+ end
180
+
181
+ described_class.start(['--video', 'test.mp4'])
182
+ end
183
+ end
158
184
  end
159
185
 
160
186
  describe '--image flag' do
@@ -425,6 +451,225 @@ RSpec.describe RubySpriter::CLI do
425
451
 
426
452
  described_class.start(['--image', fixture_with_meta, '--output', 'custom_output.png'])
427
453
  end
454
+
455
+ it 'works with --overwrite option' do
456
+ processor_double = instance_double(RubySpriter::Processor)
457
+ allow(processor_double).to receive(:run)
458
+
459
+ allow(RubySpriter::Processor).to receive(:new) do |options|
460
+ expect(options[:image]).to eq(fixture_with_meta)
461
+ expect(options[:remove_bg]).to eq(true)
462
+ expect(options[:overwrite]).to eq(true)
463
+ processor_double
464
+ end
465
+
466
+ described_class.start(['--image', fixture_with_meta, '--remove-bg', '--overwrite'])
467
+ end
468
+
469
+ it 'works with --overwrite and --output options combined' do
470
+ processor_double = instance_double(RubySpriter::Processor)
471
+ allow(processor_double).to receive(:run)
472
+
473
+ allow(RubySpriter::Processor).to receive(:new) do |options|
474
+ expect(options[:image]).to eq(fixture_with_meta)
475
+ expect(options[:scale_percent]).to eq(50)
476
+ expect(options[:output]).to eq('custom.png')
477
+ expect(options[:overwrite]).to eq(true)
478
+ processor_double
479
+ end
480
+
481
+ described_class.start(['--image', fixture_with_meta, '--scale', '50', '--output', 'custom.png', '--overwrite'])
482
+ end
483
+ end
484
+
485
+ describe 'output filename behavior with processing' do
486
+ it 'generates unique filename by default when processing without --output' do
487
+ # Mock all dependencies
488
+ allow(RubySpriter::Utils::FileHelper).to receive(:validate_exists!)
489
+ allow(RubySpriter::Utils::FileHelper).to receive(:validate_readable!)
490
+
491
+ # Mock GimpProcessor to return a processed file
492
+ gimp_double = instance_double(RubySpriter::GimpProcessor)
493
+ allow(RubySpriter::GimpProcessor).to receive(:new).and_return(gimp_double)
494
+ allow(gimp_double).to receive(:process).and_return('input-nobg-fuzzy_20251023_123456_789.png')
495
+
496
+ processor = RubySpriter::Processor.new(
497
+ image: fixture_with_meta,
498
+ remove_bg: true,
499
+ overwrite: false
500
+ )
501
+
502
+ allow(processor).to receive(:check_dependencies!)
503
+ allow(processor).to receive(:setup_temp_directory)
504
+ allow(processor).to receive(:cleanup)
505
+ allow(processor).to receive(:gimp_path).and_return('/usr/bin/gimp')
506
+
507
+ result = nil
508
+ expect { result = processor.run }.to output(/SUCCESS/).to_stdout
509
+
510
+ # Should return the uniquely-named file from GIMP processing
511
+ expect(result[:output_file]).to match(/-nobg-fuzzy.*\.png$/)
512
+ end
513
+
514
+ it 'overwrites output file when --overwrite is specified' do
515
+ # Mock all dependencies
516
+ allow(RubySpriter::Utils::FileHelper).to receive(:validate_exists!)
517
+ allow(RubySpriter::Utils::FileHelper).to receive(:validate_readable!)
518
+
519
+ # Mock GimpProcessor - with overwrite:true, it should return same filename
520
+ gimp_double = instance_double(RubySpriter::GimpProcessor)
521
+ allow(RubySpriter::GimpProcessor).to receive(:new).and_return(gimp_double)
522
+ allow(gimp_double).to receive(:process).and_return('input-scaled-50pct.png')
523
+
524
+ processor = RubySpriter::Processor.new(
525
+ image: fixture_with_meta,
526
+ scale_percent: 50,
527
+ overwrite: true
528
+ )
529
+
530
+ allow(processor).to receive(:check_dependencies!)
531
+ allow(processor).to receive(:setup_temp_directory)
532
+ allow(processor).to receive(:cleanup)
533
+ allow(processor).to receive(:gimp_path).and_return('/usr/bin/gimp')
534
+
535
+ result = nil
536
+ expect { result = processor.run }.to output(/SUCCESS/).to_stdout
537
+
538
+ # Should return the base filename (no timestamp)
539
+ expect(result[:output_file]).to eq('input-scaled-50pct.png')
540
+ end
541
+
542
+ it 'generates unique output filename when --output is used without --overwrite' do
543
+ # Mock all dependencies
544
+ allow(RubySpriter::Utils::FileHelper).to receive(:validate_exists!)
545
+ allow(RubySpriter::Utils::FileHelper).to receive(:validate_readable!)
546
+
547
+ # Mock ensure_unique_output to verify it's called correctly
548
+ allow(RubySpriter::Utils::FileHelper).to receive(:ensure_unique_output) do |path, overwrite:|
549
+ expect(path).to eq('custom_output.png')
550
+ expect(overwrite).to eq(false)
551
+ 'custom_output_20251023_123456_789.png'
552
+ end
553
+
554
+ # Mock GimpProcessor
555
+ gimp_double = instance_double(RubySpriter::GimpProcessor)
556
+ allow(RubySpriter::GimpProcessor).to receive(:new).and_return(gimp_double)
557
+ allow(gimp_double).to receive(:process).and_return('temp-processed.png')
558
+
559
+ # Mock file operations
560
+ allow(FileUtils).to receive(:cp)
561
+
562
+ processor = RubySpriter::Processor.new(
563
+ image: fixture_with_meta,
564
+ remove_bg: true,
565
+ output: 'custom_output.png',
566
+ overwrite: false
567
+ )
568
+
569
+ allow(processor).to receive(:check_dependencies!)
570
+ allow(processor).to receive(:setup_temp_directory)
571
+ allow(processor).to receive(:cleanup)
572
+ allow(processor).to receive(:gimp_path).and_return('/usr/bin/gimp')
573
+
574
+ result = nil
575
+ expect { result = processor.run }.to output(/SUCCESS/).to_stdout
576
+
577
+ # Should return unique filename
578
+ expect(result[:output_file]).to match(/custom_output_\d{8}_\d{6}_\d{3}\.png$/)
579
+ end
580
+
581
+ it 'uses exact output filename when --output and --overwrite are both specified' do
582
+ # Mock all dependencies
583
+ allow(RubySpriter::Utils::FileHelper).to receive(:validate_exists!)
584
+ allow(RubySpriter::Utils::FileHelper).to receive(:validate_readable!)
585
+
586
+ # Mock ensure_unique_output to verify it's called with overwrite:true
587
+ allow(RubySpriter::Utils::FileHelper).to receive(:ensure_unique_output) do |path, overwrite:|
588
+ expect(path).to eq('exact_output.png')
589
+ expect(overwrite).to eq(true)
590
+ 'exact_output.png'
591
+ end
592
+
593
+ # Mock GimpProcessor
594
+ gimp_double = instance_double(RubySpriter::GimpProcessor)
595
+ allow(RubySpriter::GimpProcessor).to receive(:new).and_return(gimp_double)
596
+ allow(gimp_double).to receive(:process).and_return('temp-processed.png')
597
+
598
+ # Mock file operations
599
+ allow(FileUtils).to receive(:cp)
600
+
601
+ processor = RubySpriter::Processor.new(
602
+ image: fixture_with_meta,
603
+ scale_percent: 50,
604
+ output: 'exact_output.png',
605
+ overwrite: true
606
+ )
607
+
608
+ allow(processor).to receive(:check_dependencies!)
609
+ allow(processor).to receive(:setup_temp_directory)
610
+ allow(processor).to receive(:cleanup)
611
+ allow(processor).to receive(:gimp_path).and_return('/usr/bin/gimp')
612
+
613
+ result = nil
614
+ expect { result = processor.run }.to output(/SUCCESS/).to_stdout
615
+
616
+ # Should return exact filename (no timestamp)
617
+ expect(result[:output_file]).to eq('exact_output.png')
618
+ end
619
+
620
+ it 'generates unique filename when using --sharpen alone without --output' do
621
+ # Mock all dependencies
622
+ allow(RubySpriter::Utils::FileHelper).to receive(:validate_exists!)
623
+ allow(RubySpriter::Utils::FileHelper).to receive(:validate_readable!)
624
+
625
+ # Mock GimpProcessor to return a sharpened file
626
+ gimp_double = instance_double(RubySpriter::GimpProcessor)
627
+ allow(RubySpriter::GimpProcessor).to receive(:new).and_return(gimp_double)
628
+ allow(gimp_double).to receive(:process).and_return('input-sharpened_20251023_123456_789.png')
629
+
630
+ processor = RubySpriter::Processor.new(
631
+ image: fixture_with_meta,
632
+ sharpen: true,
633
+ overwrite: false
634
+ )
635
+
636
+ allow(processor).to receive(:check_dependencies!)
637
+ allow(processor).to receive(:setup_temp_directory)
638
+ allow(processor).to receive(:cleanup)
639
+
640
+ result = nil
641
+ expect { result = processor.run }.to output(/SUCCESS/).to_stdout
642
+
643
+ # Should return the uniquely-named sharpened file
644
+ expect(result[:output_file]).to match(/-sharpened.*\.png$/)
645
+ end
646
+
647
+ it 'overwrites sharpened file when --sharpen with --overwrite' do
648
+ # Mock all dependencies
649
+ allow(RubySpriter::Utils::FileHelper).to receive(:validate_exists!)
650
+ allow(RubySpriter::Utils::FileHelper).to receive(:validate_readable!)
651
+
652
+ # Mock GimpProcessor - with overwrite:true, should return base filename
653
+ gimp_double = instance_double(RubySpriter::GimpProcessor)
654
+ allow(RubySpriter::GimpProcessor).to receive(:new).and_return(gimp_double)
655
+ allow(gimp_double).to receive(:process).and_return('input-sharpened.png')
656
+
657
+ processor = RubySpriter::Processor.new(
658
+ image: fixture_with_meta,
659
+ sharpen: true,
660
+ overwrite: true
661
+ )
662
+
663
+ allow(processor).to receive(:check_dependencies!)
664
+ allow(processor).to receive(:setup_temp_directory)
665
+ allow(processor).to receive(:cleanup)
666
+
667
+ result = nil
668
+ expect { result = processor.run }.to output(/SUCCESS/).to_stdout
669
+
670
+ # Should return the base filename (no timestamp)
671
+ expect(result[:output_file]).to eq('input-sharpened.png')
672
+ end
428
673
  end
429
674
  end
430
675
 
@@ -763,6 +1008,19 @@ RSpec.describe RubySpriter::CLI do
763
1008
 
764
1009
  described_class.start(['--video', fixture_video, '--output', 'custom_spritesheet.png'])
765
1010
  end
1011
+
1012
+ it 'works with --save-frames option' do
1013
+ processor_double = instance_double(RubySpriter::Processor)
1014
+ allow(processor_double).to receive(:run)
1015
+
1016
+ allow(RubySpriter::Processor).to receive(:new) do |options|
1017
+ expect(options[:video]).to eq(fixture_video)
1018
+ expect(options[:save_frames]).to eq(true)
1019
+ processor_double
1020
+ end
1021
+
1022
+ described_class.start(['--video', fixture_video, '--save-frames'])
1023
+ end
766
1024
  end
767
1025
 
768
1026
  describe 'preset configurations' do
@@ -1117,6 +1375,83 @@ RSpec.describe RubySpriter::CLI do
1117
1375
  '--debug'
1118
1376
  ])
1119
1377
  end
1378
+
1379
+ it 'works with --overwrite option' do
1380
+ processor_double = instance_double(RubySpriter::Processor)
1381
+ allow(processor_double).to receive(:run)
1382
+
1383
+ allow(RubySpriter::Processor).to receive(:new) do |options|
1384
+ expect(options[:consolidate]).to eq([spritesheet_4x2, spritesheet_6x2])
1385
+ expect(options[:overwrite]).to eq(true)
1386
+ processor_double
1387
+ end
1388
+
1389
+ described_class.start(['--consolidate', "#{spritesheet_4x2},#{spritesheet_6x2}", '--overwrite'])
1390
+ end
1391
+ end
1392
+
1393
+ describe 'default output filename behavior' do
1394
+ it 'generates consolidated_spritesheet.png when no --output specified' do
1395
+ # Mock all the dependencies
1396
+ allow(RubySpriter::Utils::FileHelper).to receive(:validate_exists!)
1397
+ allow(RubySpriter::Utils::FileHelper).to receive(:ensure_unique_output) do |path, overwrite:|
1398
+ expect(path).to eq('consolidated_spritesheet.png')
1399
+ expect(overwrite).to eq(false)
1400
+ 'consolidated_spritesheet.png'
1401
+ end
1402
+
1403
+ consolidator_double = instance_double(RubySpriter::Consolidator)
1404
+ allow(RubySpriter::Consolidator).to receive(:new).and_return(consolidator_double)
1405
+ allow(consolidator_double).to receive(:consolidate).and_return({
1406
+ output_file: 'consolidated_spritesheet.png',
1407
+ columns: 2,
1408
+ rows: 4,
1409
+ frames: 8
1410
+ })
1411
+
1412
+ processor = RubySpriter::Processor.new(
1413
+ consolidate: [spritesheet_4x2, spritesheet_6x2],
1414
+ overwrite: false
1415
+ )
1416
+
1417
+ allow(processor).to receive(:check_dependencies!)
1418
+ allow(processor).to receive(:setup_temp_directory)
1419
+ allow(processor).to receive(:cleanup)
1420
+
1421
+ # Capture output to suppress console messages
1422
+ expect { processor.run }.to output(/SUCCESS/).to_stdout
1423
+ end
1424
+
1425
+ it 'respects --overwrite flag with default filename' do
1426
+ # Mock all the dependencies
1427
+ allow(RubySpriter::Utils::FileHelper).to receive(:validate_exists!)
1428
+ allow(RubySpriter::Utils::FileHelper).to receive(:ensure_unique_output) do |path, overwrite:|
1429
+ expect(path).to eq('consolidated_spritesheet.png')
1430
+ expect(overwrite).to eq(true)
1431
+ 'consolidated_spritesheet.png'
1432
+ end
1433
+
1434
+ consolidator_double = instance_double(RubySpriter::Consolidator)
1435
+ allow(RubySpriter::Consolidator).to receive(:new).and_return(consolidator_double)
1436
+ allow(consolidator_double).to receive(:consolidate).and_return({
1437
+ output_file: 'consolidated_spritesheet.png',
1438
+ columns: 2,
1439
+ rows: 4,
1440
+ frames: 8
1441
+ })
1442
+
1443
+ processor = RubySpriter::Processor.new(
1444
+ consolidate: [spritesheet_4x2, spritesheet_6x2],
1445
+ overwrite: true
1446
+ )
1447
+
1448
+ allow(processor).to receive(:check_dependencies!)
1449
+ allow(processor).to receive(:setup_temp_directory)
1450
+ allow(processor).to receive(:cleanup)
1451
+
1452
+ # Capture output to suppress console messages
1453
+ expect { processor.run }.to output(/SUCCESS/).to_stdout
1454
+ end
1120
1455
  end
1121
1456
  end
1122
1457
 
@@ -1138,5 +1473,33 @@ RSpec.describe RubySpriter::CLI do
1138
1473
  end.to raise_error(SystemExit)
1139
1474
  end
1140
1475
  end
1476
+
1477
+ describe '--split option' do
1478
+ it 'parses split option with R:C format' do
1479
+ processor_double = instance_double(RubySpriter::Processor)
1480
+ allow(processor_double).to receive(:run)
1481
+
1482
+ allow(RubySpriter::Processor).to receive(:new) do |options|
1483
+ expect(options[:split]).to eq('4:4')
1484
+ processor_double
1485
+ end
1486
+
1487
+ described_class.start(['--image', 'test.png', '--split', '4:4'])
1488
+ end
1489
+ end
1490
+
1491
+ describe '--override-md option' do
1492
+ it 'sets override_md option to true' do
1493
+ processor_double = instance_double(RubySpriter::Processor)
1494
+ allow(processor_double).to receive(:run)
1495
+
1496
+ allow(RubySpriter::Processor).to receive(:new) do |options|
1497
+ expect(options[:override_md]).to eq(true)
1498
+ processor_double
1499
+ end
1500
+
1501
+ described_class.start(['--image', 'test.png', '--split', '4:4', '--override-md'])
1502
+ end
1503
+ end
1141
1504
  end
1142
1505
  end