ruby_spriter 0.6.6 → 0.6.7.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/CHANGELOG.md +257 -0
- data/README.md +384 -33
- data/lib/ruby_spriter/batch_processor.rb +214 -0
- data/lib/ruby_spriter/cli.rb +355 -8
- data/lib/ruby_spriter/compression_manager.rb +101 -0
- data/lib/ruby_spriter/consolidator.rb +33 -0
- data/lib/ruby_spriter/dependency_checker.rb +65 -15
- data/lib/ruby_spriter/gimp_processor.rb +395 -4
- data/lib/ruby_spriter/platform.rb +56 -1
- data/lib/ruby_spriter/processor.rb +419 -9
- data/lib/ruby_spriter/version.rb +2 -2
- data/lib/ruby_spriter.rb +2 -0
- data/spec/ruby_spriter/batch_processor_spec.rb +200 -0
- data/spec/ruby_spriter/cli_spec.rb +387 -0
- data/spec/ruby_spriter/compression_manager_spec.rb +157 -0
- data/spec/ruby_spriter/consolidator_spec.rb +163 -0
- data/spec/ruby_spriter/platform_spec.rb +11 -1
- data/spec/ruby_spriter/processor_spec.rb +350 -0
- metadata +6 -2
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RubySpriter::CompressionManager do
|
|
6
|
+
let(:input_file) { 'E:/test/input.png' }
|
|
7
|
+
let(:output_file) { 'E:/test/output.png' }
|
|
8
|
+
let(:temp_file) { 'E:/test/temp.png' }
|
|
9
|
+
|
|
10
|
+
before do
|
|
11
|
+
allow(File).to receive(:exist?).and_return(true)
|
|
12
|
+
allow(File).to receive(:readable?).and_return(true)
|
|
13
|
+
allow(File).to receive(:size).with(input_file).and_return(100000)
|
|
14
|
+
allow(File).to receive(:size).with(output_file).and_return(80000)
|
|
15
|
+
allow(RubySpriter::Utils::FileHelper).to receive(:validate_readable!)
|
|
16
|
+
allow(RubySpriter::Utils::FileHelper).to receive(:validate_exists!)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe '.compress' do
|
|
20
|
+
it 'compresses PNG file using ImageMagick' do
|
|
21
|
+
magick_cmd = RubySpriter::Platform.imagemagick_convert_cmd
|
|
22
|
+
|
|
23
|
+
expect(Open3).to receive(:capture3) do |cmd|
|
|
24
|
+
expect(cmd).to include(magick_cmd.split.first) # Check for 'convert' or 'magick'
|
|
25
|
+
expect(cmd).to include(input_file)
|
|
26
|
+
expect(cmd).to include(output_file)
|
|
27
|
+
expect(cmd).to include('-strip')
|
|
28
|
+
expect(cmd).to include('-define png:compression-level=9')
|
|
29
|
+
expect(cmd).to include('-define png:compression-filter=5')
|
|
30
|
+
expect(cmd).to include('-define png:compression-strategy=1')
|
|
31
|
+
['', '', double(success?: true)]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
described_class.compress(input_file, output_file)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'raises error if compression fails' do
|
|
38
|
+
allow(Open3).to receive(:capture3).and_return(['', 'error', double(success?: false)])
|
|
39
|
+
|
|
40
|
+
expect {
|
|
41
|
+
described_class.compress(input_file, output_file)
|
|
42
|
+
}.to raise_error(RubySpriter::ProcessingError, /Failed to compress/)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'validates input file exists and is readable' do
|
|
46
|
+
expect(RubySpriter::Utils::FileHelper).to receive(:validate_readable!).with(input_file)
|
|
47
|
+
|
|
48
|
+
allow(Open3).to receive(:capture3).and_return(['', '', double(success?: true)])
|
|
49
|
+
|
|
50
|
+
described_class.compress(input_file, output_file)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'validates output file was created' do
|
|
54
|
+
allow(Open3).to receive(:capture3).and_return(['', '', double(success?: true)])
|
|
55
|
+
expect(RubySpriter::Utils::FileHelper).to receive(:validate_exists!).with(output_file)
|
|
56
|
+
|
|
57
|
+
described_class.compress(input_file, output_file)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
describe '.compress_with_metadata' do
|
|
62
|
+
let(:metadata) { { columns: 4, rows: 4, frames: 16 } }
|
|
63
|
+
|
|
64
|
+
before do
|
|
65
|
+
allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata)
|
|
66
|
+
allow(RubySpriter::MetadataManager).to receive(:embed)
|
|
67
|
+
allow(FileUtils).to receive(:mv)
|
|
68
|
+
allow(File).to receive(:exist?).with(temp_file).and_return(true)
|
|
69
|
+
allow(FileUtils).to receive(:rm_f)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'reads metadata before compression' do
|
|
73
|
+
expect(RubySpriter::MetadataManager).to receive(:read).with(input_file)
|
|
74
|
+
|
|
75
|
+
allow(Open3).to receive(:capture3).and_return(['', '', double(success?: true)])
|
|
76
|
+
|
|
77
|
+
described_class.compress_with_metadata(input_file, output_file)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'compresses file with metadata preservation' do
|
|
81
|
+
allow(Open3).to receive(:capture3).and_return(['', '', double(success?: true)])
|
|
82
|
+
|
|
83
|
+
expect(described_class).to receive(:compress).with(input_file, anything, debug: false)
|
|
84
|
+
expect(RubySpriter::MetadataManager).to receive(:embed)
|
|
85
|
+
|
|
86
|
+
described_class.compress_with_metadata(input_file, output_file)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 're-embeds metadata after compression' do
|
|
90
|
+
allow(Open3).to receive(:capture3).and_return(['', '', double(success?: true)])
|
|
91
|
+
|
|
92
|
+
expect(RubySpriter::MetadataManager).to receive(:embed) do |temp, output, **opts|
|
|
93
|
+
expect(opts[:columns]).to eq(4)
|
|
94
|
+
expect(opts[:rows]).to eq(4)
|
|
95
|
+
expect(opts[:frames]).to eq(16)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
described_class.compress_with_metadata(input_file, output_file)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'handles files without metadata' do
|
|
102
|
+
allow(RubySpriter::MetadataManager).to receive(:read).and_return(nil)
|
|
103
|
+
allow(Open3).to receive(:capture3).and_return(['', '', double(success?: true)])
|
|
104
|
+
|
|
105
|
+
# Should just compress without re-embedding metadata
|
|
106
|
+
expect(RubySpriter::MetadataManager).not_to receive(:embed)
|
|
107
|
+
|
|
108
|
+
described_class.compress_with_metadata(input_file, output_file)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'cleans up temp file after successful compression' do
|
|
112
|
+
allow(Open3).to receive(:capture3).and_return(['', '', double(success?: true)])
|
|
113
|
+
allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata)
|
|
114
|
+
allow(RubySpriter::MetadataManager).to receive(:embed)
|
|
115
|
+
|
|
116
|
+
# Simulate temp file creation
|
|
117
|
+
temp_path = nil
|
|
118
|
+
allow(described_class).to receive(:compress) do |input, output|
|
|
119
|
+
temp_path = output
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
expect(FileUtils).to receive(:rm_f).with(anything)
|
|
123
|
+
|
|
124
|
+
described_class.compress_with_metadata(input_file, output_file)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
describe '.compression_stats' do
|
|
129
|
+
it 'returns compression statistics' do
|
|
130
|
+
original_size = 100000
|
|
131
|
+
compressed_size = 80000
|
|
132
|
+
|
|
133
|
+
allow(File).to receive(:size).with(input_file).and_return(original_size)
|
|
134
|
+
allow(File).to receive(:size).with(output_file).and_return(compressed_size)
|
|
135
|
+
|
|
136
|
+
stats = described_class.compression_stats(input_file, output_file)
|
|
137
|
+
|
|
138
|
+
expect(stats[:original_size]).to eq(original_size)
|
|
139
|
+
expect(stats[:compressed_size]).to eq(compressed_size)
|
|
140
|
+
expect(stats[:saved_bytes]).to eq(20000)
|
|
141
|
+
expect(stats[:reduction_percent]).to be_within(0.1).of(20.0)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it 'handles case where compressed file is larger' do
|
|
145
|
+
original_size = 100000
|
|
146
|
+
compressed_size = 120000
|
|
147
|
+
|
|
148
|
+
allow(File).to receive(:size).with(input_file).and_return(original_size)
|
|
149
|
+
allow(File).to receive(:size).with(output_file).and_return(compressed_size)
|
|
150
|
+
|
|
151
|
+
stats = described_class.compression_stats(input_file, output_file)
|
|
152
|
+
|
|
153
|
+
expect(stats[:saved_bytes]).to eq(-20000)
|
|
154
|
+
expect(stats[:reduction_percent]).to be_within(0.1).of(-20.0)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
|
+
require 'securerandom'
|
|
4
5
|
|
|
5
6
|
RSpec.describe RubySpriter::Consolidator do
|
|
6
7
|
let(:spritesheet1) { File.join(__dir__, '..', 'fixtures', 'spritesheet_4x2.png') }
|
|
@@ -372,4 +373,166 @@ RSpec.describe RubySpriter::Consolidator do
|
|
|
372
373
|
end
|
|
373
374
|
end
|
|
374
375
|
end
|
|
376
|
+
|
|
377
|
+
describe '#find_spritesheets_in_directory' do
|
|
378
|
+
let(:consolidator) { described_class.new }
|
|
379
|
+
let(:image_without_meta) { File.join(__dir__, '..', 'fixtures', 'image_without_metadata.png') }
|
|
380
|
+
|
|
381
|
+
# Helper to create a unique test directory for each test
|
|
382
|
+
def create_test_dir
|
|
383
|
+
dir = File.join($test_temp_dir, "consolidate_test_#{SecureRandom.hex(8)}")
|
|
384
|
+
FileUtils.mkdir_p(dir)
|
|
385
|
+
dir
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
it 'finds all PNG files with metadata in directory' do
|
|
389
|
+
test_dir = create_test_dir
|
|
390
|
+
# Copy spritesheets with metadata to test directory
|
|
391
|
+
sprite1_path = File.join(test_dir, 'sprite1.png')
|
|
392
|
+
sprite2_path = File.join(test_dir, 'sprite2.png')
|
|
393
|
+
FileUtils.cp(spritesheet1, sprite1_path)
|
|
394
|
+
FileUtils.cp(spritesheet2, sprite2_path)
|
|
395
|
+
|
|
396
|
+
# Mock metadata reading to return valid metadata for these files
|
|
397
|
+
allow(RubySpriter::MetadataManager).to receive(:read).and_call_original
|
|
398
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(sprite1_path).and_return(
|
|
399
|
+
{ columns: 2, rows: 2, frames: 4, version: '0.6' }
|
|
400
|
+
)
|
|
401
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(sprite2_path).and_return(
|
|
402
|
+
{ columns: 2, rows: 3, frames: 6, version: '0.6' }
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
found_files = consolidator.find_spritesheets_in_directory(test_dir)
|
|
406
|
+
|
|
407
|
+
expect(found_files.length).to eq(2)
|
|
408
|
+
expect(found_files).to include(sprite1_path)
|
|
409
|
+
expect(found_files).to include(sprite2_path)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
it 'excludes PNG files without metadata' do
|
|
413
|
+
test_dir = create_test_dir
|
|
414
|
+
# Copy two spritesheets with metadata and one image without
|
|
415
|
+
sprite1_path = File.join(test_dir, 'sprite1.png')
|
|
416
|
+
sprite2_path = File.join(test_dir, 'sprite2.png')
|
|
417
|
+
no_meta_path = File.join(test_dir, 'no_meta.png')
|
|
418
|
+
FileUtils.cp(spritesheet1, sprite1_path)
|
|
419
|
+
FileUtils.cp(spritesheet2, sprite2_path)
|
|
420
|
+
FileUtils.cp(image_without_meta, no_meta_path)
|
|
421
|
+
|
|
422
|
+
# Mock metadata reading
|
|
423
|
+
allow(RubySpriter::MetadataManager).to receive(:read).and_call_original
|
|
424
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(sprite1_path).and_return(
|
|
425
|
+
{ columns: 2, rows: 2, frames: 4, version: '0.6' }
|
|
426
|
+
)
|
|
427
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(sprite2_path).and_return(
|
|
428
|
+
{ columns: 2, rows: 3, frames: 6, version: '0.6' }
|
|
429
|
+
)
|
|
430
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(no_meta_path).and_return(nil)
|
|
431
|
+
|
|
432
|
+
found_files = consolidator.find_spritesheets_in_directory(test_dir)
|
|
433
|
+
|
|
434
|
+
expect(found_files.length).to eq(2)
|
|
435
|
+
expect(found_files).to include(sprite1_path)
|
|
436
|
+
expect(found_files).to include(sprite2_path)
|
|
437
|
+
expect(found_files).not_to include(no_meta_path)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
it 'returns files sorted alphabetically' do
|
|
441
|
+
test_dir = create_test_dir
|
|
442
|
+
# Copy spritesheets with names that test alphabetical sorting
|
|
443
|
+
c_sprite_path = File.join(test_dir, 'c_sprite.png')
|
|
444
|
+
a_sprite_path = File.join(test_dir, 'a_sprite.png')
|
|
445
|
+
b_sprite_path = File.join(test_dir, 'b_sprite.png')
|
|
446
|
+
FileUtils.cp(spritesheet1, c_sprite_path)
|
|
447
|
+
FileUtils.cp(spritesheet2, a_sprite_path)
|
|
448
|
+
FileUtils.cp(spritesheet3, b_sprite_path)
|
|
449
|
+
|
|
450
|
+
# Mock metadata reading
|
|
451
|
+
allow(RubySpriter::MetadataManager).to receive(:read).and_call_original
|
|
452
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(c_sprite_path).and_return(
|
|
453
|
+
{ columns: 4, rows: 1, frames: 4, version: '0.6' }
|
|
454
|
+
)
|
|
455
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(a_sprite_path).and_return(
|
|
456
|
+
{ columns: 2, rows: 3, frames: 6, version: '0.6' }
|
|
457
|
+
)
|
|
458
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(b_sprite_path).and_return(
|
|
459
|
+
{ columns: 4, rows: 4, frames: 16, version: '0.6' }
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
found_files = consolidator.find_spritesheets_in_directory(test_dir)
|
|
463
|
+
|
|
464
|
+
expect(found_files.length).to eq(3)
|
|
465
|
+
expect(found_files[0]).to end_with('a_sprite.png')
|
|
466
|
+
expect(found_files[1]).to end_with('b_sprite.png')
|
|
467
|
+
expect(found_files[2]).to end_with('c_sprite.png')
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
it 'raises error when directory does not exist' do
|
|
471
|
+
expect {
|
|
472
|
+
consolidator.find_spritesheets_in_directory('nonexistent_directory')
|
|
473
|
+
}.to raise_error(RubySpriter::ValidationError, /Directory not found/)
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
it 'raises error when no PNG files with metadata found' do
|
|
477
|
+
# Create empty directory
|
|
478
|
+
empty_dir = File.join($test_temp_dir, 'empty_dir')
|
|
479
|
+
FileUtils.mkdir_p(empty_dir)
|
|
480
|
+
|
|
481
|
+
expect {
|
|
482
|
+
consolidator.find_spritesheets_in_directory(empty_dir)
|
|
483
|
+
}.to raise_error(RubySpriter::ValidationError, /No PNG files with spritesheet metadata found/)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
it 'raises error when directory has PNGs but none with metadata' do
|
|
487
|
+
test_dir = create_test_dir
|
|
488
|
+
# Copy only image without metadata
|
|
489
|
+
FileUtils.cp(image_without_meta, File.join(test_dir, 'no_meta.png'))
|
|
490
|
+
|
|
491
|
+
expect {
|
|
492
|
+
consolidator.find_spritesheets_in_directory(test_dir)
|
|
493
|
+
}.to raise_error(RubySpriter::ValidationError, /No PNG files with spritesheet metadata found/)
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
it 'handles directory with mixed file types' do
|
|
497
|
+
test_dir = create_test_dir
|
|
498
|
+
# Copy spritesheets and create non-PNG files
|
|
499
|
+
sprite1_path = File.join(test_dir, 'sprite1.png')
|
|
500
|
+
sprite2_path = File.join(test_dir, 'sprite2.png')
|
|
501
|
+
FileUtils.cp(spritesheet1, sprite1_path)
|
|
502
|
+
FileUtils.cp(spritesheet2, sprite2_path)
|
|
503
|
+
File.write(File.join(test_dir, 'readme.txt'), 'test')
|
|
504
|
+
File.write(File.join(test_dir, 'data.json'), '{}')
|
|
505
|
+
|
|
506
|
+
# Mock metadata reading
|
|
507
|
+
allow(RubySpriter::MetadataManager).to receive(:read).and_call_original
|
|
508
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(sprite1_path).and_return(
|
|
509
|
+
{ columns: 2, rows: 2, frames: 4, version: '0.6' }
|
|
510
|
+
)
|
|
511
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(sprite2_path).and_return(
|
|
512
|
+
{ columns: 2, rows: 3, frames: 6, version: '0.6' }
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
found_files = consolidator.find_spritesheets_in_directory(test_dir)
|
|
516
|
+
|
|
517
|
+
expect(found_files.length).to eq(2)
|
|
518
|
+
expect(found_files).to all(end_with('.png'))
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
it 'requires at least 2 spritesheets' do
|
|
522
|
+
test_dir = create_test_dir
|
|
523
|
+
# Copy only one spritesheet
|
|
524
|
+
sprite1_path = File.join(test_dir, 'sprite1.png')
|
|
525
|
+
FileUtils.cp(spritesheet1, sprite1_path)
|
|
526
|
+
|
|
527
|
+
# Mock metadata reading
|
|
528
|
+
allow(RubySpriter::MetadataManager).to receive(:read).and_call_original
|
|
529
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(sprite1_path).and_return(
|
|
530
|
+
{ columns: 2, rows: 2, frames: 4, version: '0.6' }
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
expect {
|
|
534
|
+
consolidator.find_spritesheets_in_directory(test_dir)
|
|
535
|
+
}.to raise_error(RubySpriter::ValidationError, /Found only 1 spritesheet, need at least 2/)
|
|
536
|
+
end
|
|
537
|
+
end
|
|
375
538
|
end
|
|
@@ -71,7 +71,7 @@ RSpec.describe RubySpriter::Platform do
|
|
|
71
71
|
describe '.imagemagick_identify_cmd' do
|
|
72
72
|
it 'returns appropriate command for platform' do
|
|
73
73
|
cmd = described_class.imagemagick_identify_cmd
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
if described_class.windows?
|
|
76
76
|
expect(cmd).to eq('magick identify')
|
|
77
77
|
else
|
|
@@ -79,4 +79,14 @@ RSpec.describe RubySpriter::Platform do
|
|
|
79
79
|
end
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
|
+
|
|
83
|
+
describe '.detect_gimp_version' do
|
|
84
|
+
it 'detects GIMP 3.x version from command output' do
|
|
85
|
+
gimp3_output = "GNU Image Manipulation Program version 3.0.0"
|
|
86
|
+
version = described_class.detect_gimp_version(gimp3_output)
|
|
87
|
+
expect(version[:major]).to eq(3)
|
|
88
|
+
expect(version[:minor]).to eq(0)
|
|
89
|
+
expect(version[:full]).to eq('3.0.0')
|
|
90
|
+
end
|
|
91
|
+
end
|
|
82
92
|
end
|