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,174 +1,224 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'open3'
|
|
4
|
-
|
|
5
|
-
module RubySpriter
|
|
6
|
-
# Checks for required external dependencies
|
|
7
|
-
class DependencyChecker
|
|
8
|
-
REQUIRED_TOOLS = {
|
|
9
|
-
ffmpeg: {
|
|
10
|
-
command: 'ffmpeg -version',
|
|
11
|
-
pattern: /ffmpeg version/i,
|
|
12
|
-
install: {
|
|
13
|
-
windows: 'choco install ffmpeg',
|
|
14
|
-
linux: 'sudo apt install ffmpeg',
|
|
15
|
-
macos: 'brew install ffmpeg'
|
|
16
|
-
}
|
|
17
|
-
},
|
|
18
|
-
ffprobe: {
|
|
19
|
-
command: 'ffprobe -version',
|
|
20
|
-
pattern: /ffprobe version/i,
|
|
21
|
-
install: {
|
|
22
|
-
windows: 'Included with ffmpeg',
|
|
23
|
-
linux: 'Included with ffmpeg',
|
|
24
|
-
macos: 'Included with ffmpeg'
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
imagemagick: {
|
|
28
|
-
command: Platform.imagemagick_identify_cmd + ' -version',
|
|
29
|
-
pattern: /ImageMagick/i,
|
|
30
|
-
install: {
|
|
31
|
-
windows: 'choco install imagemagick',
|
|
32
|
-
linux: 'sudo apt install imagemagick',
|
|
33
|
-
macos: 'brew install imagemagick'
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
results.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
# Try
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module RubySpriter
|
|
6
|
+
# Checks for required external dependencies
|
|
7
|
+
class DependencyChecker
|
|
8
|
+
REQUIRED_TOOLS = {
|
|
9
|
+
ffmpeg: {
|
|
10
|
+
command: 'ffmpeg -version',
|
|
11
|
+
pattern: /ffmpeg version/i,
|
|
12
|
+
install: {
|
|
13
|
+
windows: 'choco install ffmpeg',
|
|
14
|
+
linux: 'sudo apt install ffmpeg',
|
|
15
|
+
macos: 'brew install ffmpeg'
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
ffprobe: {
|
|
19
|
+
command: 'ffprobe -version',
|
|
20
|
+
pattern: /ffprobe version/i,
|
|
21
|
+
install: {
|
|
22
|
+
windows: 'Included with ffmpeg',
|
|
23
|
+
linux: 'Included with ffmpeg',
|
|
24
|
+
macos: 'Included with ffmpeg'
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
imagemagick: {
|
|
28
|
+
command: Platform.imagemagick_identify_cmd + ' -version',
|
|
29
|
+
pattern: /ImageMagick/i,
|
|
30
|
+
install: {
|
|
31
|
+
windows: 'choco install imagemagick',
|
|
32
|
+
linux: 'sudo apt install imagemagick',
|
|
33
|
+
macos: 'brew install imagemagick'
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
xvfb: {
|
|
37
|
+
command: 'xvfb-run --help',
|
|
38
|
+
pattern: /xvfb-run/i,
|
|
39
|
+
install: {
|
|
40
|
+
windows: 'Not required on Windows',
|
|
41
|
+
linux: 'sudo apt install xvfb',
|
|
42
|
+
macos: 'Not required on macOS'
|
|
43
|
+
},
|
|
44
|
+
optional_for: [:windows, :macos] # Only required on Linux
|
|
45
|
+
}
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
def initialize(verbose: false)
|
|
49
|
+
@verbose = verbose
|
|
50
|
+
@gimp_path = nil
|
|
51
|
+
@gimp_version = nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Check all dependencies
|
|
55
|
+
# @return [Hash] Results of dependency checks
|
|
56
|
+
def check_all
|
|
57
|
+
results = {}
|
|
58
|
+
|
|
59
|
+
REQUIRED_TOOLS.each do |tool, config|
|
|
60
|
+
results[tool] = check_tool(tool, config)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
results[:gimp] = check_gimp
|
|
64
|
+
|
|
65
|
+
results
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Check if all dependencies are satisfied
|
|
69
|
+
# @return [Boolean] true if all dependencies are available
|
|
70
|
+
def all_satisfied?
|
|
71
|
+
results = check_all
|
|
72
|
+
results.all? { |tool, status| status[:available] || status[:optional] }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Print dependency status report
|
|
76
|
+
def print_report
|
|
77
|
+
results = check_all
|
|
78
|
+
|
|
79
|
+
puts "\n" + "=" * 60
|
|
80
|
+
puts "Dependency Check"
|
|
81
|
+
puts "=" * 60
|
|
82
|
+
|
|
83
|
+
results.each do |tool, status|
|
|
84
|
+
if status[:optional] && !status[:available]
|
|
85
|
+
icon = "⚪"
|
|
86
|
+
optional_text = " (Optional for #{Platform.current})"
|
|
87
|
+
else
|
|
88
|
+
icon = status[:available] ? "✅" : "❌"
|
|
89
|
+
optional_text = ""
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
puts "\n#{icon} #{tool.to_s.upcase}#{optional_text}"
|
|
93
|
+
|
|
94
|
+
if status[:available]
|
|
95
|
+
if tool == :gimp && status[:version]
|
|
96
|
+
version_str = "GIMP #{status[:version][:full]}"
|
|
97
|
+
puts " Found: #{status[:path]}"
|
|
98
|
+
puts " Version: #{version_str}"
|
|
99
|
+
else
|
|
100
|
+
puts " Found: #{status[:path] || status[:version]}"
|
|
101
|
+
end
|
|
102
|
+
else
|
|
103
|
+
if status[:optional]
|
|
104
|
+
puts " Status: NOT FOUND (not required for this platform)"
|
|
105
|
+
else
|
|
106
|
+
puts " Status: NOT FOUND"
|
|
107
|
+
puts " Install: #{status[:install_cmd]}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
puts "\n" + "=" * 60 + "\n"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Get the found GIMP executable path
|
|
116
|
+
attr_reader :gimp_path
|
|
117
|
+
|
|
118
|
+
# Get the detected GIMP version info
|
|
119
|
+
attr_reader :gimp_version
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def check_tool(tool, config)
|
|
124
|
+
# Check if this tool is optional for current platform
|
|
125
|
+
optional_platforms = config[:optional_for] || []
|
|
126
|
+
is_optional = optional_platforms.include?(Platform.current)
|
|
127
|
+
|
|
128
|
+
stdout, stderr, status = Open3.capture3(config[:command])
|
|
129
|
+
output = stdout + stderr
|
|
130
|
+
|
|
131
|
+
if status.success? && output.match?(config[:pattern])
|
|
132
|
+
version = extract_version(output)
|
|
133
|
+
{
|
|
134
|
+
available: true,
|
|
135
|
+
version: version,
|
|
136
|
+
install_cmd: nil,
|
|
137
|
+
optional: is_optional
|
|
138
|
+
}
|
|
139
|
+
else
|
|
140
|
+
{
|
|
141
|
+
available: false,
|
|
142
|
+
version: nil,
|
|
143
|
+
install_cmd: config[:install][Platform.current],
|
|
144
|
+
optional: is_optional
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
rescue StandardError => e
|
|
148
|
+
puts "DEBUG: Error checking #{tool}: #{e.message}" if @verbose
|
|
149
|
+
{
|
|
150
|
+
available: false,
|
|
151
|
+
version: nil,
|
|
152
|
+
install_cmd: config[:install][Platform.current],
|
|
153
|
+
optional: is_optional
|
|
154
|
+
}
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def check_gimp
|
|
158
|
+
# Try default path first
|
|
159
|
+
default_path = Platform.default_gimp_path
|
|
160
|
+
if gimp_exists?(default_path)
|
|
161
|
+
@gimp_path = default_path
|
|
162
|
+
@gimp_version = Platform.get_gimp_version(default_path)
|
|
163
|
+
return gimp_status(true, default_path, @gimp_version)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Try alternative paths
|
|
167
|
+
Platform.alternative_gimp_paths.each do |path|
|
|
168
|
+
if gimp_exists?(path)
|
|
169
|
+
@gimp_path = path
|
|
170
|
+
@gimp_version = Platform.get_gimp_version(path)
|
|
171
|
+
return gimp_status(true, path, @gimp_version)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Not found
|
|
176
|
+
gimp_status(false, nil, nil)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def gimp_exists?(path)
|
|
180
|
+
return false if path.nil? || path.empty?
|
|
181
|
+
|
|
182
|
+
# Handle Flatpak GIMP
|
|
183
|
+
if path.start_with?('flatpak:')
|
|
184
|
+
flatpak_app = path.sub('flatpak:', '')
|
|
185
|
+
stdout, _stderr, status = Open3.capture3("flatpak list --app | grep #{flatpak_app}")
|
|
186
|
+
return status.success? && !stdout.strip.empty?
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
File.exist?(path)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def gimp_status(available, path, version)
|
|
193
|
+
status = {
|
|
194
|
+
available: available,
|
|
195
|
+
path: path,
|
|
196
|
+
install_cmd: gimp_install_command
|
|
197
|
+
}
|
|
198
|
+
status[:version] = version if version
|
|
199
|
+
status
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def gimp_install_command
|
|
203
|
+
case Platform.current
|
|
204
|
+
when :windows
|
|
205
|
+
'Download from https://www.gimp.org/downloads/ or use: choco install gimp'
|
|
206
|
+
when :linux
|
|
207
|
+
'sudo apt install gimp'
|
|
208
|
+
when :macos
|
|
209
|
+
'brew install gimp'
|
|
210
|
+
else
|
|
211
|
+
'Visit https://www.gimp.org/downloads/'
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def extract_version(output)
|
|
216
|
+
# Try to extract version number from output
|
|
217
|
+
if output =~ /version\s+(\d+\.\d+[\.\d]*)/i
|
|
218
|
+
$1
|
|
219
|
+
else
|
|
220
|
+
output.lines.first&.strip || 'Unknown'
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module RubySpriter
|
|
7
|
+
# GhostEdgeCleaner removes semi-transparent "ghost" pixels around sprite edges
|
|
8
|
+
# using multi-pass alpha channel cleanup while preserving anti-aliasing
|
|
9
|
+
class GhostEdgeCleaner
|
|
10
|
+
attr_reader :input_image, :output_image, :config
|
|
11
|
+
attr_reader :ghost_pixels_detected, :passes_performed, :processing_time
|
|
12
|
+
|
|
13
|
+
MAX_PASSES = 3 # Maximum cleanup passes to prevent infinite loops
|
|
14
|
+
|
|
15
|
+
def initialize(input_image, output_image, config)
|
|
16
|
+
@input_image = input_image
|
|
17
|
+
@output_image = output_image
|
|
18
|
+
@config = config
|
|
19
|
+
@ghost_pixels_detected = 0
|
|
20
|
+
@passes_performed = 0
|
|
21
|
+
@processing_time = 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Main processing method
|
|
25
|
+
def process
|
|
26
|
+
start_time = Time.now
|
|
27
|
+
|
|
28
|
+
# Copy input to output as starting point
|
|
29
|
+
FileUtils.cp(@input_image, @output_image)
|
|
30
|
+
|
|
31
|
+
# Detect ghost pixels before cleanup
|
|
32
|
+
@ghost_pixels_detected = detect_ghost_pixels
|
|
33
|
+
|
|
34
|
+
# Perform multi-pass cleanup if multi_pass enabled
|
|
35
|
+
if @config.multi_pass
|
|
36
|
+
@passes_performed = multi_pass_cleanup
|
|
37
|
+
else
|
|
38
|
+
# Single pass cleanup
|
|
39
|
+
clean_edges
|
|
40
|
+
@passes_performed = 1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@processing_time = Time.now - start_time
|
|
44
|
+
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Detect pixels with alpha below threshold
|
|
49
|
+
def detect_ghost_pixels
|
|
50
|
+
threshold = @config.ghost_threshold
|
|
51
|
+
|
|
52
|
+
# Use ImageMagick to count pixels with alpha below threshold
|
|
53
|
+
# Alpha values are 0-255, threshold is 0-255
|
|
54
|
+
threshold_fraction = threshold / 255.0
|
|
55
|
+
|
|
56
|
+
convert_cmd = Platform.imagemagick_convert_cmd
|
|
57
|
+
cmd = "#{convert_cmd} #{Utils::PathHelper.quote_path(@input_image)} " \
|
|
58
|
+
"-channel A " \
|
|
59
|
+
"-separate " \
|
|
60
|
+
"-threshold #{(threshold_fraction * 100).to_i}% " \
|
|
61
|
+
"-format '%[fx:w*h*(1-mean)]' " \
|
|
62
|
+
"info:"
|
|
63
|
+
|
|
64
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
65
|
+
|
|
66
|
+
if status.success?
|
|
67
|
+
stdout.strip.gsub("'", '').to_f.to_i
|
|
68
|
+
else
|
|
69
|
+
0
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Clean edges by removing low-alpha pixels
|
|
74
|
+
def clean_edges
|
|
75
|
+
threshold = @config.ghost_threshold
|
|
76
|
+
|
|
77
|
+
# Use ImageMagick to selectively remove pixels below alpha threshold
|
|
78
|
+
# This preserves RGB data and anti-aliasing for pixels above threshold
|
|
79
|
+
threshold_fraction = threshold / 255.0
|
|
80
|
+
|
|
81
|
+
# Use direct -fx operation on alpha channel with explicit RGB preservation
|
|
82
|
+
# Use png:color-type=6 to force RGBA output (handles grayscale inputs)
|
|
83
|
+
# Use 'u' to refer to current channel value in -channel context
|
|
84
|
+
convert_cmd = Platform.imagemagick_convert_cmd
|
|
85
|
+
cmd = "#{convert_cmd} #{Utils::PathHelper.quote_path(@output_image)} " \
|
|
86
|
+
"-define png:color-type=6 " \
|
|
87
|
+
"-alpha set " \
|
|
88
|
+
"-channel A " \
|
|
89
|
+
"-fx \"u < #{threshold_fraction} ? 0 : u\" " \
|
|
90
|
+
"+channel " \
|
|
91
|
+
"#{Utils::PathHelper.quote_path(@output_image)}"
|
|
92
|
+
|
|
93
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
94
|
+
|
|
95
|
+
unless status.success?
|
|
96
|
+
warn "Failed to clean edges: #{stderr}"
|
|
97
|
+
return false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
true
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Perform multiple cleanup passes
|
|
104
|
+
def multi_pass_cleanup
|
|
105
|
+
passes = 0
|
|
106
|
+
previous_ghost_count = @ghost_pixels_detected
|
|
107
|
+
|
|
108
|
+
MAX_PASSES.times do
|
|
109
|
+
passes += 1
|
|
110
|
+
|
|
111
|
+
# Perform cleanup pass
|
|
112
|
+
clean_edges
|
|
113
|
+
|
|
114
|
+
# Check if we still have ghost pixels
|
|
115
|
+
current_ghost_count = detect_ghost_pixels_from_file(@output_image)
|
|
116
|
+
|
|
117
|
+
# Stop if no improvement or no ghost pixels remain
|
|
118
|
+
break if current_ghost_count == 0
|
|
119
|
+
break if current_ghost_count >= previous_ghost_count
|
|
120
|
+
|
|
121
|
+
previous_ghost_count = current_ghost_count
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Update instance variable for reporting
|
|
125
|
+
@passes_performed = passes
|
|
126
|
+
|
|
127
|
+
passes
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Generate processing report
|
|
131
|
+
def report
|
|
132
|
+
{
|
|
133
|
+
ghost_pixels_detected: @ghost_pixels_detected,
|
|
134
|
+
threshold_used: @config.ghost_threshold,
|
|
135
|
+
passes_performed: @passes_performed,
|
|
136
|
+
processing_time: @processing_time.round(3),
|
|
137
|
+
max_passes: MAX_PASSES
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def detect_ghost_pixels_from_file(file_path)
|
|
144
|
+
threshold = @config.ghost_threshold
|
|
145
|
+
threshold_fraction = threshold / 255.0
|
|
146
|
+
|
|
147
|
+
convert_cmd = Platform.imagemagick_convert_cmd
|
|
148
|
+
cmd = "#{convert_cmd} #{Utils::PathHelper.quote_path(file_path)} " \
|
|
149
|
+
"-channel A " \
|
|
150
|
+
"-separate " \
|
|
151
|
+
"-threshold #{(threshold_fraction * 100).to_i}% " \
|
|
152
|
+
"-format '%[fx:w*h*(1-mean)]' " \
|
|
153
|
+
"info:"
|
|
154
|
+
|
|
155
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
156
|
+
|
|
157
|
+
if status.success?
|
|
158
|
+
stdout.strip.gsub("'", '').to_f.to_i
|
|
159
|
+
else
|
|
160
|
+
0
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|