ruby_spriter 0.6.5
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 +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +217 -0
- data/Gemfile +17 -0
- data/LICENSE +21 -0
- data/README.md +561 -0
- data/bin/ruby_spriter +20 -0
- data/lib/ruby_spriter/cli.rb +249 -0
- data/lib/ruby_spriter/consolidator.rb +146 -0
- data/lib/ruby_spriter/dependency_checker.rb +174 -0
- data/lib/ruby_spriter/gimp_processor.rb +664 -0
- data/lib/ruby_spriter/metadata_manager.rb +116 -0
- data/lib/ruby_spriter/platform.rb +82 -0
- data/lib/ruby_spriter/processor.rb +251 -0
- data/lib/ruby_spriter/utils/file_helper.rb +57 -0
- data/lib/ruby_spriter/utils/output_formatter.rb +65 -0
- data/lib/ruby_spriter/utils/path_helper.rb +59 -0
- data/lib/ruby_spriter/version.rb +7 -0
- data/lib/ruby_spriter/video_processor.rb +139 -0
- data/lib/ruby_spriter.rb +31 -0
- data/ruby_spriter.gemspec +42 -0
- data/spec/fixtures/image_without_metadata.png +0 -0
- data/spec/fixtures/spritesheet_4x2.png +0 -0
- data/spec/fixtures/spritesheet_4x4.png +0 -0
- data/spec/fixtures/spritesheet_6x2.png +0 -0
- data/spec/fixtures/spritesheet_with_metadata.png +0 -0
- data/spec/fixtures/test_video.mp4 +0 -0
- data/spec/ruby_spriter/cli_spec.rb +1142 -0
- data/spec/ruby_spriter/consolidator_spec.rb +375 -0
- data/spec/ruby_spriter/dependency_checker_spec.rb +0 -0
- data/spec/ruby_spriter/gimp_processor_spec.rb +425 -0
- data/spec/ruby_spriter/metadata_manager_spec.rb +0 -0
- data/spec/ruby_spriter/platform_spec.rb +82 -0
- data/spec/ruby_spriter/processor_spec.rb +0 -0
- data/spec/ruby_spriter/utils/file_helper_spec.rb +71 -0
- data/spec/ruby_spriter/utils/output_formatter_spec.rb +0 -0
- data/spec/ruby_spriter/utils/path_helper_spec.rb +78 -0
- data/spec/ruby_spriter/video_processor_spec.rb +0 -0
- data/spec/spec_helper.rb +41 -0
- metadata +88 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RubySpriter::GimpProcessor do
|
|
6
|
+
let(:gimp_path) { '/usr/bin/gimp' }
|
|
7
|
+
let(:test_image) { File.join(__dir__, '..', 'fixtures', 'spritesheet_4x2.png') }
|
|
8
|
+
|
|
9
|
+
describe '#initialize' do
|
|
10
|
+
it 'initializes with gimp_path and options' do
|
|
11
|
+
processor = described_class.new(gimp_path, scale_percent: 50)
|
|
12
|
+
|
|
13
|
+
expect(processor.gimp_path).to eq(gimp_path)
|
|
14
|
+
expect(processor.options[:scale_percent]).to eq(50)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'initializes with empty options by default' do
|
|
18
|
+
processor = described_class.new(gimp_path)
|
|
19
|
+
|
|
20
|
+
expect(processor.gimp_path).to eq(gimp_path)
|
|
21
|
+
expect(processor.options).to eq({})
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'stores multiple options' do
|
|
25
|
+
processor = described_class.new(gimp_path, {
|
|
26
|
+
scale_percent: 50,
|
|
27
|
+
remove_bg: true,
|
|
28
|
+
scale_interpolation: 'nohalo',
|
|
29
|
+
sharpen: true
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
expect(processor.options[:scale_percent]).to eq(50)
|
|
33
|
+
expect(processor.options[:remove_bg]).to eq(true)
|
|
34
|
+
expect(processor.options[:scale_interpolation]).to eq('nohalo')
|
|
35
|
+
expect(processor.options[:sharpen]).to eq(true)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe '#determine_operations' do
|
|
40
|
+
context 'with no operations requested' do
|
|
41
|
+
it 'returns empty array' do
|
|
42
|
+
processor = described_class.new(gimp_path, {})
|
|
43
|
+
operations = processor.send(:determine_operations)
|
|
44
|
+
|
|
45
|
+
expect(operations).to eq([])
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
context 'with scale only' do
|
|
50
|
+
it 'returns scale operation only' do
|
|
51
|
+
processor = described_class.new(gimp_path, scale_percent: 50)
|
|
52
|
+
operations = processor.send(:determine_operations)
|
|
53
|
+
|
|
54
|
+
expect(operations).to eq([:scale_image])
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
context 'with remove_bg only' do
|
|
59
|
+
it 'returns remove_background operation only' do
|
|
60
|
+
processor = described_class.new(gimp_path, remove_bg: true)
|
|
61
|
+
operations = processor.send(:determine_operations)
|
|
62
|
+
|
|
63
|
+
expect(operations).to eq([:remove_background])
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
context 'with both scale and remove_bg (auto-optimization)' do
|
|
68
|
+
it 'automatically does remove_bg first by default' do
|
|
69
|
+
processor = described_class.new(gimp_path, {
|
|
70
|
+
scale_percent: 50,
|
|
71
|
+
remove_bg: true,
|
|
72
|
+
operation_order: :scale_then_remove_bg # Default, but will be auto-optimized
|
|
73
|
+
})
|
|
74
|
+
operations = processor.send(:determine_operations)
|
|
75
|
+
|
|
76
|
+
# Auto-optimization: remove_bg should come first
|
|
77
|
+
expect(operations).to eq([:remove_background, :scale_image])
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'respects explicit remove_bg_then_scale order' do
|
|
81
|
+
processor = described_class.new(gimp_path, {
|
|
82
|
+
scale_percent: 50,
|
|
83
|
+
remove_bg: true,
|
|
84
|
+
operation_order: :remove_bg_then_scale
|
|
85
|
+
})
|
|
86
|
+
operations = processor.send(:determine_operations)
|
|
87
|
+
|
|
88
|
+
expect(operations).to eq([:remove_background, :scale_image])
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'respects explicit scale_then_remove_bg order when not auto-optimized' do
|
|
92
|
+
processor = described_class.new(gimp_path, {
|
|
93
|
+
scale_percent: 50,
|
|
94
|
+
remove_bg: true,
|
|
95
|
+
operation_order: :scale_then_remove_bg,
|
|
96
|
+
auto_optimize: false # If this were implemented
|
|
97
|
+
})
|
|
98
|
+
operations = processor.send(:determine_operations)
|
|
99
|
+
|
|
100
|
+
# With auto-optimization, this still gets optimized
|
|
101
|
+
expect(operations).to eq([:remove_background, :scale_image])
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
context 'operation order edge cases' do
|
|
106
|
+
it 'handles scale=nil, remove_bg=true' do
|
|
107
|
+
processor = described_class.new(gimp_path, {
|
|
108
|
+
scale_percent: nil,
|
|
109
|
+
remove_bg: true
|
|
110
|
+
})
|
|
111
|
+
operations = processor.send(:determine_operations)
|
|
112
|
+
|
|
113
|
+
expect(operations).to eq([:remove_background])
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'handles scale=50, remove_bg=false' do
|
|
117
|
+
processor = described_class.new(gimp_path, {
|
|
118
|
+
scale_percent: 50,
|
|
119
|
+
remove_bg: false
|
|
120
|
+
})
|
|
121
|
+
operations = processor.send(:determine_operations)
|
|
122
|
+
|
|
123
|
+
expect(operations).to eq([:scale_image])
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
describe '#map_interpolation_method' do
|
|
129
|
+
let(:processor) { described_class.new(gimp_path) }
|
|
130
|
+
|
|
131
|
+
it 'maps "none" to GIMP NONE constant' do
|
|
132
|
+
result = processor.send(:map_interpolation_method, 'none')
|
|
133
|
+
expect(result).to eq('Gimp.InterpolationType.NONE')
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'maps "linear" to GIMP LINEAR constant' do
|
|
137
|
+
result = processor.send(:map_interpolation_method, 'linear')
|
|
138
|
+
expect(result).to eq('Gimp.InterpolationType.LINEAR')
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it 'maps "cubic" to GIMP CUBIC constant' do
|
|
142
|
+
result = processor.send(:map_interpolation_method, 'cubic')
|
|
143
|
+
expect(result).to eq('Gimp.InterpolationType.CUBIC')
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'maps "nohalo" to GIMP NOHALO constant' do
|
|
147
|
+
result = processor.send(:map_interpolation_method, 'nohalo')
|
|
148
|
+
expect(result).to eq('Gimp.InterpolationType.NOHALO')
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it 'maps "lohalo" to GIMP LOHALO constant' do
|
|
152
|
+
result = processor.send(:map_interpolation_method, 'lohalo')
|
|
153
|
+
expect(result).to eq('Gimp.InterpolationType.LOHALO')
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'is case-insensitive' do
|
|
157
|
+
expect(processor.send(:map_interpolation_method, 'NOHALO')).to eq('Gimp.InterpolationType.NOHALO')
|
|
158
|
+
expect(processor.send(:map_interpolation_method, 'NoHalo')).to eq('Gimp.InterpolationType.NOHALO')
|
|
159
|
+
expect(processor.send(:map_interpolation_method, 'LINEAR')).to eq('Gimp.InterpolationType.LINEAR')
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it 'accepts symbol input' do
|
|
163
|
+
result = processor.send(:map_interpolation_method, :nohalo)
|
|
164
|
+
expect(result).to eq('Gimp.InterpolationType.NOHALO')
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'defaults to NOHALO for unknown methods' do
|
|
168
|
+
expect(processor.send(:map_interpolation_method, 'unknown')).to eq('Gimp.InterpolationType.NOHALO')
|
|
169
|
+
expect(processor.send(:map_interpolation_method, 'foo')).to eq('Gimp.InterpolationType.NOHALO')
|
|
170
|
+
expect(processor.send(:map_interpolation_method, '')).to eq('Gimp.InterpolationType.NOHALO')
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it 'defaults to NOHALO for nil' do
|
|
174
|
+
result = processor.send(:map_interpolation_method, nil)
|
|
175
|
+
expect(result).to eq('Gimp.InterpolationType.NOHALO')
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
describe '#filter_gimp_output' do
|
|
180
|
+
let(:processor) { described_class.new(gimp_path) }
|
|
181
|
+
|
|
182
|
+
it 'filters GEGL-WARNING lines' do
|
|
183
|
+
output = "GEGL-WARNING: some warning\nUseful output\n"
|
|
184
|
+
result = processor.send(:filter_gimp_output, output)
|
|
185
|
+
|
|
186
|
+
expect(result).to eq("Useful output\n")
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it 'filters gegl_tile_cache_destroy lines' do
|
|
190
|
+
output = "gegl_tile_cache_destroy: leaked tiles\nUseful output\n"
|
|
191
|
+
result = processor.send(:filter_gimp_output, output)
|
|
192
|
+
|
|
193
|
+
expect(result).to eq("Useful output\n")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it 'filters runtime check failed lines' do
|
|
197
|
+
output = "runtime check failed: something\nUseful output\n"
|
|
198
|
+
result = processor.send(:filter_gimp_output, output)
|
|
199
|
+
|
|
200
|
+
expect(result).to eq("Useful output\n")
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it 'filters batch command executed successfully lines' do
|
|
204
|
+
output = "batch command executed successfully\nUseful output\n"
|
|
205
|
+
result = processor.send(:filter_gimp_output, output)
|
|
206
|
+
|
|
207
|
+
expect(result).to eq("Useful output\n")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it 'filters GEGL_DEBUG buffer-alloc lines' do
|
|
211
|
+
output = "GEGL_DEBUG: buffer-alloc details\nUseful output\n"
|
|
212
|
+
result = processor.send(:filter_gimp_output, output)
|
|
213
|
+
|
|
214
|
+
expect(result).to eq("Useful output\n")
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it 'filters GeglBuffers leaked lines' do
|
|
218
|
+
output = "GeglBuffers leaked: 5\nUseful output\n"
|
|
219
|
+
result = processor.send(:filter_gimp_output, output)
|
|
220
|
+
|
|
221
|
+
expect(result).to eq("Useful output\n")
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
it 'filters EEEEeEeek lines' do
|
|
225
|
+
output = "EEEEeEeek! scary message\nUseful output\n"
|
|
226
|
+
result = processor.send(:filter_gimp_output, output)
|
|
227
|
+
|
|
228
|
+
expect(result).to eq("Useful output\n")
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
it 'filters empty lines' do
|
|
232
|
+
output = "Line 1\n\nLine 2\n \nLine 3\n"
|
|
233
|
+
result = processor.send(:filter_gimp_output, output)
|
|
234
|
+
|
|
235
|
+
expect(result).to eq("Line 1\nLine 2\nLine 3\n")
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it 'filters multiple warning types in one output' do
|
|
239
|
+
output = <<~OUTPUT
|
|
240
|
+
GEGL-WARNING: warning 1
|
|
241
|
+
Useful line 1
|
|
242
|
+
gegl_tile_cache_destroy: leak
|
|
243
|
+
Useful line 2
|
|
244
|
+
runtime check failed
|
|
245
|
+
batch command executed successfully
|
|
246
|
+
Useful line 3
|
|
247
|
+
OUTPUT
|
|
248
|
+
|
|
249
|
+
result = processor.send(:filter_gimp_output, output)
|
|
250
|
+
|
|
251
|
+
expect(result).to eq("Useful line 1\nUseful line 2\nUseful line 3\n")
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
it 'returns empty string when all lines are filtered' do
|
|
255
|
+
output = <<~OUTPUT
|
|
256
|
+
GEGL-WARNING: warning
|
|
257
|
+
gegl_tile_cache_destroy: leak
|
|
258
|
+
runtime check failed
|
|
259
|
+
OUTPUT
|
|
260
|
+
|
|
261
|
+
result = processor.send(:filter_gimp_output, output)
|
|
262
|
+
|
|
263
|
+
expect(result).to eq("")
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
it 'preserves important error messages' do
|
|
267
|
+
output = "Error: File not found\nGEGL-WARNING: some warning\n"
|
|
268
|
+
result = processor.send(:filter_gimp_output, output)
|
|
269
|
+
|
|
270
|
+
expect(result).to eq("Error: File not found\n")
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
describe '#has_important_messages?' do
|
|
275
|
+
let(:processor) { described_class.new(gimp_path) }
|
|
276
|
+
|
|
277
|
+
it 'returns false for only filtered warnings' do
|
|
278
|
+
output = <<~OUTPUT
|
|
279
|
+
GEGL-WARNING: warning
|
|
280
|
+
gegl_tile_cache_destroy: leak
|
|
281
|
+
OUTPUT
|
|
282
|
+
|
|
283
|
+
expect(processor.send(:has_important_messages?, output)).to be false
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
it 'returns false for empty output' do
|
|
287
|
+
expect(processor.send(:has_important_messages?, "")).to be false
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
it 'returns false for only SUCCESS messages' do
|
|
291
|
+
output = "SUCCESS: Operation completed\n"
|
|
292
|
+
|
|
293
|
+
expect(processor.send(:has_important_messages?, output)).to be false
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
it 'returns false for SUCCESS messages mixed with filtered warnings' do
|
|
297
|
+
output = <<~OUTPUT
|
|
298
|
+
GEGL-WARNING: warning
|
|
299
|
+
SUCCESS: Operation completed
|
|
300
|
+
gegl_tile_cache_destroy: leak
|
|
301
|
+
OUTPUT
|
|
302
|
+
|
|
303
|
+
expect(processor.send(:has_important_messages?, output)).to be false
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
it 'returns true for error messages' do
|
|
307
|
+
output = "Error: Something went wrong\n"
|
|
308
|
+
|
|
309
|
+
expect(processor.send(:has_important_messages?, output)).to be true
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
it 'returns true for important messages mixed with warnings' do
|
|
313
|
+
output = <<~OUTPUT
|
|
314
|
+
GEGL-WARNING: warning
|
|
315
|
+
Error: File not found
|
|
316
|
+
gegl_tile_cache_destroy: leak
|
|
317
|
+
OUTPUT
|
|
318
|
+
|
|
319
|
+
expect(processor.send(:has_important_messages?, output)).to be true
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
it 'returns true for non-filtered, non-SUCCESS output' do
|
|
323
|
+
output = "Processing image...\nDone\n"
|
|
324
|
+
|
|
325
|
+
expect(processor.send(:has_important_messages?, output)).to be true
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
describe 'script generation' do
|
|
330
|
+
let(:processor) { described_class.new(gimp_path, scale_percent: 50, scale_interpolation: 'nohalo') }
|
|
331
|
+
let(:input_file) { '/path/to/input.png' }
|
|
332
|
+
let(:output_file) { '/path/to/output.png' }
|
|
333
|
+
|
|
334
|
+
describe '#generate_scale_script' do
|
|
335
|
+
it 'generates Python script with correct file paths' do
|
|
336
|
+
script = processor.send(:generate_scale_script, input_file, output_file, 50)
|
|
337
|
+
|
|
338
|
+
expect(script).to include('input.png')
|
|
339
|
+
expect(script).to include('output.png')
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
it 'includes correct scale percentage' do
|
|
343
|
+
script = processor.send(:generate_scale_script, input_file, output_file, 50)
|
|
344
|
+
|
|
345
|
+
# Percent is embedded directly and divided by 100 in Python: int(w * 50 / 100.0)
|
|
346
|
+
expect(script).to include('* 50 / 100.0')
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
it 'includes correct interpolation method' do
|
|
350
|
+
script = processor.send(:generate_scale_script, input_file, output_file, 50)
|
|
351
|
+
|
|
352
|
+
expect(script).to include('Gimp.InterpolationType.NOHALO')
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
it 'uses correct GIMP 3.x API' do
|
|
356
|
+
script = processor.send(:generate_scale_script, input_file, output_file, 50)
|
|
357
|
+
|
|
358
|
+
expect(script).to include('Gimp.file_load')
|
|
359
|
+
expect(script).to include('gimp-context-set-interpolation')
|
|
360
|
+
expect(script).to include('gimp-layer-scale')
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
it 'handles different scale percentages' do
|
|
364
|
+
script_25 = processor.send(:generate_scale_script, input_file, output_file, 25)
|
|
365
|
+
script_75 = processor.send(:generate_scale_script, input_file, output_file, 75)
|
|
366
|
+
|
|
367
|
+
expect(script_25).to include('* 25 / 100.0')
|
|
368
|
+
expect(script_75).to include('* 75 / 100.0')
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
describe '#generate_remove_bg_script' do
|
|
373
|
+
context 'with fuzzy select (default)' do
|
|
374
|
+
let(:processor_fuzzy) { described_class.new(gimp_path, remove_bg: true, fuzzy_select: true) }
|
|
375
|
+
|
|
376
|
+
it 'generates script with fuzzy select' do
|
|
377
|
+
script = processor_fuzzy.send(:generate_remove_bg_script, input_file, output_file)
|
|
378
|
+
|
|
379
|
+
expect(script).to include('gimp-image-select-contiguous-color')
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
it 'samples all four corners' do
|
|
383
|
+
script = processor_fuzzy.send(:generate_remove_bg_script, input_file, output_file)
|
|
384
|
+
|
|
385
|
+
# The procedure is looked up once, then used in a loop for all 4 corners
|
|
386
|
+
expect(script).to include('gimp-image-select-contiguous-color')
|
|
387
|
+
expect(script).to include('for i, (x, y) in enumerate(corners):')
|
|
388
|
+
# Verify corners array has 4 entries
|
|
389
|
+
expect(script).to include('(0, 0)') # Top-left
|
|
390
|
+
expect(script).to include('(w-1, 0)') # Top-right
|
|
391
|
+
expect(script).to include('(0, h-1)') # Bottom-left
|
|
392
|
+
expect(script).to include('(w-1, h-1)') # Bottom-right
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
context 'with global color select' do
|
|
397
|
+
let(:processor_global) { described_class.new(gimp_path, remove_bg: true, fuzzy_select: false) }
|
|
398
|
+
|
|
399
|
+
it 'generates script with global color select' do
|
|
400
|
+
script = processor_global.send(:generate_remove_bg_script, input_file, output_file)
|
|
401
|
+
|
|
402
|
+
expect(script).to include('gimp-image-select-color')
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
it 'includes file paths' do
|
|
407
|
+
processor_bg = described_class.new(gimp_path, remove_bg: true)
|
|
408
|
+
script = processor_bg.send(:generate_remove_bg_script, input_file, output_file)
|
|
409
|
+
|
|
410
|
+
expect(script).to include('input.png')
|
|
411
|
+
expect(script).to include('output.png')
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
it 'uses correct GIMP 3.x API' do
|
|
415
|
+
processor_bg = described_class.new(gimp_path, remove_bg: true)
|
|
416
|
+
script = processor_bg.send(:generate_remove_bg_script, input_file, output_file)
|
|
417
|
+
|
|
418
|
+
expect(script).to include('Gimp.file_load')
|
|
419
|
+
# Background removal selects corners directly (no inversion needed)
|
|
420
|
+
expect(script).to include('gimp-drawable-edit-clear')
|
|
421
|
+
expect(script).to include('gimp-selection-none')
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
end
|
|
File without changes
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RubySpriter::Platform do
|
|
6
|
+
describe '.current' do
|
|
7
|
+
it 'returns a valid platform type' do
|
|
8
|
+
expect([:windows, :linux, :macos, :unknown]).to include(described_class.current)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe '.windows?' do
|
|
13
|
+
it 'returns boolean' do
|
|
14
|
+
expect([true, false]).to include(described_class.windows?)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe '.linux?' do
|
|
19
|
+
it 'returns boolean' do
|
|
20
|
+
expect([true, false]).to include(described_class.linux?)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe '.macos?' do
|
|
25
|
+
it 'returns boolean' do
|
|
26
|
+
expect([true, false]).to include(described_class.macos?)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe '.default_gimp_path' do
|
|
31
|
+
it 'returns a string path' do
|
|
32
|
+
expect(described_class.default_gimp_path).to be_a(String)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'returns platform-appropriate path' do
|
|
36
|
+
path = described_class.default_gimp_path
|
|
37
|
+
|
|
38
|
+
if described_class.windows?
|
|
39
|
+
expect(path).to match(/GIMP/)
|
|
40
|
+
expect(path).to match(/\.exe$/)
|
|
41
|
+
else
|
|
42
|
+
expect(path).to start_with('/')
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe '.alternative_gimp_paths' do
|
|
48
|
+
it 'returns an array' do
|
|
49
|
+
expect(described_class.alternative_gimp_paths).to be_an(Array)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'contains only strings' do
|
|
53
|
+
described_class.alternative_gimp_paths.each do |path|
|
|
54
|
+
expect(path).to be_a(String)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
describe '.imagemagick_convert_cmd' do
|
|
60
|
+
it 'returns appropriate command for platform' do
|
|
61
|
+
cmd = described_class.imagemagick_convert_cmd
|
|
62
|
+
|
|
63
|
+
if described_class.windows?
|
|
64
|
+
expect(cmd).to eq('magick convert')
|
|
65
|
+
else
|
|
66
|
+
expect(cmd).to eq('convert')
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe '.imagemagick_identify_cmd' do
|
|
72
|
+
it 'returns appropriate command for platform' do
|
|
73
|
+
cmd = described_class.imagemagick_identify_cmd
|
|
74
|
+
|
|
75
|
+
if described_class.windows?
|
|
76
|
+
expect(cmd).to eq('magick identify')
|
|
77
|
+
else
|
|
78
|
+
expect(cmd).to eq('identify')
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
File without changes
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
end
|
|
File without changes
|
|
@@ -0,0 +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
|
|
File without changes
|