ruby_spriter 0.6.7 → 0.7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +3 -3
  3. data/CHANGELOG.md +1035 -405
  4. data/Gemfile +17 -17
  5. data/LICENSE +21 -21
  6. data/README.md +183 -902
  7. data/bin/ruby_spriter +20 -20
  8. data/lib/ruby_spriter/background_sampler.rb +140 -0
  9. data/lib/ruby_spriter/batch_processor.rb +268 -212
  10. data/lib/ruby_spriter/cell_cleanup_config.rb +21 -0
  11. data/lib/ruby_spriter/cell_cleanup_gimp_script.rb +123 -0
  12. data/lib/ruby_spriter/cell_cleanup_processor.rb +230 -0
  13. data/lib/ruby_spriter/cli.rb +676 -612
  14. data/lib/ruby_spriter/compression_manager.rb +101 -101
  15. data/lib/ruby_spriter/consolidator.rb +179 -179
  16. data/lib/ruby_spriter/dependency_checker.rb +224 -174
  17. data/lib/ruby_spriter/ghost_edge_cleaner.rb +164 -0
  18. data/lib/ruby_spriter/gimp_processor.rb +1188 -667
  19. data/lib/ruby_spriter/metadata_manager.rb +117 -116
  20. data/lib/ruby_spriter/platform.rb +137 -82
  21. data/lib/ruby_spriter/processor.rb +1230 -886
  22. data/lib/ruby_spriter/smoke_detector.rb +223 -0
  23. data/lib/ruby_spriter/threshold_stepper.rb +227 -0
  24. data/lib/ruby_spriter/utils/file_helper.rb +82 -82
  25. data/lib/ruby_spriter/utils/image_helper.rb +16 -0
  26. data/lib/ruby_spriter/utils/output_formatter.rb +65 -65
  27. data/lib/ruby_spriter/utils/path_helper.rb +59 -59
  28. data/lib/ruby_spriter/utils/spritesheet_splitter.rb +86 -86
  29. data/lib/ruby_spriter/version.rb +6 -7
  30. data/lib/ruby_spriter/video_processor.rb +357 -139
  31. data/lib/ruby_spriter.rb +38 -34
  32. data/ruby_spriter.gemspec +44 -42
  33. data/spec/fixtures/centered_sprite_with_inner_bg.png +0 -0
  34. data/spec/fixtures/centered_sprite_with_inner_bg_inner_removed.png +0 -0
  35. data/spec/fixtures/centered_sprite_with_inner_bg_threshold_stepped.png +0 -0
  36. data/spec/fixtures/complex_background_sprite.png +0 -0
  37. data/spec/fixtures/death - bloodborne rekconing.mp4 +0 -0
  38. data/spec/fixtures/death - bloodborne rekconing_spritesheet-nobg-global.png +0 -0
  39. data/spec/fixtures/death - bloodborne rekconing_spritesheet.png +0 -0
  40. data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185027_431-nobg-global.png +0 -0
  41. data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185027_431.png +0 -0
  42. data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185125_738-nobg-global.png +0 -0
  43. data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185125_738.png +0 -0
  44. data/spec/fixtures/has_inner_bg.png +0 -0
  45. data/spec/fixtures/has_small_inner_bg.png +0 -0
  46. data/spec/fixtures/smoke_effect_sprite.png +0 -0
  47. data/spec/fixtures/spritesheet_with_metadata.png +0 -0
  48. data/spec/fixtures/test_sprite.png +0 -0
  49. data/spec/fixtures/test_sprite_smoke_processed.png +0 -0
  50. data/spec/fixtures/test_video_spritesheet.png +0 -0
  51. data/spec/fixtures/transparent_bg_sprite.png +0 -0
  52. data/spec/fixtures/walk_north_sprite-sheet.png +0 -0
  53. data/spec/integration/no_fuzzy_mode_spec.rb +100 -0
  54. data/spec/ruby_spriter/batch_processor_spec.rb +509 -200
  55. data/spec/ruby_spriter/cli_spec.rb +2026 -1892
  56. data/spec/ruby_spriter/compression_manager_spec.rb +157 -157
  57. data/spec/ruby_spriter/consolidator_spec.rb +538 -538
  58. data/spec/ruby_spriter/gimp_processor_spec.rb +523 -425
  59. data/spec/ruby_spriter/platform_spec.rb +92 -82
  60. data/spec/ruby_spriter/processor_spec.rb +911 -735
  61. data/spec/ruby_spriter/utils/file_helper_spec.rb +150 -150
  62. data/spec/ruby_spriter/utils/path_helper_spec.rb +78 -78
  63. data/spec/ruby_spriter/utils/spritesheet_splitter_spec.rb +104 -104
  64. data/spec/ruby_spriter/video_processor_spec.rb +346 -29
  65. data/spec/spec_helper.rb +41 -41
  66. data/spec/tmp/cli_test_output.png +0 -0
  67. data/spec/tmp/cli_test_output_20251105_114647_395.png +0 -0
  68. data/spec/tmp/combined_test.png +0 -0
  69. data/spec/tmp/compat_test.png +0 -0
  70. data/spec/tmp/compat_test_20251030_174233_361.png +0 -0
  71. data/spec/tmp/final_all_features.png +0 -0
  72. data/spec/tmp/final_test_all_features.png +0 -0
  73. data/spec/tmp/full_pipeline_test.png +0 -0
  74. data/spec/tmp/inner_test.png +0 -0
  75. data/spec/tmp/integration_test.png +0 -0
  76. data/spec/tmp/validation_test.png +0 -0
  77. data/spec/unit/background_sampler_spec.rb +132 -0
  78. data/spec/unit/cell_cleanup_config_spec.rb +32 -0
  79. data/spec/unit/cell_cleanup_gimp_script_spec.rb +59 -0
  80. data/spec/unit/cell_cleanup_processor_spec.rb +261 -0
  81. data/spec/unit/ghost_edge_cleaner_spec.rb +223 -0
  82. data/spec/unit/gimp_processor_single_point_selection_spec.rb +81 -0
  83. data/spec/unit/smoke_detector_spec.rb +246 -0
  84. data/spec/unit/threshold_stepper_spec.rb +195 -0
  85. metadata +56 -10
