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.
- checksums.yaml +4 -4
- data/.rspec +3 -3
- data/CHANGELOG.md +1035 -405
- data/Gemfile +17 -17
- data/LICENSE +21 -21
- data/README.md +183 -902
- data/bin/ruby_spriter +20 -20
- data/lib/ruby_spriter/background_sampler.rb +140 -0
- data/lib/ruby_spriter/batch_processor.rb +268 -212
- data/lib/ruby_spriter/cell_cleanup_config.rb +21 -0
- data/lib/ruby_spriter/cell_cleanup_gimp_script.rb +123 -0
- data/lib/ruby_spriter/cell_cleanup_processor.rb +230 -0
- data/lib/ruby_spriter/cli.rb +676 -612
- data/lib/ruby_spriter/compression_manager.rb +101 -101
- data/lib/ruby_spriter/consolidator.rb +179 -179
- data/lib/ruby_spriter/dependency_checker.rb +224 -174
- data/lib/ruby_spriter/ghost_edge_cleaner.rb +164 -0
- data/lib/ruby_spriter/gimp_processor.rb +1188 -667
- data/lib/ruby_spriter/metadata_manager.rb +117 -116
- data/lib/ruby_spriter/platform.rb +137 -82
- data/lib/ruby_spriter/processor.rb +1230 -886
- data/lib/ruby_spriter/smoke_detector.rb +223 -0
- data/lib/ruby_spriter/threshold_stepper.rb +227 -0
- data/lib/ruby_spriter/utils/file_helper.rb +82 -82
- data/lib/ruby_spriter/utils/image_helper.rb +16 -0
- data/lib/ruby_spriter/utils/output_formatter.rb +65 -65
- data/lib/ruby_spriter/utils/path_helper.rb +59 -59
- data/lib/ruby_spriter/utils/spritesheet_splitter.rb +86 -86
- data/lib/ruby_spriter/version.rb +6 -7
- data/lib/ruby_spriter/video_processor.rb +357 -139
- data/lib/ruby_spriter.rb +38 -34
- data/ruby_spriter.gemspec +44 -42
- data/spec/fixtures/centered_sprite_with_inner_bg.png +0 -0
- data/spec/fixtures/centered_sprite_with_inner_bg_inner_removed.png +0 -0
- data/spec/fixtures/centered_sprite_with_inner_bg_threshold_stepped.png +0 -0
- data/spec/fixtures/complex_background_sprite.png +0 -0
- data/spec/fixtures/death - bloodborne rekconing.mp4 +0 -0
- data/spec/fixtures/death - bloodborne rekconing_spritesheet-nobg-global.png +0 -0
- data/spec/fixtures/death - bloodborne rekconing_spritesheet.png +0 -0
- data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185027_431-nobg-global.png +0 -0
- data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185027_431.png +0 -0
- data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185125_738-nobg-global.png +0 -0
- data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185125_738.png +0 -0
- data/spec/fixtures/has_inner_bg.png +0 -0
- data/spec/fixtures/has_small_inner_bg.png +0 -0
- data/spec/fixtures/smoke_effect_sprite.png +0 -0
- data/spec/fixtures/spritesheet_with_metadata.png +0 -0
- data/spec/fixtures/test_sprite.png +0 -0
- data/spec/fixtures/test_sprite_smoke_processed.png +0 -0
- data/spec/fixtures/test_video_spritesheet.png +0 -0
- data/spec/fixtures/transparent_bg_sprite.png +0 -0
- data/spec/fixtures/walk_north_sprite-sheet.png +0 -0
- data/spec/integration/no_fuzzy_mode_spec.rb +100 -0
- data/spec/ruby_spriter/batch_processor_spec.rb +509 -200
- data/spec/ruby_spriter/cli_spec.rb +2026 -1892
- data/spec/ruby_spriter/compression_manager_spec.rb +157 -157
- data/spec/ruby_spriter/consolidator_spec.rb +538 -538
- data/spec/ruby_spriter/gimp_processor_spec.rb +523 -425
- data/spec/ruby_spriter/platform_spec.rb +92 -82
- data/spec/ruby_spriter/processor_spec.rb +911 -735
- data/spec/ruby_spriter/utils/file_helper_spec.rb +150 -150
- data/spec/ruby_spriter/utils/path_helper_spec.rb +78 -78
- data/spec/ruby_spriter/utils/spritesheet_splitter_spec.rb +104 -104
- data/spec/ruby_spriter/video_processor_spec.rb +346 -29
- data/spec/spec_helper.rb +41 -41
- data/spec/tmp/cli_test_output.png +0 -0
- data/spec/tmp/cli_test_output_20251105_114647_395.png +0 -0
- data/spec/tmp/combined_test.png +0 -0
- data/spec/tmp/compat_test.png +0 -0
- data/spec/tmp/compat_test_20251030_174233_361.png +0 -0
- data/spec/tmp/final_all_features.png +0 -0
- data/spec/tmp/final_test_all_features.png +0 -0
- data/spec/tmp/full_pipeline_test.png +0 -0
- data/spec/tmp/inner_test.png +0 -0
- data/spec/tmp/integration_test.png +0 -0
- data/spec/tmp/validation_test.png +0 -0
- data/spec/unit/background_sampler_spec.rb +132 -0
- data/spec/unit/cell_cleanup_config_spec.rb +32 -0
- data/spec/unit/cell_cleanup_gimp_script_spec.rb +59 -0
- data/spec/unit/cell_cleanup_processor_spec.rb +261 -0
- data/spec/unit/ghost_edge_cleaner_spec.rb +223 -0
- data/spec/unit/gimp_processor_single_point_selection_spec.rb +81 -0
- data/spec/unit/smoke_detector_spec.rb +246 -0
- data/spec/unit/threshold_stepper_spec.rb +195 -0
- metadata +56 -10
|
@@ -1,538 +1,538 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'spec_helper'
|
|
4
|
-
require 'securerandom'
|
|
5
|
-
|
|
6
|
-
RSpec.describe RubySpriter::Consolidator do
|
|
7
|
-
let(:spritesheet1) { File.join(__dir__, '..', 'fixtures', 'spritesheet_4x2.png') }
|
|
8
|
-
let(:spritesheet2) { File.join(__dir__, '..', 'fixtures', 'spritesheet_6x2.png') }
|
|
9
|
-
let(:spritesheet3) { File.join(__dir__, '..', 'fixtures', 'spritesheet_4x4.png') }
|
|
10
|
-
let(:output_file) { File.join($test_temp_dir, 'consolidated.png') }
|
|
11
|
-
|
|
12
|
-
describe '#initialize' do
|
|
13
|
-
it 'initializes with empty options by default' do
|
|
14
|
-
consolidator = described_class.new
|
|
15
|
-
|
|
16
|
-
expect(consolidator.options).to eq({})
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
it 'initializes with provided options' do
|
|
20
|
-
consolidator = described_class.new(debug: true, validate_columns: false)
|
|
21
|
-
|
|
22
|
-
expect(consolidator.options[:debug]).to be true
|
|
23
|
-
expect(consolidator.options[:validate_columns]).to be false
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
describe '#consolidate' do
|
|
28
|
-
let(:consolidator) { described_class.new }
|
|
29
|
-
|
|
30
|
-
# Helper to set up common mocks for successful consolidation
|
|
31
|
-
def stub_consolidation_success
|
|
32
|
-
allow(RubySpriter::MetadataManager).to receive(:embed)
|
|
33
|
-
allow(RubySpriter::Utils::FileHelper).to receive(:validate_exists!)
|
|
34
|
-
allow(File).to receive(:rename)
|
|
35
|
-
allow(File).to receive(:exist?).and_call_original
|
|
36
|
-
allow(File).to receive(:exist?).with(/consolidated.*temp/).and_return(true)
|
|
37
|
-
allow(File).to receive(:delete)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
context 'with file validation' do
|
|
41
|
-
it 'raises error when given 0 files' do
|
|
42
|
-
expect {
|
|
43
|
-
consolidator.consolidate([], output_file)
|
|
44
|
-
}.to raise_error(RubySpriter::ValidationError, /Need at least 2 files/)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
it 'raises error when given only 1 file' do
|
|
48
|
-
expect {
|
|
49
|
-
consolidator.consolidate([spritesheet1], output_file)
|
|
50
|
-
}.to raise_error(RubySpriter::ValidationError, /Need at least 2 files/)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
it 'accepts 2 files minimum' do
|
|
54
|
-
stub_consolidation_success
|
|
55
|
-
allow(RubySpriter::MetadataManager).to receive(:read).and_return(
|
|
56
|
-
{ columns: 2, rows: 2, frames: 4 }
|
|
57
|
-
)
|
|
58
|
-
allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
|
|
59
|
-
allow(File).to receive(:size).and_return(1000)
|
|
60
|
-
|
|
61
|
-
expect {
|
|
62
|
-
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
63
|
-
}.not_to raise_error
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
it 'raises error when file does not exist' do
|
|
67
|
-
non_existent = 'non_existent.png'
|
|
68
|
-
|
|
69
|
-
expect {
|
|
70
|
-
consolidator.consolidate([spritesheet1, non_existent], output_file)
|
|
71
|
-
}.to raise_error(RubySpriter::ValidationError, /not found/)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
context 'with metadata reading' do
|
|
76
|
-
it 'reads metadata from all files' do
|
|
77
|
-
stub_consolidation_success
|
|
78
|
-
metadata1 = { columns: 2, rows: 2, frames: 4 }
|
|
79
|
-
metadata2 = { columns: 2, rows: 3, frames: 6 }
|
|
80
|
-
|
|
81
|
-
expect(RubySpriter::MetadataManager).to receive(:read).with(spritesheet1).and_return(metadata1)
|
|
82
|
-
expect(RubySpriter::MetadataManager).to receive(:read).with(spritesheet2).and_return(metadata2)
|
|
83
|
-
|
|
84
|
-
allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
|
|
85
|
-
allow(File).to receive(:size).and_return(1000)
|
|
86
|
-
|
|
87
|
-
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
it 'raises ValidationError when file missing metadata' do
|
|
91
|
-
allow(RubySpriter::MetadataManager).to receive(:read).with(spritesheet1).and_return(nil)
|
|
92
|
-
|
|
93
|
-
expect {
|
|
94
|
-
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
95
|
-
}.to raise_error(RubySpriter::ValidationError, /missing metadata/)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
it 'raises error with helpful message about Ruby Spriter spritesheets' do
|
|
99
|
-
allow(RubySpriter::MetadataManager).to receive(:read).with(spritesheet1).and_return(nil)
|
|
100
|
-
|
|
101
|
-
expect {
|
|
102
|
-
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
103
|
-
}.to raise_error(RubySpriter::ValidationError, /All files must be Ruby Spriter spritesheets/)
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
context 'with column validation enabled (default)' do
|
|
108
|
-
let(:consolidator) { described_class.new(validate_columns: true) }
|
|
109
|
-
|
|
110
|
-
it 'passes when all files have same column count' do
|
|
111
|
-
stub_consolidation_success
|
|
112
|
-
metadata1 = { columns: 2, rows: 2, frames: 4 }
|
|
113
|
-
metadata2 = { columns: 2, rows: 3, frames: 6 }
|
|
114
|
-
|
|
115
|
-
allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata1, metadata2)
|
|
116
|
-
allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
|
|
117
|
-
allow(File).to receive(:size).and_return(1000)
|
|
118
|
-
|
|
119
|
-
expect {
|
|
120
|
-
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
121
|
-
}.not_to raise_error
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
it 'raises error when column counts do not match' do
|
|
125
|
-
metadata1 = { columns: 2, rows: 2, frames: 4 }
|
|
126
|
-
metadata2 = { columns: 4, rows: 1, frames: 4 }
|
|
127
|
-
|
|
128
|
-
allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata1, metadata2)
|
|
129
|
-
|
|
130
|
-
expect {
|
|
131
|
-
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
132
|
-
}.to raise_error(RubySpriter::ValidationError, /Column count mismatch/)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
it 'error message shows expected and actual column counts' do
|
|
136
|
-
metadata1 = { columns: 2, rows: 2, frames: 4 }
|
|
137
|
-
metadata2 = { columns: 4, rows: 1, frames: 4 }
|
|
138
|
-
|
|
139
|
-
allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata1, metadata2)
|
|
140
|
-
|
|
141
|
-
expect {
|
|
142
|
-
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
143
|
-
}.to raise_error(RubySpriter::ValidationError, /Expected 2, found 4/)
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
it 'error message suggests --no-validate-columns flag' do
|
|
147
|
-
metadata1 = { columns: 2, rows: 2, frames: 4 }
|
|
148
|
-
metadata2 = { columns: 4, rows: 1, frames: 4 }
|
|
149
|
-
|
|
150
|
-
allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata1, metadata2)
|
|
151
|
-
|
|
152
|
-
expect {
|
|
153
|
-
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
154
|
-
}.to raise_error(RubySpriter::ValidationError, /--no-validate-columns/)
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
context 'with column validation disabled' do
|
|
159
|
-
let(:consolidator) { described_class.new(validate_columns: false) }
|
|
160
|
-
before { stub_consolidation_success }
|
|
161
|
-
|
|
162
|
-
it 'allows mismatched column counts' do
|
|
163
|
-
metadata1 = { columns: 2, rows: 2, frames: 4 }
|
|
164
|
-
metadata2 = { columns: 4, rows: 1, frames: 4 }
|
|
165
|
-
|
|
166
|
-
allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata1, metadata2)
|
|
167
|
-
allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
|
|
168
|
-
allow(File).to receive(:size).and_return(1000)
|
|
169
|
-
|
|
170
|
-
expect {
|
|
171
|
-
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
172
|
-
}.not_to raise_error
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
context 'with ImageMagick consolidation' do
|
|
177
|
-
before do
|
|
178
|
-
stub_consolidation_success
|
|
179
|
-
allow(RubySpriter::MetadataManager).to receive(:read).and_return(
|
|
180
|
-
{ columns: 2, rows: 2, frames: 4 }
|
|
181
|
-
)
|
|
182
|
-
allow(File).to receive(:size).and_return(1000)
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
it 'calls ImageMagick with correct command' do
|
|
186
|
-
expect(Open3).to receive(:capture3) do |cmd|
|
|
187
|
-
expect(cmd).to include('convert')
|
|
188
|
-
expect(cmd).to include('-append')
|
|
189
|
-
expect(cmd).to include(spritesheet1)
|
|
190
|
-
expect(cmd).to include(spritesheet2)
|
|
191
|
-
expect(cmd).to include(output_file)
|
|
192
|
-
['', '', instance_double(Process::Status, success?: true)]
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
it 'uses -append flag for vertical stacking' do
|
|
199
|
-
expect(Open3).to receive(:capture3) do |cmd|
|
|
200
|
-
expect(cmd).to include('-append')
|
|
201
|
-
['', '', instance_double(Process::Status, success?: true)]
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
it 'raises ProcessingError when ImageMagick fails' do
|
|
208
|
-
allow(Open3).to receive(:capture3).and_return(
|
|
209
|
-
['', 'ImageMagick error', instance_double(Process::Status, success?: false)]
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
expect {
|
|
213
|
-
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
214
|
-
}.to raise_error(RubySpriter::ProcessingError, /ImageMagick consolidation failed/)
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
it 'shows debug output when debug option enabled' do
|
|
218
|
-
consolidator = described_class.new(debug: true)
|
|
219
|
-
|
|
220
|
-
allow(RubySpriter::MetadataManager).to receive(:read).and_return(
|
|
221
|
-
{ columns: 2, rows: 2, frames: 4 }
|
|
222
|
-
)
|
|
223
|
-
allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
|
|
224
|
-
allow(File).to receive(:size).and_return(1000)
|
|
225
|
-
|
|
226
|
-
expect {
|
|
227
|
-
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
228
|
-
}.to output(/DEBUG: ImageMagick command/).to_stdout
|
|
229
|
-
end
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
context 'with successful consolidation' do
|
|
233
|
-
before do
|
|
234
|
-
stub_consolidation_success
|
|
235
|
-
allow(RubySpriter::MetadataManager).to receive(:read).and_return(
|
|
236
|
-
{ columns: 2, rows: 2, frames: 4 },
|
|
237
|
-
{ columns: 2, rows: 3, frames: 6 }
|
|
238
|
-
)
|
|
239
|
-
allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
|
|
240
|
-
allow(File).to receive(:size).and_return(5000)
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
it 'calculates correct total frames' do
|
|
244
|
-
result = consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
245
|
-
|
|
246
|
-
expect(result[:frames]).to eq(10) # 4 + 6
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
it 'calculates correct row count' do
|
|
250
|
-
result = consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
251
|
-
|
|
252
|
-
# 10 frames / 2 columns = 5 rows
|
|
253
|
-
expect(result[:rows]).to eq(5)
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
it 'uses columns from first spritesheet' do
|
|
257
|
-
result = consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
258
|
-
|
|
259
|
-
expect(result[:columns]).to eq(2)
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
it 'embeds metadata in output file' do
|
|
263
|
-
expect(RubySpriter::MetadataManager).to receive(:embed).with(
|
|
264
|
-
anything,
|
|
265
|
-
output_file,
|
|
266
|
-
hash_including(columns: 2, rows: 5, frames: 10)
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
it 'returns hash with correct keys' do
|
|
273
|
-
result = consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
274
|
-
|
|
275
|
-
expect(result).to include(
|
|
276
|
-
output_file: output_file,
|
|
277
|
-
columns: 2,
|
|
278
|
-
rows: 5,
|
|
279
|
-
frames: 10,
|
|
280
|
-
size: 5000
|
|
281
|
-
)
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
it 'returns correct file size' do
|
|
285
|
-
result = consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
286
|
-
|
|
287
|
-
expect(result[:size]).to eq(5000)
|
|
288
|
-
end
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
context 'with 3 spritesheets' do
|
|
292
|
-
before do
|
|
293
|
-
stub_consolidation_success
|
|
294
|
-
allow(RubySpriter::MetadataManager).to receive(:read).and_return(
|
|
295
|
-
{ columns: 4, rows: 1, frames: 4 },
|
|
296
|
-
{ columns: 4, rows: 1, frames: 4 },
|
|
297
|
-
{ columns: 4, rows: 1, frames: 4 }
|
|
298
|
-
)
|
|
299
|
-
allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
|
|
300
|
-
allow(File).to receive(:size).and_return(8000)
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
it 'successfully consolidates all 3 files' do
|
|
304
|
-
expect(Open3).to receive(:capture3) do |cmd|
|
|
305
|
-
expect(cmd).to include(spritesheet1)
|
|
306
|
-
expect(cmd).to include(spritesheet2)
|
|
307
|
-
expect(cmd).to include(spritesheet3)
|
|
308
|
-
['', '', instance_double(Process::Status, success?: true)]
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
consolidator.consolidate([spritesheet1, spritesheet2, spritesheet3], output_file)
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
it 'calculates correct total frames for 3 files' do
|
|
315
|
-
result = consolidator.consolidate([spritesheet1, spritesheet2, spritesheet3], output_file)
|
|
316
|
-
|
|
317
|
-
expect(result[:frames]).to eq(12) # 4 + 4 + 4
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
it 'calculates correct row count for 3 files' do
|
|
321
|
-
result = consolidator.consolidate([spritesheet1, spritesheet2, spritesheet3], output_file)
|
|
322
|
-
|
|
323
|
-
# 12 frames / 4 columns = 3 rows
|
|
324
|
-
expect(result[:rows]).to eq(3)
|
|
325
|
-
end
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
context 'with output display' do
|
|
329
|
-
before do
|
|
330
|
-
stub_consolidation_success
|
|
331
|
-
allow(RubySpriter::MetadataManager).to receive(:read).and_return(
|
|
332
|
-
{ columns: 2, rows: 2, frames: 4 },
|
|
333
|
-
{ columns: 2, rows: 3, frames: 6 }
|
|
334
|
-
)
|
|
335
|
-
allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
|
|
336
|
-
allow(File).to receive(:size).and_return(5000)
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
it 'displays success message' do
|
|
340
|
-
expect {
|
|
341
|
-
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
342
|
-
}.to output(/Consolidated spritesheet created/).to_stdout
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
it 'displays output file path' do
|
|
346
|
-
expect {
|
|
347
|
-
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
348
|
-
}.to output(/Output:.*consolidated\.png/).to_stdout
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
it 'displays grid layout information' do
|
|
352
|
-
expect {
|
|
353
|
-
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
354
|
-
}.to output(/Grid Layout:.*Columns: 2.*Rows: 5.*Total Frames: 10/m).to_stdout
|
|
355
|
-
end
|
|
356
|
-
|
|
357
|
-
it 'displays Godot AnimatedSprite2D settings' do
|
|
358
|
-
expect {
|
|
359
|
-
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
360
|
-
}.to output(/Godot AnimatedSprite2D Settings:.*HFrames = 2.*VFrames = 5/m).to_stdout
|
|
361
|
-
end
|
|
362
|
-
|
|
363
|
-
it 'displays source breakdown with frame counts' do
|
|
364
|
-
expect {
|
|
365
|
-
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
366
|
-
}.to output(/Source Breakdown:.*spritesheet_4x2\.png.*grid \(4 frames\).*spritesheet_6x2\.png.*grid \(6 frames\)/m).to_stdout
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
it 'displays number of combined spritesheets' do
|
|
370
|
-
expect {
|
|
371
|
-
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
372
|
-
}.to output(/Combined 2 spritesheets/).to_stdout
|
|
373
|
-
end
|
|
374
|
-
end
|
|
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
|
|
538
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
|
|
6
|
+
RSpec.describe RubySpriter::Consolidator do
|
|
7
|
+
let(:spritesheet1) { File.join(__dir__, '..', 'fixtures', 'spritesheet_4x2.png') }
|
|
8
|
+
let(:spritesheet2) { File.join(__dir__, '..', 'fixtures', 'spritesheet_6x2.png') }
|
|
9
|
+
let(:spritesheet3) { File.join(__dir__, '..', 'fixtures', 'spritesheet_4x4.png') }
|
|
10
|
+
let(:output_file) { File.join($test_temp_dir, 'consolidated.png') }
|
|
11
|
+
|
|
12
|
+
describe '#initialize' do
|
|
13
|
+
it 'initializes with empty options by default' do
|
|
14
|
+
consolidator = described_class.new
|
|
15
|
+
|
|
16
|
+
expect(consolidator.options).to eq({})
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'initializes with provided options' do
|
|
20
|
+
consolidator = described_class.new(debug: true, validate_columns: false)
|
|
21
|
+
|
|
22
|
+
expect(consolidator.options[:debug]).to be true
|
|
23
|
+
expect(consolidator.options[:validate_columns]).to be false
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe '#consolidate' do
|
|
28
|
+
let(:consolidator) { described_class.new }
|
|
29
|
+
|
|
30
|
+
# Helper to set up common mocks for successful consolidation
|
|
31
|
+
def stub_consolidation_success
|
|
32
|
+
allow(RubySpriter::MetadataManager).to receive(:embed)
|
|
33
|
+
allow(RubySpriter::Utils::FileHelper).to receive(:validate_exists!)
|
|
34
|
+
allow(File).to receive(:rename)
|
|
35
|
+
allow(File).to receive(:exist?).and_call_original
|
|
36
|
+
allow(File).to receive(:exist?).with(/consolidated.*temp/).and_return(true)
|
|
37
|
+
allow(File).to receive(:delete)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
context 'with file validation' do
|
|
41
|
+
it 'raises error when given 0 files' do
|
|
42
|
+
expect {
|
|
43
|
+
consolidator.consolidate([], output_file)
|
|
44
|
+
}.to raise_error(RubySpriter::ValidationError, /Need at least 2 files/)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'raises error when given only 1 file' do
|
|
48
|
+
expect {
|
|
49
|
+
consolidator.consolidate([spritesheet1], output_file)
|
|
50
|
+
}.to raise_error(RubySpriter::ValidationError, /Need at least 2 files/)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'accepts 2 files minimum' do
|
|
54
|
+
stub_consolidation_success
|
|
55
|
+
allow(RubySpriter::MetadataManager).to receive(:read).and_return(
|
|
56
|
+
{ columns: 2, rows: 2, frames: 4 }
|
|
57
|
+
)
|
|
58
|
+
allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
|
|
59
|
+
allow(File).to receive(:size).and_return(1000)
|
|
60
|
+
|
|
61
|
+
expect {
|
|
62
|
+
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
63
|
+
}.not_to raise_error
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'raises error when file does not exist' do
|
|
67
|
+
non_existent = 'non_existent.png'
|
|
68
|
+
|
|
69
|
+
expect {
|
|
70
|
+
consolidator.consolidate([spritesheet1, non_existent], output_file)
|
|
71
|
+
}.to raise_error(RubySpriter::ValidationError, /not found/)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
context 'with metadata reading' do
|
|
76
|
+
it 'reads metadata from all files' do
|
|
77
|
+
stub_consolidation_success
|
|
78
|
+
metadata1 = { columns: 2, rows: 2, frames: 4 }
|
|
79
|
+
metadata2 = { columns: 2, rows: 3, frames: 6 }
|
|
80
|
+
|
|
81
|
+
expect(RubySpriter::MetadataManager).to receive(:read).with(spritesheet1).and_return(metadata1)
|
|
82
|
+
expect(RubySpriter::MetadataManager).to receive(:read).with(spritesheet2).and_return(metadata2)
|
|
83
|
+
|
|
84
|
+
allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
|
|
85
|
+
allow(File).to receive(:size).and_return(1000)
|
|
86
|
+
|
|
87
|
+
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'raises ValidationError when file missing metadata' do
|
|
91
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(spritesheet1).and_return(nil)
|
|
92
|
+
|
|
93
|
+
expect {
|
|
94
|
+
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
95
|
+
}.to raise_error(RubySpriter::ValidationError, /missing metadata/)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'raises error with helpful message about Ruby Spriter spritesheets' do
|
|
99
|
+
allow(RubySpriter::MetadataManager).to receive(:read).with(spritesheet1).and_return(nil)
|
|
100
|
+
|
|
101
|
+
expect {
|
|
102
|
+
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
103
|
+
}.to raise_error(RubySpriter::ValidationError, /All files must be Ruby Spriter spritesheets/)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
context 'with column validation enabled (default)' do
|
|
108
|
+
let(:consolidator) { described_class.new(validate_columns: true) }
|
|
109
|
+
|
|
110
|
+
it 'passes when all files have same column count' do
|
|
111
|
+
stub_consolidation_success
|
|
112
|
+
metadata1 = { columns: 2, rows: 2, frames: 4 }
|
|
113
|
+
metadata2 = { columns: 2, rows: 3, frames: 6 }
|
|
114
|
+
|
|
115
|
+
allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata1, metadata2)
|
|
116
|
+
allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
|
|
117
|
+
allow(File).to receive(:size).and_return(1000)
|
|
118
|
+
|
|
119
|
+
expect {
|
|
120
|
+
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
121
|
+
}.not_to raise_error
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'raises error when column counts do not match' do
|
|
125
|
+
metadata1 = { columns: 2, rows: 2, frames: 4 }
|
|
126
|
+
metadata2 = { columns: 4, rows: 1, frames: 4 }
|
|
127
|
+
|
|
128
|
+
allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata1, metadata2)
|
|
129
|
+
|
|
130
|
+
expect {
|
|
131
|
+
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
132
|
+
}.to raise_error(RubySpriter::ValidationError, /Column count mismatch/)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'error message shows expected and actual column counts' do
|
|
136
|
+
metadata1 = { columns: 2, rows: 2, frames: 4 }
|
|
137
|
+
metadata2 = { columns: 4, rows: 1, frames: 4 }
|
|
138
|
+
|
|
139
|
+
allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata1, metadata2)
|
|
140
|
+
|
|
141
|
+
expect {
|
|
142
|
+
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
143
|
+
}.to raise_error(RubySpriter::ValidationError, /Expected 2, found 4/)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'error message suggests --no-validate-columns flag' do
|
|
147
|
+
metadata1 = { columns: 2, rows: 2, frames: 4 }
|
|
148
|
+
metadata2 = { columns: 4, rows: 1, frames: 4 }
|
|
149
|
+
|
|
150
|
+
allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata1, metadata2)
|
|
151
|
+
|
|
152
|
+
expect {
|
|
153
|
+
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
154
|
+
}.to raise_error(RubySpriter::ValidationError, /--no-validate-columns/)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
context 'with column validation disabled' do
|
|
159
|
+
let(:consolidator) { described_class.new(validate_columns: false) }
|
|
160
|
+
before { stub_consolidation_success }
|
|
161
|
+
|
|
162
|
+
it 'allows mismatched column counts' do
|
|
163
|
+
metadata1 = { columns: 2, rows: 2, frames: 4 }
|
|
164
|
+
metadata2 = { columns: 4, rows: 1, frames: 4 }
|
|
165
|
+
|
|
166
|
+
allow(RubySpriter::MetadataManager).to receive(:read).and_return(metadata1, metadata2)
|
|
167
|
+
allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
|
|
168
|
+
allow(File).to receive(:size).and_return(1000)
|
|
169
|
+
|
|
170
|
+
expect {
|
|
171
|
+
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
172
|
+
}.not_to raise_error
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
context 'with ImageMagick consolidation' do
|
|
177
|
+
before do
|
|
178
|
+
stub_consolidation_success
|
|
179
|
+
allow(RubySpriter::MetadataManager).to receive(:read).and_return(
|
|
180
|
+
{ columns: 2, rows: 2, frames: 4 }
|
|
181
|
+
)
|
|
182
|
+
allow(File).to receive(:size).and_return(1000)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it 'calls ImageMagick with correct command' do
|
|
186
|
+
expect(Open3).to receive(:capture3) do |cmd|
|
|
187
|
+
expect(cmd).to include('convert')
|
|
188
|
+
expect(cmd).to include('-append')
|
|
189
|
+
expect(cmd).to include(spritesheet1)
|
|
190
|
+
expect(cmd).to include(spritesheet2)
|
|
191
|
+
expect(cmd).to include(output_file)
|
|
192
|
+
['', '', instance_double(Process::Status, success?: true)]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it 'uses -append flag for vertical stacking' do
|
|
199
|
+
expect(Open3).to receive(:capture3) do |cmd|
|
|
200
|
+
expect(cmd).to include('-append')
|
|
201
|
+
['', '', instance_double(Process::Status, success?: true)]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it 'raises ProcessingError when ImageMagick fails' do
|
|
208
|
+
allow(Open3).to receive(:capture3).and_return(
|
|
209
|
+
['', 'ImageMagick error', instance_double(Process::Status, success?: false)]
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
expect {
|
|
213
|
+
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
214
|
+
}.to raise_error(RubySpriter::ProcessingError, /ImageMagick consolidation failed/)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it 'shows debug output when debug option enabled' do
|
|
218
|
+
consolidator = described_class.new(debug: true)
|
|
219
|
+
|
|
220
|
+
allow(RubySpriter::MetadataManager).to receive(:read).and_return(
|
|
221
|
+
{ columns: 2, rows: 2, frames: 4 }
|
|
222
|
+
)
|
|
223
|
+
allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
|
|
224
|
+
allow(File).to receive(:size).and_return(1000)
|
|
225
|
+
|
|
226
|
+
expect {
|
|
227
|
+
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
228
|
+
}.to output(/DEBUG: ImageMagick command/).to_stdout
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
context 'with successful consolidation' do
|
|
233
|
+
before do
|
|
234
|
+
stub_consolidation_success
|
|
235
|
+
allow(RubySpriter::MetadataManager).to receive(:read).and_return(
|
|
236
|
+
{ columns: 2, rows: 2, frames: 4 },
|
|
237
|
+
{ columns: 2, rows: 3, frames: 6 }
|
|
238
|
+
)
|
|
239
|
+
allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
|
|
240
|
+
allow(File).to receive(:size).and_return(5000)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it 'calculates correct total frames' do
|
|
244
|
+
result = consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
245
|
+
|
|
246
|
+
expect(result[:frames]).to eq(10) # 4 + 6
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
it 'calculates correct row count' do
|
|
250
|
+
result = consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
251
|
+
|
|
252
|
+
# 10 frames / 2 columns = 5 rows
|
|
253
|
+
expect(result[:rows]).to eq(5)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
it 'uses columns from first spritesheet' do
|
|
257
|
+
result = consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
258
|
+
|
|
259
|
+
expect(result[:columns]).to eq(2)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it 'embeds metadata in output file' do
|
|
263
|
+
expect(RubySpriter::MetadataManager).to receive(:embed).with(
|
|
264
|
+
anything,
|
|
265
|
+
output_file,
|
|
266
|
+
hash_including(columns: 2, rows: 5, frames: 10)
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
it 'returns hash with correct keys' do
|
|
273
|
+
result = consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
274
|
+
|
|
275
|
+
expect(result).to include(
|
|
276
|
+
output_file: output_file,
|
|
277
|
+
columns: 2,
|
|
278
|
+
rows: 5,
|
|
279
|
+
frames: 10,
|
|
280
|
+
size: 5000
|
|
281
|
+
)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
it 'returns correct file size' do
|
|
285
|
+
result = consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
286
|
+
|
|
287
|
+
expect(result[:size]).to eq(5000)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
context 'with 3 spritesheets' do
|
|
292
|
+
before do
|
|
293
|
+
stub_consolidation_success
|
|
294
|
+
allow(RubySpriter::MetadataManager).to receive(:read).and_return(
|
|
295
|
+
{ columns: 4, rows: 1, frames: 4 },
|
|
296
|
+
{ columns: 4, rows: 1, frames: 4 },
|
|
297
|
+
{ columns: 4, rows: 1, frames: 4 }
|
|
298
|
+
)
|
|
299
|
+
allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
|
|
300
|
+
allow(File).to receive(:size).and_return(8000)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
it 'successfully consolidates all 3 files' do
|
|
304
|
+
expect(Open3).to receive(:capture3) do |cmd|
|
|
305
|
+
expect(cmd).to include(spritesheet1)
|
|
306
|
+
expect(cmd).to include(spritesheet2)
|
|
307
|
+
expect(cmd).to include(spritesheet3)
|
|
308
|
+
['', '', instance_double(Process::Status, success?: true)]
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
consolidator.consolidate([spritesheet1, spritesheet2, spritesheet3], output_file)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
it 'calculates correct total frames for 3 files' do
|
|
315
|
+
result = consolidator.consolidate([spritesheet1, spritesheet2, spritesheet3], output_file)
|
|
316
|
+
|
|
317
|
+
expect(result[:frames]).to eq(12) # 4 + 4 + 4
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
it 'calculates correct row count for 3 files' do
|
|
321
|
+
result = consolidator.consolidate([spritesheet1, spritesheet2, spritesheet3], output_file)
|
|
322
|
+
|
|
323
|
+
# 12 frames / 4 columns = 3 rows
|
|
324
|
+
expect(result[:rows]).to eq(3)
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
context 'with output display' do
|
|
329
|
+
before do
|
|
330
|
+
stub_consolidation_success
|
|
331
|
+
allow(RubySpriter::MetadataManager).to receive(:read).and_return(
|
|
332
|
+
{ columns: 2, rows: 2, frames: 4 },
|
|
333
|
+
{ columns: 2, rows: 3, frames: 6 }
|
|
334
|
+
)
|
|
335
|
+
allow(Open3).to receive(:capture3).and_return(['', '', instance_double(Process::Status, success?: true)])
|
|
336
|
+
allow(File).to receive(:size).and_return(5000)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
it 'displays success message' do
|
|
340
|
+
expect {
|
|
341
|
+
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
342
|
+
}.to output(/Consolidated spritesheet created/).to_stdout
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
it 'displays output file path' do
|
|
346
|
+
expect {
|
|
347
|
+
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
348
|
+
}.to output(/Output:.*consolidated\.png/).to_stdout
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
it 'displays grid layout information' do
|
|
352
|
+
expect {
|
|
353
|
+
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
354
|
+
}.to output(/Grid Layout:.*Columns: 2.*Rows: 5.*Total Frames: 10/m).to_stdout
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
it 'displays Godot AnimatedSprite2D settings' do
|
|
358
|
+
expect {
|
|
359
|
+
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
360
|
+
}.to output(/Godot AnimatedSprite2D Settings:.*HFrames = 2.*VFrames = 5/m).to_stdout
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
it 'displays source breakdown with frame counts' do
|
|
364
|
+
expect {
|
|
365
|
+
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
366
|
+
}.to output(/Source Breakdown:.*spritesheet_4x2\.png.*grid \(4 frames\).*spritesheet_6x2\.png.*grid \(6 frames\)/m).to_stdout
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
it 'displays number of combined spritesheets' do
|
|
370
|
+
expect {
|
|
371
|
+
consolidator.consolidate([spritesheet1, spritesheet2], output_file)
|
|
372
|
+
}.to output(/Combined 2 spritesheets/).to_stdout
|
|
373
|
+
end
|
|
374
|
+
end
|
|
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
|
|
538
|
+
end
|