ruby_spriter 0.6.7.1 → 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 -524
- data/Gemfile +17 -17
- data/LICENSE +21 -21
- data/README.md +183 -950
- data/bin/ruby_spriter +20 -20
- data/lib/ruby_spriter/background_sampler.rb +140 -0
- data/lib/ruby_spriter/batch_processor.rb +268 -214
- 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 -224
- data/lib/ruby_spriter/ghost_edge_cleaner.rb +164 -0
- data/lib/ruby_spriter/gimp_processor.rb +1188 -1058
- data/lib/ruby_spriter/metadata_manager.rb +117 -116
- data/lib/ruby_spriter/platform.rb +137 -137
- data/lib/ruby_spriter/processor.rb +1230 -891
- 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 -92
- 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,425 +1,523 @@
|
|
|
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
|
|
383
|
-
script = processor_fuzzy.send(:generate_remove_bg_script, input_file, output_file)
|
|
384
|
-
|
|
385
|
-
#
|
|
386
|
-
expect(script).to include('gimp-image-select-contiguous-color')
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
expect(script).to include('
|
|
390
|
-
|
|
391
|
-
expect(script).
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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 from single interior point (not corners)' do
|
|
383
|
+
script = processor_fuzzy.send(:generate_remove_bg_script, input_file, output_file)
|
|
384
|
+
|
|
385
|
+
# Should use fuzzy select procedure
|
|
386
|
+
expect(script).to include('gimp-image-select-contiguous-color')
|
|
387
|
+
# Should sample from single interior point (5, 5) to avoid edge artifacts
|
|
388
|
+
expect(script).to include('x = 5')
|
|
389
|
+
expect(script).to include('y = 5')
|
|
390
|
+
# Should NOT loop through multiple corners
|
|
391
|
+
expect(script).not_to include('for i, (x, y) in enumerate(corners):')
|
|
392
|
+
# Should NOT use ADD operation (only REPLACE)
|
|
393
|
+
expect(script).not_to include('Gimp.ChannelOps.ADD')
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
context 'with global color select' do
|
|
398
|
+
let(:processor_global) { described_class.new(gimp_path, remove_bg: true, fuzzy_select: false) }
|
|
399
|
+
|
|
400
|
+
it 'generates script with global color select' do
|
|
401
|
+
script = processor_global.send(:generate_remove_bg_script, input_file, output_file)
|
|
402
|
+
|
|
403
|
+
expect(script).to include('gimp-image-select-color')
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
describe '#generate_global_select_code' do
|
|
408
|
+
let(:processor_with_threshold) do
|
|
409
|
+
described_class.new(gimp_path, remove_bg: true, fuzzy_select: false, bg_threshold: 5.0)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
it 'includes threshold parameter in global color select script' do
|
|
413
|
+
script = processor_with_threshold.send(:generate_global_select_code)
|
|
414
|
+
|
|
415
|
+
# Should set threshold via context API (0-255 range)
|
|
416
|
+
expect(script).to include("Gimp.context_set_sample_threshold_int(int(5.0))")
|
|
417
|
+
expect(script).to include('Threshold: 5.0')
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
it 'uses default threshold of 15.0 when not specified' do
|
|
421
|
+
processor_default = described_class.new(gimp_path, remove_bg: true, fuzzy_select: false)
|
|
422
|
+
script = processor_default.send(:generate_global_select_code)
|
|
423
|
+
|
|
424
|
+
# Should use default threshold via context API
|
|
425
|
+
expect(script).to include("Gimp.context_set_sample_threshold_int(int(15.0))")
|
|
426
|
+
expect(script).to include('Threshold: 15.0')
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
it 'uses 0.0 threshold for exact color matching when threshold is 0' do
|
|
430
|
+
processor_exact = described_class.new(gimp_path, remove_bg: true, fuzzy_select: false, bg_threshold: 0.0)
|
|
431
|
+
script = processor_exact.send(:generate_global_select_code)
|
|
432
|
+
|
|
433
|
+
expect(script).to include("Gimp.context_set_sample_threshold_int(int(0.0))")
|
|
434
|
+
expect(script).to include('Threshold: 0.0')
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
describe '#generate_feather_selection_code' do
|
|
439
|
+
it 'returns no feathering when feather_radius is not set' do
|
|
440
|
+
processor = described_class.new(gimp_path, remove_bg: true)
|
|
441
|
+
code = processor.send(:generate_feather_selection_code)
|
|
442
|
+
|
|
443
|
+
expect(code).to include('# No feathering')
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
it 'returns no feathering when feather_radius is 0' do
|
|
447
|
+
processor = described_class.new(gimp_path, remove_bg: true, feather_radius: 0.0)
|
|
448
|
+
code = processor.send(:generate_feather_selection_code)
|
|
449
|
+
|
|
450
|
+
expect(code).to include('# No feathering')
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
it 'includes feathering code when feather_radius is set' do
|
|
454
|
+
processor = described_class.new(gimp_path, remove_bg: true, feather_radius: 2.0)
|
|
455
|
+
code = processor.send(:generate_feather_selection_code)
|
|
456
|
+
|
|
457
|
+
expect(code).to include('gimp-selection-feather')
|
|
458
|
+
expect(code).to include('2.0')
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
it 'uses feather_radius parameter, not bg_threshold' do
|
|
462
|
+
processor = described_class.new(gimp_path, remove_bg: true, bg_threshold: 25.0, feather_radius: 3.0)
|
|
463
|
+
code = processor.send(:generate_feather_selection_code)
|
|
464
|
+
|
|
465
|
+
# Should use feather_radius (3.0), not bg_threshold (25.0)
|
|
466
|
+
expect(code).to include('3.0')
|
|
467
|
+
expect(code).not_to include('25.0')
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
describe 'threshold and feathering separation' do
|
|
472
|
+
it 'uses bg_threshold for color tolerance in global select' do
|
|
473
|
+
processor = described_class.new(gimp_path, remove_bg: true, fuzzy_select: false, bg_threshold: 20.0)
|
|
474
|
+
code = processor.send(:generate_global_select_code)
|
|
475
|
+
|
|
476
|
+
expect(code).to include("Gimp.context_set_sample_threshold_int(int(20.0))")
|
|
477
|
+
expect(code).to include('Threshold: 20.0')
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
it 'uses bg_threshold for color tolerance in fuzzy select' do
|
|
481
|
+
processor = described_class.new(gimp_path, remove_bg: true, fuzzy_select: true, bg_threshold: 10.0)
|
|
482
|
+
code = processor.send(:generate_fuzzy_select_code)
|
|
483
|
+
|
|
484
|
+
# Fuzzy select now supports threshold via context API
|
|
485
|
+
expect(code).to include("Gimp.context_set_sample_threshold_int(int(10.0))")
|
|
486
|
+
expect(code).to include('Threshold: 10.0')
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
it 'uses feather_radius for edge softening, independent of threshold' do
|
|
490
|
+
processor = described_class.new(gimp_path, remove_bg: true, bg_threshold: 15.0, feather_radius: 2.5)
|
|
491
|
+
|
|
492
|
+
select_code = processor.send(:generate_global_select_code)
|
|
493
|
+
feather_code = processor.send(:generate_feather_selection_code)
|
|
494
|
+
|
|
495
|
+
# Threshold used for color selection (0-255 range)
|
|
496
|
+
expect(select_code).to include('Gimp.context_set_sample_threshold_int(int(15.0))')
|
|
497
|
+
expect(select_code).to include('Threshold: 15.0')
|
|
498
|
+
|
|
499
|
+
# Feather radius used for edge softening
|
|
500
|
+
expect(feather_code).to include('2.5')
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
it 'includes file paths' do
|
|
505
|
+
processor_bg = described_class.new(gimp_path, remove_bg: true)
|
|
506
|
+
script = processor_bg.send(:generate_remove_bg_script, input_file, output_file)
|
|
507
|
+
|
|
508
|
+
expect(script).to include('input.png')
|
|
509
|
+
expect(script).to include('output.png')
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
it 'uses correct GIMP 3.x API' do
|
|
513
|
+
processor_bg = described_class.new(gimp_path, remove_bg: true)
|
|
514
|
+
script = processor_bg.send(:generate_remove_bg_script, input_file, output_file)
|
|
515
|
+
|
|
516
|
+
expect(script).to include('Gimp.file_load')
|
|
517
|
+
# Background removal selects corners directly (no inversion needed)
|
|
518
|
+
expect(script).to include('gimp-drawable-edit-clear')
|
|
519
|
+
expect(script).to include('gimp-selection-none')
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
end
|