@@ -1,150 +1,150 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe RubySpriter::Utils::FileHelper do
6
- describe '.spritesheet_filename' do
7
- it 'generates correct filename from video' do
8
- result = described_class.spritesheet_filename('/path/to/video.mp4')
9
- expect(result).to eq('/path/to/video_spritesheet.png')
10
- end
11
-
12
- it 'handles different extensions' do
13
- result = described_class.spritesheet_filename('C:\\videos\\clip.avi')
14
- expect(result).to match(/clip_spritesheet\.png$/)
15
- end
16
- end
17
-
18
- describe '.output_filename' do
19
- it 'generates filename with suffix' do
20
- result = described_class.output_filename('/path/to/image.png', 'scaled')
21
- expect(result).to eq('/path/to/image-scaled.png')
22
- end
23
-
24
- it 'preserves directory path' do
25
- result = described_class.output_filename('/some/deep/path/file.png', 'nobg')
26
- expect(result).to start_with('/some/deep/path/')
27
- end
28
- end
29
-
30
- describe '.format_size' do
31
- it 'formats bytes correctly' do
32
- expect(described_class.format_size(500)).to eq('500 bytes')
33
- end
34
-
35
- it 'formats kilobytes correctly' do
36
- expect(described_class.format_size(2048)).to eq('2.0 KB')
37
- end
38
-
39
- it 'formats megabytes correctly' do
40
- expect(described_class.format_size(5_242_880)).to eq('5.0 MB')
41
- end
42
- end
43
-
44
- describe '.validate_exists!' do
45
- it 'raises error for non-existent file' do
46
- expect {
47
- described_class.validate_exists!('/nonexistent/file.txt')
48
- }.to raise_error(RubySpriter::ValidationError, /File not found/)
49
- end
50
-
51
- it 'does not raise for existing file' do
52
- file = File.join(@test_dir, 'test.txt')
53
- File.write(file, 'test')
54
-
55
- expect {
56
- described_class.validate_exists!(file)
57
- }.not_to raise_error
58
- end
59
- end
60
-
61
- describe '.validate_readable!' do
62
- it 'validates file exists and is readable' do
63
- file = File.join(@test_dir, 'test.txt')
64
- File.write(file, 'test')
65
-
66
- expect {
67
- described_class.validate_readable!(file)
68
- }.not_to raise_error
69
- end
70
- end
71
-
72
- describe '.unique_filename' do
73
- it 'returns original filename when file does not exist' do
74
- result = described_class.unique_filename('/path/to/output.png')
75
- expect(result).to eq('/path/to/output.png')
76
- end
77
-
78
- it 'adds timestamp when file exists' do
79
- file = File.join(@test_dir, 'existing.png')
80
- File.write(file, 'test')
81
-
82
- result = described_class.unique_filename(file)
83
- expect(result).to match(/existing_\d{8}_\d{6}_\d{3}\.png$/)
84
- expect(result).not_to eq(file)
85
- end
86
-
87
- it 'handles files with multiple dots in name' do
88
- file = File.join(@test_dir, 'my.sprite.sheet.png')
89
- File.write(file, 'test')
90
-
91
- result = described_class.unique_filename(file)
92
- expect(result).to match(/my\.sprite\.sheet_\d{8}_\d{6}_\d{3}\.png$/)
93
- end
94
-
95
- it 'preserves directory path' do
96
- file = File.join(@test_dir, 'subdir', 'output.png')
97
- FileUtils.mkdir_p(File.dirname(file))
98
- File.write(file, 'test')
99
-
100
- result = described_class.unique_filename(file)
101
- expect(result).to start_with(File.join(@test_dir, 'subdir'))
102
- end
103
-
104
- it 'generates different filenames for consecutive calls' do
105
- file = File.join(@test_dir, 'test.png')
106
- File.write(file, 'test')
107
-
108
- result1 = described_class.unique_filename(file)
109
- sleep(0.01) # Small delay to ensure different timestamps
110
- result2 = described_class.unique_filename(file)
111
-
112
- expect(result1).not_to eq(result2)
113
- end
114
- end
115
-
116
- describe '.ensure_unique_output' do
117
- it 'returns original path when overwrite is true' do
118
- file = File.join(@test_dir, 'output.png')
119
- File.write(file, 'test')
120
-
121
- result = described_class.ensure_unique_output(file, overwrite: true)
122
- expect(result).to eq(file)
123
- end
124
-
125
- it 'returns original path when file does not exist and overwrite is false' do
126
- file = File.join(@test_dir, 'new_output.png')
127
-
128
- result = described_class.ensure_unique_output(file, overwrite: false)
129
- expect(result).to eq(file)
130
- end
131
-
132
- it 'returns unique filename when file exists and overwrite is false' do
133
- file = File.join(@test_dir, 'existing_output.png')
134
- File.write(file, 'test')
135
-
136
- result = described_class.ensure_unique_output(file, overwrite: false)
137
- expect(result).to match(/existing_output_\d{8}_\d{6}_\d{3}\.png$/)
138
- expect(result).not_to eq(file)
139
- end
140
-
141
- it 'defaults to overwrite false when not specified' do
142
- file = File.join(@test_dir, 'default_test.png')
143
- File.write(file, 'test')
144
-
145
- result = described_class.ensure_unique_output(file)
146
- expect(result).not_to eq(file)
147
- expect(result).to match(/default_test_\d{8}_\d{6}_\d{3}\.png$/)
148
- end
149
- end
150
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubySpriter::Utils::FileHelper do
6
+ describe '.spritesheet_filename' do
7
+ it 'generates correct filename from video' do
8
+ result = described_class.spritesheet_filename('/path/to/video.mp4')
9
+ expect(result).to eq('/path/to/video_spritesheet.png')
10
+ end
11
+
12
+ it 'handles different extensions' do
13
+ result = described_class.spritesheet_filename('C:\\videos\\clip.avi')
14
+ expect(result).to match(/clip_spritesheet\.png$/)
15
+ end
16
+ end
17
+
18
+ describe '.output_filename' do
19
+ it 'generates filename with suffix' do
20
+ result = described_class.output_filename('/path/to/image.png', 'scaled')
21
+ expect(result).to eq('/path/to/image-scaled.png')
22
+ end
23
+
24
+ it 'preserves directory path' do
25
+ result = described_class.output_filename('/some/deep/path/file.png', 'nobg')
26
+ expect(result).to start_with('/some/deep/path/')
27
+ end
28
+ end
29
+
30
+ describe '.format_size' do
31
+ it 'formats bytes correctly' do
32
+ expect(described_class.format_size(500)).to eq('500 bytes')
33
+ end
34
+
35
+ it 'formats kilobytes correctly' do
36
+ expect(described_class.format_size(2048)).to eq('2.0 KB')
37
+ end
38
+
39
+ it 'formats megabytes correctly' do
40
+ expect(described_class.format_size(5_242_880)).to eq('5.0 MB')
41
+ end
42
+ end
43
+
44
+ describe '.validate_exists!' do
45
+ it 'raises error for non-existent file' do
46
+ expect {
47
+ described_class.validate_exists!('/nonexistent/file.txt')
48
+ }.to raise_error(RubySpriter::ValidationError, /File not found/)
49
+ end
50
+
51
+ it 'does not raise for existing file' do
52
+ file = File.join(@test_dir, 'test.txt')
53
+ File.write(file, 'test')
54
+
55
+ expect {
56
+ described_class.validate_exists!(file)
57
+ }.not_to raise_error
58
+ end
59
+ end
60
+
61
+ describe '.validate_readable!' do
62
+ it 'validates file exists and is readable' do
63
+ file = File.join(@test_dir, 'test.txt')
64
+ File.write(file, 'test')
65
+
66
+ expect {
67
+ described_class.validate_readable!(file)
68
+ }.not_to raise_error
69
+ end
70
+ end
71
+
72
+ describe '.unique_filename' do
73
+ it 'returns original filename when file does not exist' do
74
+ result = described_class.unique_filename('/path/to/output.png')
75
+ expect(result).to eq('/path/to/output.png')
76
+ end
77
+
78
+ it 'adds timestamp when file exists' do
79
+ file = File.join(@test_dir, 'existing.png')
80
+ File.write(file, 'test')
81
+
82
+ result = described_class.unique_filename(file)
83
+ expect(result).to match(/existing_\d{8}_\d{6}_\d{3}\.png$/)
84
+ expect(result).not_to eq(file)
85
+ end
86
+
87
+ it 'handles files with multiple dots in name' do
88
+ file = File.join(@test_dir, 'my.sprite.sheet.png')
89
+ File.write(file, 'test')
90
+
91
+ result = described_class.unique_filename(file)
92
+ expect(result).to match(/my\.sprite\.sheet_\d{8}_\d{6}_\d{3}\.png$/)
93
+ end
94
+
95
+ it 'preserves directory path' do
96
+ file = File.join(@test_dir, 'subdir', 'output.png')
97
+ FileUtils.mkdir_p(File.dirname(file))
98
+ File.write(file, 'test')
99
+
100
+ result = described_class.unique_filename(file)
101
+ expect(result).to start_with(File.join(@test_dir, 'subdir'))
102
+ end
103
+
104
+ it 'generates different filenames for consecutive calls' do
105
+ file = File.join(@test_dir, 'test.png')
106
+ File.write(file, 'test')
107
+
108
+ result1 = described_class.unique_filename(file)
109
+ sleep(0.01) # Small delay to ensure different timestamps
110
+ result2 = described_class.unique_filename(file)
111
+
112
+ expect(result1).not_to eq(result2)
113
+ end
114
+ end
115
+
116
+ describe '.ensure_unique_output' do
117
+ it 'returns original path when overwrite is true' do
118
+ file = File.join(@test_dir, 'output.png')
119
+ File.write(file, 'test')
120
+
121
+ result = described_class.ensure_unique_output(file, overwrite: true)
122
+ expect(result).to eq(file)
123
+ end
124
+
125
+ it 'returns original path when file does not exist and overwrite is false' do
126
+ file = File.join(@test_dir, 'new_output.png')
127
+
128
+ result = described_class.ensure_unique_output(file, overwrite: false)
129
+ expect(result).to eq(file)
130
+ end
131
+
132
+ it 'returns unique filename when file exists and overwrite is false' do
133
+ file = File.join(@test_dir, 'existing_output.png')
134
+ File.write(file, 'test')
135
+
136
+ result = described_class.ensure_unique_output(file, overwrite: false)
137
+ expect(result).to match(/existing_output_\d{8}_\d{6}_\d{3}\.png$/)
138
+ expect(result).not_to eq(file)
139
+ end
140
+
141
+ it 'defaults to overwrite false when not specified' do
142
+ file = File.join(@test_dir, 'default_test.png')
143
+ File.write(file, 'test')
144
+
145
+ result = described_class.ensure_unique_output(file)
146
+ expect(result).not_to eq(file)
147
+ expect(result).to match(/default_test_\d{8}_\d{6}_\d{3}\.png$/)
148
+ end
149
+ end
150
+ end
@@ -1,78 +1,78 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe RubySpriter::Utils::PathHelper do
6
- describe '.quote_path' do
7
- context 'on Windows' do
8
- before do
9
- allow(RubySpriter::Platform).to receive(:windows?).and_return(true)
10
- end
11
-
12
- it 'wraps path in double quotes' do
13
- expect(described_class.quote_path('C:\\test\\file.txt')).to eq('"C:\\test\\file.txt"')
14
- end
15
- end
16
-
17
- context 'on Unix-like systems' do
18
- before do
19
- allow(RubySpriter::Platform).to receive(:windows?).and_return(false)
20
- end
21
-
22
- it 'wraps path in single quotes' do
23
- expect(described_class.quote_path('/test/file.txt')).to eq("'/test/file.txt'")
24
- end
25
-
26
- it 'escapes single quotes in path' do
27
- result = described_class.quote_path("/test/file's.txt")
28
- # Should wrap in single quotes and escape internal single quotes
29
- expect(result).to start_with("'")
30
- expect(result).to end_with("'")
31
- expect(result).to include("\\'") # Should contain escaped single quote
32
- end
33
- end
34
- end
35
-
36
- describe '.normalize_for_python' do
37
- it 'returns absolute path' do
38
- result = described_class.normalize_for_python('.')
39
- # Should return an absolute path (Unix: starts with /, Windows: starts with drive letter)
40
- is_unix_absolute = result.start_with?('/')
41
- is_windows_absolute = result.match?(/^[A-Z]:/i)
42
- expect(is_unix_absolute || is_windows_absolute).to be true
43
- end
44
-
45
- context 'on Windows' do
46
- before do
47
- allow(RubySpriter::Platform).to receive(:windows?).and_return(true)
48
- end
49
-
50
- it 'converts backslashes to forward slashes' do
51
- allow(File).to receive(:absolute_path).and_return('C:\\test\\file.txt')
52
- expect(described_class.normalize_for_python('file.txt')).to eq('C:/test/file.txt')
53
- end
54
- end
55
- end
56
-
57
- describe '.to_native' do
58
- context 'on Windows' do
59
- before do
60
- allow(RubySpriter::Platform).to receive(:windows?).and_return(true)
61
- end
62
-
63
- it 'converts forward slashes to backslashes' do
64
- expect(described_class.to_native('C:/test/file.txt')).to eq('C:\\test\\file.txt')
65
- end
66
- end
67
-
68
- context 'on Unix-like systems' do
69
- before do
70
- allow(RubySpriter::Platform).to receive(:windows?).and_return(false)
71
- end
72
-
73
- it 'converts backslashes to forward slashes' do
74
- expect(described_class.to_native('C:\\test\\file.txt')).to eq('C:/test/file.txt')
75
- end
76
- end
77
- end
78
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubySpriter::Utils::PathHelper do
6
+ describe '.quote_path' do
7
+ context 'on Windows' do
8
+ before do
9
+ allow(RubySpriter::Platform).to receive(:windows?).and_return(true)
10
+ end
11
+
12
+ it 'wraps path in double quotes' do
13
+ expect(described_class.quote_path('C:\\test\\file.txt')).to eq('"C:\\test\\file.txt"')
14
+ end
15
+ end
16
+
17
+ context 'on Unix-like systems' do
18
+ before do
19
+ allow(RubySpriter::Platform).to receive(:windows?).and_return(false)
20
+ end
21
+
22
+ it 'wraps path in single quotes' do
23
+ expect(described_class.quote_path('/test/file.txt')).to eq("'/test/file.txt'")
24
+ end
25
+
26
+ it 'escapes single quotes in path' do
27
+ result = described_class.quote_path("/test/file's.txt")
28
+ # Should wrap in single quotes and escape internal single quotes
29
+ expect(result).to start_with("'")
30
+ expect(result).to end_with("'")
31
+ expect(result).to include("\\'") # Should contain escaped single quote
32
+ end
33
+ end
34
+ end
35
+
36
+ describe '.normalize_for_python' do
37
+ it 'returns absolute path' do
38
+ result = described_class.normalize_for_python('.')
39
+ # Should return an absolute path (Unix: starts with /, Windows: starts with drive letter)
40
+ is_unix_absolute = result.start_with?('/')
41
+ is_windows_absolute = result.match?(/^[A-Z]:/i)
42
+ expect(is_unix_absolute || is_windows_absolute).to be true
43
+ end
44
+
45
+ context 'on Windows' do
46
+ before do
47
+ allow(RubySpriter::Platform).to receive(:windows?).and_return(true)
48
+ end
49
+
50
+ it 'converts backslashes to forward slashes' do
51
+ allow(File).to receive(:absolute_path).and_return('C:\\test\\file.txt')
52
+ expect(described_class.normalize_for_python('file.txt')).to eq('C:/test/file.txt')
53
+ end
54
+ end
55
+ end
56
+
57
+ describe '.to_native' do
58
+ context 'on Windows' do
59
+ before do
60
+ allow(RubySpriter::Platform).to receive(:windows?).and_return(true)
61
+ end
62
+
63
+ it 'converts forward slashes to backslashes' do
64
+ expect(described_class.to_native('C:/test/file.txt')).to eq('C:\\test\\file.txt')
65
+ end
66
+ end
67
+
68
+ context 'on Unix-like systems' do
69
+ before do
70
+ allow(RubySpriter::Platform).to receive(:windows?).and_return(false)
71
+ end
72
+
73
+ it 'converts backslashes to forward slashes' do
74
+ expect(described_class.to_native('C:\\test\\file.txt')).to eq('C:/test/file.txt')
75
+ end
76
+ end
77
+ end
78
+ end
@@ -1,104 +1,104 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe RubySpriter::Utils::SpritesheetSplitter do
6
- describe '#split_into_frames' do
7
- let(:spritesheet_file) { 'spritesheet.png' }
8
- let(:output_dir) { '/tmp/frames' }
9
- let(:columns) { 4 }
10
- let(:rows) { 4 }
11
- let(:frames) { 16 }
12
- let(:tile_width) { 100 }
13
- let(:tile_height) { 100 }
14
-
15
- let(:splitter) { described_class.new }
16
-
17
- before do
18
- allow(FileUtils).to receive(:mkdir_p)
19
- allow(File).to receive(:exist?).and_return(true)
20
- allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
21
-
22
- # Mock ImageMagick identify to return dimensions
23
- allow(Open3).to receive(:capture3).with(/identify/).and_return(
24
- ["400x400\n", '', instance_double(Process::Status, success?: true)]
25
- )
26
- end
27
-
28
- it 'creates output directory for frames' do
29
- expect(FileUtils).to receive(:mkdir_p).with(output_dir)
30
-
31
- splitter.split_into_frames(spritesheet_file, output_dir, columns, rows, frames)
32
- end
33
-
34
- it 'extracts each frame with FR prefix and zero-padded numbers' do
35
- # Expect ImageMagick convert commands for each frame
36
- expect(Open3).to receive(:capture3).with(/identify/).once.and_return(
37
- ["400x400\n", '', instance_double(Process::Status, success?: true)]
38
- )
39
-
40
- (1..frames).each do |i|
41
- expect(Open3).to receive(:capture3).with(/convert.*FR#{format('%03d', i)}_/).and_return(
42
- ['', '', instance_double(Process::Status, success?: true)]
43
- )
44
- end
45
-
46
- splitter.split_into_frames(spritesheet_file, output_dir, columns, rows, frames)
47
- end
48
-
49
- it 'calculates tile dimensions from spritesheet size and grid' do
50
- # For 400x400 image with 4x4 grid, tiles should be 100x100
51
- expect(Open3).to receive(:capture3).with(/identify/).and_return(
52
- ["400x400\n", '', instance_double(Process::Status, success?: true)]
53
- )
54
-
55
- # Check that crop parameters use 100x100
56
- expect(Open3).to receive(:capture3).with(/100x100\+0\+0/).and_return(
57
- ['', '', instance_double(Process::Status, success?: true)]
58
- )
59
-
60
- allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
61
-
62
- splitter.split_into_frames(spritesheet_file, output_dir, columns, rows, 1)
63
- end
64
-
65
- it 'includes spritesheet basename in frame output names' do
66
- basename = File.basename(spritesheet_file, '.*')
67
-
68
- # Mock identify first
69
- allow(Open3).to receive(:capture3).with(/identify/).and_return(
70
- ["400x400\n", '', instance_double(Process::Status, success?: true)]
71
- )
72
-
73
- # Expect convert with frame filename
74
- expect(Open3).to receive(:capture3).with(/FR001_#{basename}\.png/).and_return(
75
- ['', '', instance_double(Process::Status, success?: true)]
76
- )
77
-
78
- splitter.split_into_frames(spritesheet_file, output_dir, columns, rows, 1)
79
- end
80
-
81
- it 'raises ProcessingError when ImageMagick fails' do
82
- allow(Open3).to receive(:capture3).with(/identify/).and_return(
83
- ["400x400\n", '', instance_double(Process::Status, success?: true)]
84
- )
85
-
86
- allow(Open3).to receive(:capture3).with(/convert/).and_return(
87
- ['', 'Error message', instance_double(Process::Status, success?: false)]
88
- )
89
-
90
- expect {
91
- splitter.split_into_frames(spritesheet_file, output_dir, columns, rows, 1)
92
- }.to raise_error(RubySpriter::ProcessingError, /Could not extract frame/)
93
- end
94
-
95
- it 'displays progress information' do
96
- expect(RubySpriter::Utils::OutputFormatter).to receive(:header).with(/Extracting Frames/)
97
- expect(RubySpriter::Utils::OutputFormatter).to receive(:indent).with(/Splitting spritesheet into #{frames} frames/)
98
- expect(RubySpriter::Utils::OutputFormatter).to receive(:indent).with(/Output directory:/)
99
- expect(RubySpriter::Utils::OutputFormatter).to receive(:indent).with(/Frames extracted successfully/)
100
-
101
- splitter.split_into_frames(spritesheet_file, output_dir, columns, rows, frames)
102
- end
103
- end
104
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubySpriter::Utils::SpritesheetSplitter do
6
+ describe '#split_into_frames' do
7
+ let(:spritesheet_file) { 'spritesheet.png' }
8
+ let(:output_dir) { '/tmp/frames' }
9
+ let(:columns) { 4 }
10
+ let(:rows) { 4 }
11
+ let(:frames) { 16 }
12
+ let(:tile_width) { 100 }
13
+ let(:tile_height) { 100 }
14
+
15
+ let(:splitter) { described_class.new }
16
+
17
+ before do
18
+ allow(FileUtils).to receive(:mkdir_p)
19
+ allow(File).to receive(:exist?).and_return(true)
20
+ allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
21
+
22
+ # Mock ImageMagick identify to return dimensions
23
+ allow(Open3).to receive(:capture3).with(/identify/).and_return(
24
+ ["400x400\n", '', instance_double(Process::Status, success?: true)]
25
+ )
26
+ end
27
+
28
+ it 'creates output directory for frames' do
29
+ expect(FileUtils).to receive(:mkdir_p).with(output_dir)
30
+
31
+ splitter.split_into_frames(spritesheet_file, output_dir, columns, rows, frames)
32
+ end
33
+
34
+ it 'extracts each frame with FR prefix and zero-padded numbers' do
35
+ # Expect ImageMagick convert commands for each frame
36
+ expect(Open3).to receive(:capture3).with(/identify/).once.and_return(
37
+ ["400x400\n", '', instance_double(Process::Status, success?: true)]
38
+ )
39
+
40
+ (1..frames).each do |i|
41
+ expect(Open3).to receive(:capture3).with(/convert.*FR#{format('%03d', i)}_/).and_return(
42
+ ['', '', instance_double(Process::Status, success?: true)]
43
+ )
44
+ end
45
+
46
+ splitter.split_into_frames(spritesheet_file, output_dir, columns, rows, frames)
47
+ end
48
+
49
+ it 'calculates tile dimensions from spritesheet size and grid' do
50
+ # For 400x400 image with 4x4 grid, tiles should be 100x100
51
+ expect(Open3).to receive(:capture3).with(/identify/).and_return(
52
+ ["400x400\n", '', instance_double(Process::Status, success?: true)]
53
+ )
54
+
55
+ # Check that crop parameters use 100x100
56
+ expect(Open3).to receive(:capture3).with(/100x100\+0\+0/).and_return(
57
+ ['', '', instance_double(Process::Status, success?: true)]
58
+ )
59
+
60
+ allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
61
+
62
+ splitter.split_into_frames(spritesheet_file, output_dir, columns, rows, 1)
63
+ end
64
+
65
+ it 'includes spritesheet basename in frame output names' do
66
+ basename = File.basename(spritesheet_file, '.*')
67
+
68
+ # Mock identify first
69
+ allow(Open3).to receive(:capture3).with(/identify/).and_return(
70
+ ["400x400\n", '', instance_double(Process::Status, success?: true)]
71
+ )
72
+
73
+ # Expect convert with frame filename
74
+ expect(Open3).to receive(:capture3).with(/FR001_#{basename}\.png/).and_return(
75
+ ['', '', instance_double(Process::Status, success?: true)]
76
+ )
77
+
78
+ splitter.split_into_frames(spritesheet_file, output_dir, columns, rows, 1)
79
+ end
80
+
81
+ it 'raises ProcessingError when ImageMagick fails' do
82
+ allow(Open3).to receive(:capture3).with(/identify/).and_return(
83
+ ["400x400\n", '', instance_double(Process::Status, success?: true)]
84
+ )
85
+
86
+ allow(Open3).to receive(:capture3).with(/convert/).and_return(
87
+ ['', 'Error message', instance_double(Process::Status, success?: false)]
88
+ )
89
+
90
+ expect {
91
+ splitter.split_into_frames(spritesheet_file, output_dir, columns, rows, 1)
92
+ }.to raise_error(RubySpriter::ProcessingError, /Could not extract frame/)
93
+ end
94
+
95
+ it 'displays progress information' do
96
+ expect(RubySpriter::Utils::OutputFormatter).to receive(:header).with(/Extracting Frames/)
97
+ expect(RubySpriter::Utils::OutputFormatter).to receive(:indent).with(/Splitting spritesheet into #{frames} frames/)
98
+ expect(RubySpriter::Utils::OutputFormatter).to receive(:indent).with(/Output directory:/)
99
+ expect(RubySpriter::Utils::OutputFormatter).to receive(:indent).with(/Frames extracted successfully/)
100
+
101
+ splitter.split_into_frames(spritesheet_file, output_dir, columns, rows, frames)
102
+ end
103
+ end
104
+ end