ruby_spriter 0.6.7 → 0.6.7.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/CHANGELOG.md +119 -0
- data/README.md +51 -3
- data/lib/ruby_spriter/batch_processor.rb +4 -2
- data/lib/ruby_spriter/cli.rb +1 -1
- data/lib/ruby_spriter/dependency_checker.rb +65 -15
- data/lib/ruby_spriter/gimp_processor.rb +395 -4
- data/lib/ruby_spriter/platform.rb +56 -1
- data/lib/ruby_spriter/processor.rb +7 -2
- data/lib/ruby_spriter/version.rb +1 -1
- data/spec/ruby_spriter/platform_spec.rb +11 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b62fa9c591b35279199986edc2cf288249c74d80d3479eba177293c53b1dca1
|
|
4
|
+
data.tar.gz: 71603609b398ec1ac7d0caac7c7194ef0d5e09170a4841e6e3f2f2c0cec65ff7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 634d8516173fcb9461af2b457003e7661728e0dfbc7ac72904b0d771831e5bb730ccba867fd03fdae86acbde1e525300ad88a367c29c118b5dbb6002332a2e9f
|
|
7
|
+
data.tar.gz: 5330230b6690fd1aaed69f62bd31e901bb5119ed8fa0a2c07a8e3a6b164b190c230809688d5b5142a254bc231a0fd9ac556ed2a97230f807f061c5be5ba4175c
|
data/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,125 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
+
## [0.6.7.1] - 2025-10-24
|
|
16
|
+
|
|
17
|
+
### 🐧 Linux Support Enhancement Release
|
|
18
|
+
|
|
19
|
+
#### Added
|
|
20
|
+
- **Linux GIMP 3.x Support**: Full support for GIMP 3.x on Linux via Flatpak
|
|
21
|
+
- Automatic detection of Flatpak GIMP installation (`flatpak:org.gimp.GIMP`)
|
|
22
|
+
- Automatic Xvfb integration for completely headless GIMP operation
|
|
23
|
+
- Virtual display provided by Xvfb eliminates display connection requirement
|
|
24
|
+
- Flatpak socket isolation (`--nosocket=x11 --nosocket=wayland`) prevents GUI from appearing
|
|
25
|
+
- Python-fu batch mode works correctly with `python-fu-eval` interpreter
|
|
26
|
+
- Background removal, scaling, and all GIMP features fully functional
|
|
27
|
+
- Perfect for desktop use (no GUI distraction) and server environments (CI/CD, Docker, SSH)
|
|
28
|
+
- **GIMP Version Detection**: Detect and report GIMP version (2.x or 3.x)
|
|
29
|
+
- `Platform.detect_gimp_version(version_output)` - Parse version from `--version` output
|
|
30
|
+
- `Platform.get_gimp_version(gimp_path)` - Get version from executable or Flatpak
|
|
31
|
+
- Works with both regular executables and Flatpak installations
|
|
32
|
+
- **Xvfb Dependency Checking**: Added Xvfb to dependency checker (Linux only)
|
|
33
|
+
- Marked as required on Linux, optional on Windows/macOS
|
|
34
|
+
- Provides clear installation instructions if missing
|
|
35
|
+
- Validates availability before GIMP operations
|
|
36
|
+
- **DependencyChecker Version Tracking**: Store and report detected GIMP version
|
|
37
|
+
- **Xvfb Integration**: Transparent Xvfb usage when GIMP Flatpak detected
|
|
38
|
+
- Command format: `xvfb-run --auto-servernum --server-args='-screen 0 1024x768x24' flatpak run --nosocket=x11 --nosocket=wayland org.gimp.GIMP --no-splash --quit --batch-interpreter=python-fu-eval`
|
|
39
|
+
- No user configuration required - works automatically
|
|
40
|
+
- Completely headless - no GUI windows appear on screen
|
|
41
|
+
|
|
42
|
+
#### Changed
|
|
43
|
+
- **Platform Detection**: Enhanced to detect Flatpak GIMP alongside traditional installations
|
|
44
|
+
- **GimpProcessor**: Updated to support both GIMP 2.x and 3.x APIs (version-aware)
|
|
45
|
+
- **Unix GIMP Execution**: Automatically uses Xvfb with socket isolation for Flatpak installations
|
|
46
|
+
- **Alternative GIMP Paths**: Added `flatpak:org.gimp.GIMP` to Linux search paths
|
|
47
|
+
- **Warning Filters**: Enhanced to filter Xvfb, Wayland, and Flatpak cosmetic warnings
|
|
48
|
+
- **DependencyChecker**: Now supports platform-specific optional dependencies
|
|
49
|
+
|
|
50
|
+
#### Technical Details
|
|
51
|
+
- **GIMP Flatpak Detection**: Uses `flatpak list --app | grep` to verify installation
|
|
52
|
+
- **Version Parsing**: Regex-based parsing of `GIMP version X.Y.Z` output
|
|
53
|
+
- **Python Interpreter**: Correct name is `python-fu-eval` (not `python-eval`)
|
|
54
|
+
- **Xvfb Flags**:
|
|
55
|
+
- `--auto-servernum` - Automatically finds free display number
|
|
56
|
+
- `--server-args='-screen 0 1024x768x24'` - Configures virtual display
|
|
57
|
+
- **Flatpak Socket Isolation**:
|
|
58
|
+
- `--nosocket=x11` - Prevents access to host X11 display socket
|
|
59
|
+
- `--nosocket=wayland` - Prevents access to host Wayland display socket
|
|
60
|
+
- Ensures GIMP runs only in Xvfb virtual display
|
|
61
|
+
- **Platform Module**: New methods for version detection and Flatpak handling
|
|
62
|
+
- **Warning Filtering**: Filters Gdk-WARNING, LibGimp-WARNING, Gimp-Core-WARNING, X11 socket messages
|
|
63
|
+
|
|
64
|
+
#### Installation Requirements (Linux)
|
|
65
|
+
```bash
|
|
66
|
+
# Ubuntu/Debian
|
|
67
|
+
sudo apt install flatpak xvfb -y
|
|
68
|
+
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
|
69
|
+
flatpak install flathub org.gimp.GIMP -y
|
|
70
|
+
|
|
71
|
+
# Fedora/RHEL
|
|
72
|
+
sudo dnf install flatpak xorg-x11-server-Xvfb -y
|
|
73
|
+
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
|
74
|
+
flatpak install flathub org.gimp.GIMP -y
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### Example Output
|
|
78
|
+
```bash
|
|
79
|
+
$ ruby_spriter --check-dependencies
|
|
80
|
+
============================================================
|
|
81
|
+
Dependency Check
|
|
82
|
+
============================================================
|
|
83
|
+
|
|
84
|
+
✅ FFMPEG
|
|
85
|
+
Found: 6.1.1
|
|
86
|
+
|
|
87
|
+
✅ FFPROBE
|
|
88
|
+
Found: 6.1.1
|
|
89
|
+
|
|
90
|
+
✅ IMAGEMAGICK
|
|
91
|
+
Found: Version: ImageMagick 6.9.12-98 Q16 x86_64
|
|
92
|
+
|
|
93
|
+
✅ XVFB
|
|
94
|
+
Found: Usage: xvfb-run [OPTION ...] COMMAND
|
|
95
|
+
|
|
96
|
+
✅ GIMP
|
|
97
|
+
Found: flatpak:org.gimp.GIMP
|
|
98
|
+
Version: GIMP 3.0.6
|
|
99
|
+
|
|
100
|
+
============================================================
|
|
101
|
+
|
|
102
|
+
$ ruby_spriter --image sprite.png --remove-bg
|
|
103
|
+
============================================================
|
|
104
|
+
GIMP Processing
|
|
105
|
+
============================================================
|
|
106
|
+
📝 Using GIMP via Xvfb (virtual display)
|
|
107
|
+
Removing background (fuzzy select)...
|
|
108
|
+
=== GIMP Messages ===
|
|
109
|
+
Loading image...
|
|
110
|
+
Image size: 1280x748
|
|
111
|
+
Added alpha channel
|
|
112
|
+
Sampling 4 corners...
|
|
113
|
+
Using FUZZY SELECT (contiguous regions only)
|
|
114
|
+
Corner 1 at (0, 0)
|
|
115
|
+
Corner 2 at (1279, 0)
|
|
116
|
+
Corner 3 at (0, 747)
|
|
117
|
+
Corner 4 at (1279, 747)
|
|
118
|
+
Selection complete
|
|
119
|
+
Growing selection by 1 pixels...
|
|
120
|
+
Selection grown
|
|
121
|
+
Removing background...
|
|
122
|
+
Background removed
|
|
123
|
+
Deselecting...
|
|
124
|
+
Exporting...
|
|
125
|
+
SUCCESS - Background removed!
|
|
126
|
+
====================
|
|
127
|
+
✅ Background Removal complete (142.15 KB)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Note**: No GIMP GUI window appears on screen - completely headless operation!
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
15
134
|
## [0.6.7] - 2025-10-24
|
|
16
135
|
|
|
17
136
|
### 🚀 Batch Processing, Compression, Directory Consolidation & Frame Extraction Release
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Ruby Spriter v0.6.7
|
|
1
|
+
# Ruby Spriter v0.6.7.1
|
|
2
2
|
|
|
3
3
|
[](https://www.ruby-lang.org/)
|
|
4
4
|
[](LICENSE)
|
|
@@ -68,6 +68,7 @@ A powerful cross-platform Ruby tool for creating high-quality spritesheets from
|
|
|
68
68
|
| **FFprobe** | Latest | Video analysis (included with FFmpeg) |
|
|
69
69
|
| **ImageMagick** | 7.x+ | Metadata and sharpening |
|
|
70
70
|
| **GIMP** | 3.x (or 2.10) | Scaling and background removal |
|
|
71
|
+
| **Xvfb** | Latest (Linux only) | Virtual display for headless GIMP |
|
|
71
72
|
|
|
72
73
|
### Ruby Version
|
|
73
74
|
- Ruby 2.7.0 or higher
|
|
@@ -90,6 +91,7 @@ Ruby Spriter requires these external tools for video and image processing:
|
|
|
90
91
|
| **FFmpeg** | Video frame extraction | Any recent version |
|
|
91
92
|
| **ImageMagick** | Image manipulation & metadata | 7.x or 6.9+ |
|
|
92
93
|
| **GIMP** | Advanced image processing | 3.x (or 2.10) |
|
|
94
|
+
| **Xvfb** | Virtual display (Linux only) | Any recent version |
|
|
93
95
|
|
|
94
96
|
#### Installing Prerequisites
|
|
95
97
|
|
|
@@ -158,9 +160,19 @@ brew install ffmpeg imagemagick gimp
|
|
|
158
160
|
sudo apt update && sudo apt install ruby-full -y
|
|
159
161
|
|
|
160
162
|
# Install Ruby Spriter dependencies
|
|
161
|
-
sudo apt install ffmpeg imagemagick
|
|
163
|
+
sudo apt install ffmpeg imagemagick -y
|
|
164
|
+
|
|
165
|
+
# Install GIMP 3.x via Flatpak (recommended)
|
|
166
|
+
sudo apt install flatpak -y
|
|
167
|
+
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
|
168
|
+
flatpak install flathub org.gimp.GIMP -y
|
|
169
|
+
|
|
170
|
+
# Install Xvfb for headless GIMP operation
|
|
171
|
+
sudo apt install xvfb -y
|
|
162
172
|
```
|
|
163
173
|
|
|
174
|
+
**Note for Linux Users**: GIMP 3.x requires a display connection. Ruby Spriter automatically uses Xvfb (X Virtual Framebuffer) with Flatpak socket isolation to provide a completely headless virtual display. No GIMP GUI windows will appear on your screen - perfect for both desktop use (no distractions) and server environments (CI/CD, Docker, SSH sessions).
|
|
175
|
+
|
|
164
176
|
**Linux (Fedora/RHEL)**
|
|
165
177
|
|
|
166
178
|
```bash
|
|
@@ -168,7 +180,15 @@ sudo apt install ffmpeg imagemagick gimp -y
|
|
|
168
180
|
sudo dnf install ruby -y
|
|
169
181
|
|
|
170
182
|
# Install Ruby Spriter dependencies
|
|
171
|
-
sudo dnf install ffmpeg imagemagick
|
|
183
|
+
sudo dnf install ffmpeg imagemagick -y
|
|
184
|
+
|
|
185
|
+
# Install GIMP 3.x via Flatpak (recommended)
|
|
186
|
+
sudo dnf install flatpak -y
|
|
187
|
+
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
|
188
|
+
flatpak install flathub org.gimp.GIMP -y
|
|
189
|
+
|
|
190
|
+
# Install Xvfb for headless GIMP operation
|
|
191
|
+
sudo dnf install xorg-x11-server-Xvfb -y
|
|
172
192
|
```
|
|
173
193
|
|
|
174
194
|
---
|
|
@@ -685,6 +705,34 @@ ruby_spriter --video input.mp4 --scale 50 --sharpen --debug
|
|
|
685
705
|
# - Processing timestamps
|
|
686
706
|
```
|
|
687
707
|
|
|
708
|
+
### Headless Linux Operation (v0.6.7.1+)
|
|
709
|
+
|
|
710
|
+
Ruby Spriter provides completely headless GIMP operation on Linux via Xvfb and Flatpak socket isolation:
|
|
711
|
+
|
|
712
|
+
```bash
|
|
713
|
+
# No GIMP GUI appears during processing
|
|
714
|
+
ruby_spriter --image sprite.png --remove-bg --scale 50
|
|
715
|
+
|
|
716
|
+
# Perfect for server environments
|
|
717
|
+
ruby_spriter --batch --dir "sprites/" --remove-bg --max-compress
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
**How it Works:**
|
|
721
|
+
- Automatically detects GIMP 3.x Flatpak installation
|
|
722
|
+
- Uses Xvfb (X Virtual Framebuffer) to provide virtual display
|
|
723
|
+
- Flatpak socket isolation (`--nosocket=x11 --nosocket=wayland`) prevents GUI from appearing
|
|
724
|
+
- No configuration required - works automatically
|
|
725
|
+
|
|
726
|
+
**Use Cases:**
|
|
727
|
+
- **Desktop**: No GUI distractions during batch processing
|
|
728
|
+
- **Server**: Headless automation on Ubuntu Server, CI/CD pipelines
|
|
729
|
+
- **Docker**: Run in containers without display server
|
|
730
|
+
- **SSH**: Process sprites remotely without X forwarding
|
|
731
|
+
|
|
732
|
+
**Requirements:**
|
|
733
|
+
- GIMP 3.x via Flatpak (`flatpak install flathub org.gimp.GIMP`)
|
|
734
|
+
- Xvfb (`sudo apt install xvfb` on Ubuntu/Debian)
|
|
735
|
+
|
|
688
736
|
---
|
|
689
737
|
|
|
690
738
|
## 🏗️ Architecture
|
|
@@ -152,16 +152,18 @@ module RubySpriter
|
|
|
152
152
|
end
|
|
153
153
|
|
|
154
154
|
def process_with_gimp(input_file, video_result)
|
|
155
|
-
# Get GIMP path from dependency checker
|
|
155
|
+
# Get GIMP path and version from dependency checker
|
|
156
156
|
checker = DependencyChecker.new(verbose: false)
|
|
157
157
|
results = checker.check_all
|
|
158
158
|
gimp_path = checker.gimp_path
|
|
159
|
+
gimp_version = checker.gimp_version
|
|
159
160
|
|
|
160
161
|
unless gimp_path
|
|
161
162
|
raise DependencyError, "GIMP not found but required for processing"
|
|
162
163
|
end
|
|
163
164
|
|
|
164
|
-
|
|
165
|
+
gimp_options = options.merge(gimp_version: gimp_version)
|
|
166
|
+
gimp_processor = GimpProcessor.new(gimp_path, gimp_options)
|
|
165
167
|
output_file = gimp_processor.process(input_file)
|
|
166
168
|
|
|
167
169
|
# Clean up intermediate file if different
|
data/lib/ruby_spriter/cli.rb
CHANGED
|
@@ -210,7 +210,7 @@ module RubySpriter
|
|
|
210
210
|
options[:remove_bg] = true
|
|
211
211
|
end
|
|
212
212
|
|
|
213
|
-
opts.on("-t", "--threshold VALUE", Float, "
|
|
213
|
+
opts.on("-t", "--threshold VALUE", Float, "Background color tolerance % (default: 15.0, range: 0-100)") do |t|
|
|
214
214
|
options[:bg_threshold] = t
|
|
215
215
|
end
|
|
216
216
|
|
|
@@ -32,12 +32,23 @@ module RubySpriter
|
|
|
32
32
|
linux: 'sudo apt install imagemagick',
|
|
33
33
|
macos: 'brew install imagemagick'
|
|
34
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
|
|
35
45
|
}
|
|
36
46
|
}.freeze
|
|
37
47
|
|
|
38
48
|
def initialize(verbose: false)
|
|
39
49
|
@verbose = verbose
|
|
40
50
|
@gimp_path = nil
|
|
51
|
+
@gimp_version = nil
|
|
41
52
|
end
|
|
42
53
|
|
|
43
54
|
# Check all dependencies
|
|
@@ -58,7 +69,7 @@ module RubySpriter
|
|
|
58
69
|
# @return [Boolean] true if all dependencies are available
|
|
59
70
|
def all_satisfied?
|
|
60
71
|
results = check_all
|
|
61
|
-
results.all? { |
|
|
72
|
+
results.all? { |tool, status| status[:available] || status[:optional] }
|
|
62
73
|
end
|
|
63
74
|
|
|
64
75
|
# Print dependency status report
|
|
@@ -70,14 +81,31 @@ module RubySpriter
|
|
|
70
81
|
puts "=" * 60
|
|
71
82
|
|
|
72
83
|
results.each do |tool, status|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
|
|
76
94
|
if status[:available]
|
|
77
|
-
|
|
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
|
|
78
102
|
else
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
81
109
|
end
|
|
82
110
|
end
|
|
83
111
|
|
|
@@ -87,9 +115,16 @@ module RubySpriter
|
|
|
87
115
|
# Get the found GIMP executable path
|
|
88
116
|
attr_reader :gimp_path
|
|
89
117
|
|
|
118
|
+
# Get the detected GIMP version info
|
|
119
|
+
attr_reader :gimp_version
|
|
120
|
+
|
|
90
121
|
private
|
|
91
122
|
|
|
92
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
|
+
|
|
93
128
|
stdout, stderr, status = Open3.capture3(config[:command])
|
|
94
129
|
output = stdout + stderr
|
|
95
130
|
|
|
@@ -98,13 +133,15 @@ module RubySpriter
|
|
|
98
133
|
{
|
|
99
134
|
available: true,
|
|
100
135
|
version: version,
|
|
101
|
-
install_cmd: nil
|
|
136
|
+
install_cmd: nil,
|
|
137
|
+
optional: is_optional
|
|
102
138
|
}
|
|
103
139
|
else
|
|
104
140
|
{
|
|
105
141
|
available: false,
|
|
106
142
|
version: nil,
|
|
107
|
-
install_cmd: config[:install][Platform.current]
|
|
143
|
+
install_cmd: config[:install][Platform.current],
|
|
144
|
+
optional: is_optional
|
|
108
145
|
}
|
|
109
146
|
end
|
|
110
147
|
rescue StandardError => e
|
|
@@ -112,7 +149,8 @@ module RubySpriter
|
|
|
112
149
|
{
|
|
113
150
|
available: false,
|
|
114
151
|
version: nil,
|
|
115
|
-
install_cmd: config[:install][Platform.current]
|
|
152
|
+
install_cmd: config[:install][Platform.current],
|
|
153
|
+
optional: is_optional
|
|
116
154
|
}
|
|
117
155
|
end
|
|
118
156
|
|
|
@@ -121,32 +159,44 @@ module RubySpriter
|
|
|
121
159
|
default_path = Platform.default_gimp_path
|
|
122
160
|
if gimp_exists?(default_path)
|
|
123
161
|
@gimp_path = default_path
|
|
124
|
-
|
|
162
|
+
@gimp_version = Platform.get_gimp_version(default_path)
|
|
163
|
+
return gimp_status(true, default_path, @gimp_version)
|
|
125
164
|
end
|
|
126
165
|
|
|
127
166
|
# Try alternative paths
|
|
128
167
|
Platform.alternative_gimp_paths.each do |path|
|
|
129
168
|
if gimp_exists?(path)
|
|
130
169
|
@gimp_path = path
|
|
131
|
-
|
|
170
|
+
@gimp_version = Platform.get_gimp_version(path)
|
|
171
|
+
return gimp_status(true, path, @gimp_version)
|
|
132
172
|
end
|
|
133
173
|
end
|
|
134
174
|
|
|
135
175
|
# Not found
|
|
136
|
-
gimp_status(false, nil)
|
|
176
|
+
gimp_status(false, nil, nil)
|
|
137
177
|
end
|
|
138
178
|
|
|
139
179
|
def gimp_exists?(path)
|
|
140
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
|
+
|
|
141
189
|
File.exist?(path)
|
|
142
190
|
end
|
|
143
191
|
|
|
144
|
-
def gimp_status(available, path)
|
|
145
|
-
{
|
|
192
|
+
def gimp_status(available, path, version)
|
|
193
|
+
status = {
|
|
146
194
|
available: available,
|
|
147
195
|
path: path,
|
|
148
196
|
install_cmd: gimp_install_command
|
|
149
197
|
}
|
|
198
|
+
status[:version] = version if version
|
|
199
|
+
status
|
|
150
200
|
end
|
|
151
201
|
|
|
152
202
|
def gimp_install_command
|
|
@@ -6,11 +6,12 @@ require 'tmpdir'
|
|
|
6
6
|
module RubySpriter
|
|
7
7
|
# Processes images with GIMP
|
|
8
8
|
class GimpProcessor
|
|
9
|
-
attr_reader :options, :gimp_path
|
|
9
|
+
attr_reader :options, :gimp_path, :gimp_version
|
|
10
10
|
|
|
11
11
|
def initialize(gimp_path, options = {})
|
|
12
12
|
@gimp_path = gimp_path
|
|
13
13
|
@options = options
|
|
14
|
+
@gimp_version = options[:gimp_version] || { major: 3, minor: 0 } # Default to GIMP 3
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
# Process image with GIMP operations
|
|
@@ -21,6 +22,11 @@ module RubySpriter
|
|
|
21
22
|
|
|
22
23
|
Utils::OutputFormatter.header("GIMP Processing")
|
|
23
24
|
|
|
25
|
+
# Inform about Xvfb usage on Linux
|
|
26
|
+
if Platform.linux? && gimp_path.start_with?('flatpak:')
|
|
27
|
+
Utils::OutputFormatter.note("Using GIMP via Xvfb (virtual display)")
|
|
28
|
+
end
|
|
29
|
+
|
|
24
30
|
# Inform user if automatic operation order optimization is applied
|
|
25
31
|
if options[:scale_percent] && options[:remove_bg] &&
|
|
26
32
|
options[:operation_order] == :scale_then_remove_bg
|
|
@@ -45,6 +51,18 @@ module RubySpriter
|
|
|
45
51
|
|
|
46
52
|
private
|
|
47
53
|
|
|
54
|
+
def gimp_major_version
|
|
55
|
+
@gimp_version[:major]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def gimp2?
|
|
59
|
+
gimp_major_version == 2
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def gimp3?
|
|
63
|
+
gimp_major_version == 3
|
|
64
|
+
end
|
|
65
|
+
|
|
48
66
|
def determine_operations
|
|
49
67
|
ops = []
|
|
50
68
|
|
|
@@ -101,6 +119,14 @@ module RubySpriter
|
|
|
101
119
|
end
|
|
102
120
|
|
|
103
121
|
def generate_scale_script(input_file, output_file, percent)
|
|
122
|
+
if gimp2?
|
|
123
|
+
generate_scale_script_gimp2(input_file, output_file, percent)
|
|
124
|
+
else
|
|
125
|
+
generate_scale_script_gimp3(input_file, output_file, percent)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def generate_scale_script_gimp3(input_file, output_file, percent)
|
|
104
130
|
input_path = Utils::PathHelper.normalize_for_python(input_file)
|
|
105
131
|
output_path = Utils::PathHelper.normalize_for_python(output_file)
|
|
106
132
|
interpolation = map_interpolation_method(options[:scale_interpolation] || 'nohalo')
|
|
@@ -216,7 +242,75 @@ module RubySpriter
|
|
|
216
242
|
PYTHON
|
|
217
243
|
end
|
|
218
244
|
|
|
245
|
+
def generate_scale_script_gimp2(input_file, output_file, percent)
|
|
246
|
+
input_path = Utils::PathHelper.normalize_for_python(input_file)
|
|
247
|
+
output_path = Utils::PathHelper.normalize_for_python(output_file)
|
|
248
|
+
interpolation = map_interpolation_method_gimp2(options[:scale_interpolation] || 'nohalo')
|
|
249
|
+
|
|
250
|
+
<<~PYTHON
|
|
251
|
+
from gimpfu import *
|
|
252
|
+
import sys
|
|
253
|
+
|
|
254
|
+
def scale_image():
|
|
255
|
+
try:
|
|
256
|
+
print "Loading image..."
|
|
257
|
+
img = pdb.gimp_file_load(r'#{input_path}', r'#{input_path}')
|
|
258
|
+
|
|
259
|
+
w = img.width
|
|
260
|
+
h = img.height
|
|
261
|
+
print "Image size: %dx%d" % (w, h)
|
|
262
|
+
|
|
263
|
+
if len(img.layers) == 0:
|
|
264
|
+
raise Exception("No layers found")
|
|
265
|
+
layer = img.layers[0]
|
|
266
|
+
|
|
267
|
+
# Calculate new dimensions
|
|
268
|
+
new_width = int(w * #{percent} / 100.0)
|
|
269
|
+
new_height = int(h * #{percent} / 100.0)
|
|
270
|
+
print "Scaling to: %dx%d" % (new_width, new_height)
|
|
271
|
+
print "Interpolation: #{interpolation}"
|
|
272
|
+
|
|
273
|
+
# Scale layer with interpolation
|
|
274
|
+
pdb.gimp_layer_scale(layer, new_width, new_height, False, #{interpolation})
|
|
275
|
+
print "Layer scaled with interpolation"
|
|
276
|
+
|
|
277
|
+
# Resize canvas to match layer
|
|
278
|
+
pdb.gimp_image_resize(img, new_width, new_height, 0, 0)
|
|
279
|
+
print "Canvas resized"
|
|
280
|
+
|
|
281
|
+
# Handle multiple layers while preserving alpha
|
|
282
|
+
if len(img.layers) > 1:
|
|
283
|
+
pdb.gimp_image_merge_visible_layers(img, EXPAND_AS_NECESSARY)
|
|
284
|
+
print "Multiple layers merged (alpha preserved)"
|
|
285
|
+
else:
|
|
286
|
+
print "Single layer - no merge needed, alpha preserved"
|
|
287
|
+
|
|
288
|
+
# Export with alpha channel intact
|
|
289
|
+
print "Exporting with alpha channel..."
|
|
290
|
+
pdb.file_png_save(img, img.layers[0], r'#{output_path}', r'#{output_path}',
|
|
291
|
+
0, 9, 0, 0, 0, 0, 0)
|
|
292
|
+
|
|
293
|
+
print "SUCCESS - Image scaled!"
|
|
294
|
+
|
|
295
|
+
except Exception as e:
|
|
296
|
+
print "ERROR: %s" % str(e)
|
|
297
|
+
import traceback
|
|
298
|
+
traceback.print_exc()
|
|
299
|
+
sys.exit(1)
|
|
300
|
+
|
|
301
|
+
scale_image()
|
|
302
|
+
PYTHON
|
|
303
|
+
end
|
|
304
|
+
|
|
219
305
|
def generate_remove_bg_script(input_file, output_file)
|
|
306
|
+
if gimp2?
|
|
307
|
+
generate_remove_bg_script_gimp2(input_file, output_file)
|
|
308
|
+
else
|
|
309
|
+
generate_remove_bg_script_gimp3(input_file, output_file)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def generate_remove_bg_script_gimp3(input_file, output_file)
|
|
220
314
|
input_path = Utils::PathHelper.normalize_for_python(input_file)
|
|
221
315
|
output_path = Utils::PathHelper.normalize_for_python(output_file)
|
|
222
316
|
|
|
@@ -328,6 +422,98 @@ module RubySpriter
|
|
|
328
422
|
PYTHON
|
|
329
423
|
end
|
|
330
424
|
|
|
425
|
+
def generate_remove_bg_script_gimp2(input_file, output_file)
|
|
426
|
+
input_path = Utils::PathHelper.normalize_for_python(input_file)
|
|
427
|
+
output_path = Utils::PathHelper.normalize_for_python(output_file)
|
|
428
|
+
|
|
429
|
+
use_fuzzy = options[:fuzzy_select]
|
|
430
|
+
grow = options[:grow_selection] || 1
|
|
431
|
+
feather = options[:bg_threshold] || 0.0
|
|
432
|
+
|
|
433
|
+
# Build selection method
|
|
434
|
+
if use_fuzzy
|
|
435
|
+
select_method = "CHANNEL_OP_REPLACE" # First corner
|
|
436
|
+
select_add = "CHANNEL_OP_ADD" # Additional corners
|
|
437
|
+
select_call = "pdb.gimp_image_select_contiguous_color(img, select_op, layer, x, y)"
|
|
438
|
+
else
|
|
439
|
+
select_method = "CHANNEL_OP_REPLACE"
|
|
440
|
+
select_add = "CHANNEL_OP_ADD"
|
|
441
|
+
select_call = "pdb.gimp_image_select_color(img, select_op, layer, color)"
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
<<~PYTHON
|
|
445
|
+
from gimpfu import *
|
|
446
|
+
import sys
|
|
447
|
+
|
|
448
|
+
def remove_background():
|
|
449
|
+
try:
|
|
450
|
+
print "Loading image..."
|
|
451
|
+
img = pdb.gimp_file_load(r'#{input_path}', r'#{input_path}')
|
|
452
|
+
|
|
453
|
+
w = img.width
|
|
454
|
+
h = img.height
|
|
455
|
+
print "Image size: %dx%d" % (w, h)
|
|
456
|
+
|
|
457
|
+
if len(img.layers) == 0:
|
|
458
|
+
raise Exception("No layers found")
|
|
459
|
+
layer = img.layers[0]
|
|
460
|
+
|
|
461
|
+
# Add alpha channel if needed
|
|
462
|
+
if not pdb.gimp_layer_has_alpha(layer):
|
|
463
|
+
pdb.gimp_layer_add_alpha(layer)
|
|
464
|
+
print "Added alpha channel"
|
|
465
|
+
|
|
466
|
+
# Sample all four corners
|
|
467
|
+
corners = [
|
|
468
|
+
(0, 0), # Top-left
|
|
469
|
+
(w-1, 0), # Top-right
|
|
470
|
+
(0, h-1), # Bottom-left
|
|
471
|
+
(w-1, h-1) # Bottom-right
|
|
472
|
+
]
|
|
473
|
+
|
|
474
|
+
print "Sampling %d corners..." % len(corners)
|
|
475
|
+
#{"print \"Using FUZZY SELECT (contiguous regions only)\"" if use_fuzzy}
|
|
476
|
+
#{"print \"Using GLOBAL COLOR SELECT (all matching pixels)\"" unless use_fuzzy}
|
|
477
|
+
|
|
478
|
+
for i, (x, y) in enumerate(corners):
|
|
479
|
+
print " Corner %d at (%d, %d)" % (i+1, x, y)
|
|
480
|
+
select_op = CHANNEL_OP_REPLACE if i == 0 else CHANNEL_OP_ADD
|
|
481
|
+
#{use_fuzzy ? "pdb.gimp_image_select_contiguous_color(img, select_op, layer, x, y)" : "color = pdb.gimp_image_get_pixel_color(img, layer, x, y)[1]\n pdb.gimp_image_select_color(img, select_op, layer, color)"}
|
|
482
|
+
|
|
483
|
+
print "Selection complete"
|
|
484
|
+
|
|
485
|
+
# Grow selection if configured
|
|
486
|
+
#{grow > 0 ? "print \"Growing selection by #{grow} pixels...\"\n pdb.gimp_selection_grow(img, #{grow})\n print \"Selection grown\"" : "# No selection growth"}
|
|
487
|
+
|
|
488
|
+
# Feather selection if configured
|
|
489
|
+
#{feather > 0 ? "print \"Feathering selection by #{feather} pixels...\"\n pdb.gimp_selection_feather(img, #{feather})\n print \"Selection feathered\"" : "# No feathering"}
|
|
490
|
+
|
|
491
|
+
# Delete selection (clear background)
|
|
492
|
+
print "Removing background..."
|
|
493
|
+
pdb.gimp_edit_clear(layer)
|
|
494
|
+
print "Background removed"
|
|
495
|
+
|
|
496
|
+
# Deselect
|
|
497
|
+
print "Deselecting..."
|
|
498
|
+
pdb.gimp_selection_none(img)
|
|
499
|
+
|
|
500
|
+
# Export
|
|
501
|
+
print "Exporting..."
|
|
502
|
+
pdb.file_png_save(img, layer, r'#{output_path}', r'#{output_path}',
|
|
503
|
+
0, 9, 0, 0, 0, 0, 0)
|
|
504
|
+
|
|
505
|
+
print "SUCCESS - Background removed!"
|
|
506
|
+
|
|
507
|
+
except Exception as e:
|
|
508
|
+
print "ERROR: %s" % str(e)
|
|
509
|
+
import traceback
|
|
510
|
+
traceback.print_exc()
|
|
511
|
+
sys.exit(1)
|
|
512
|
+
|
|
513
|
+
remove_background()
|
|
514
|
+
PYTHON
|
|
515
|
+
end
|
|
516
|
+
|
|
331
517
|
def generate_fuzzy_select_code
|
|
332
518
|
<<~PYTHON.chomp
|
|
333
519
|
# Fuzzy select (contiguous regions only)
|
|
@@ -468,7 +654,7 @@ module RubySpriter
|
|
|
468
654
|
@echo off
|
|
469
655
|
REM Suppress GEGL debug output (known GIMP 3.x batch mode issue)
|
|
470
656
|
set GEGL_DEBUG=
|
|
471
|
-
"#{gimp_path}" --quit --batch-interpreter=python-fu-eval -b "exec(open(r'#{script_file}').read())" > "#{log_file}" 2>&1
|
|
657
|
+
"#{gimp_path}" --no-splash --quit --batch-interpreter=python-fu-eval -b "exec(open(r'#{script_file}').read())" > "#{log_file}" 2>&1
|
|
472
658
|
exit /b %errorlevel%
|
|
473
659
|
BATCH
|
|
474
660
|
|
|
@@ -502,7 +688,25 @@ module RubySpriter
|
|
|
502
688
|
|
|
503
689
|
# Unix execution (Linux/macOS)
|
|
504
690
|
def execute_gimp_unix(script_file, log_file)
|
|
505
|
-
|
|
691
|
+
# Check if we're using Flatpak GIMP (needs xvfb-run)
|
|
692
|
+
use_xvfb = gimp_path.start_with?('flatpak:')
|
|
693
|
+
|
|
694
|
+
if gimp2?
|
|
695
|
+
# GIMP 2.x: Use gimp-console for batch processing
|
|
696
|
+
gimp_console_path = gimp_path.sub('/gimp', '/gimp-console')
|
|
697
|
+
cmd = "#{Utils::PathHelper.quote_path(gimp_console_path)} -i --no-splash --batch-interpreter python-fu-eval -b 'exec(open(\"#{script_file}\").read())' -b '(gimp-quit 0)' > #{Utils::PathHelper.quote_path(log_file)} 2>&1"
|
|
698
|
+
else
|
|
699
|
+
# GIMP 3.x command
|
|
700
|
+
if use_xvfb
|
|
701
|
+
# Flatpak GIMP needs xvfb-run to provide virtual display
|
|
702
|
+
# Use --nosocket options to prevent Flatpak from accessing host display
|
|
703
|
+
flatpak_app = gimp_path.sub('flatpak:', '')
|
|
704
|
+
cmd = "xvfb-run --auto-servernum --server-args='-screen 0 1024x768x24' flatpak run --nosocket=x11 --nosocket=wayland #{flatpak_app} --no-splash --quit --batch-interpreter=python-fu-eval -b \"exec(open(r'#{script_file}').read())\" > #{Utils::PathHelper.quote_path(log_file)} 2>&1"
|
|
705
|
+
else
|
|
706
|
+
# Regular GIMP 3.x installation
|
|
707
|
+
cmd = "#{Utils::PathHelper.quote_path(gimp_path)} --no-splash --quit --batch-interpreter=python-fu-eval -b \"exec(open(r'#{script_file}').read())\" > #{Utils::PathHelper.quote_path(log_file)} 2>&1"
|
|
708
|
+
end
|
|
709
|
+
end
|
|
506
710
|
|
|
507
711
|
if options[:debug]
|
|
508
712
|
Utils::OutputFormatter.indent("DEBUG: GIMP command: #{cmd}")
|
|
@@ -532,6 +736,16 @@ module RubySpriter
|
|
|
532
736
|
line.match?(/GeglBuffers leaked/) ||
|
|
533
737
|
line.match?(/EEEEeEeek!/) ||
|
|
534
738
|
line.match?(/batch command executed successfully/) ||
|
|
739
|
+
# Filter Linux/Wayland/Flatpak cosmetic warnings
|
|
740
|
+
line.match?(/Gdk-WARNING.*Failed to read portal settings/) ||
|
|
741
|
+
line.match?(/set device.*to mode: disabled/) ||
|
|
742
|
+
line.match?(/Gdk-WARNING.*Server is missing xdg_foreign support/) ||
|
|
743
|
+
line.match?(/gimp_widget_set_handle_on_mapped.*gdk_wayland_window_export_handle/) ||
|
|
744
|
+
line.match?(/It will not be possible to set windows in other processes/) ||
|
|
745
|
+
line.match?(/LibGimp-WARNING.*gimp_flush.*Broken pipe/) ||
|
|
746
|
+
line.match?(/Gimp-Core-WARNING.*gimp_finalize.*list of contexts not empty/) ||
|
|
747
|
+
line.match?(/stale context:/) ||
|
|
748
|
+
line.match?(/F: X11 socket.*does not exist in filesystem/) ||
|
|
535
749
|
line.strip.empty?
|
|
536
750
|
end
|
|
537
751
|
lines.join
|
|
@@ -598,7 +812,7 @@ module RubySpriter
|
|
|
598
812
|
end
|
|
599
813
|
end
|
|
600
814
|
|
|
601
|
-
# Map interpolation method names to GIMP interpolation type enum values
|
|
815
|
+
# Map interpolation method names to GIMP 3.x interpolation type enum values
|
|
602
816
|
def map_interpolation_method(method)
|
|
603
817
|
# GIMP 3.x GimpInterpolationType enum values
|
|
604
818
|
case method.to_s.downcase
|
|
@@ -617,6 +831,183 @@ module RubySpriter
|
|
|
617
831
|
end
|
|
618
832
|
end
|
|
619
833
|
|
|
834
|
+
# Map interpolation method names to GIMP 2.x interpolation type constants
|
|
835
|
+
def map_interpolation_method_gimp2(method)
|
|
836
|
+
# GIMP 2.x interpolation constants
|
|
837
|
+
case method.to_s.downcase
|
|
838
|
+
when 'none'
|
|
839
|
+
'INTERPOLATION_NONE'
|
|
840
|
+
when 'linear'
|
|
841
|
+
'INTERPOLATION_LINEAR'
|
|
842
|
+
when 'cubic'
|
|
843
|
+
'INTERPOLATION_CUBIC'
|
|
844
|
+
when 'nohalo'
|
|
845
|
+
'INTERPOLATION_NOHALO'
|
|
846
|
+
when 'lohalo'
|
|
847
|
+
'INTERPOLATION_LOHALO'
|
|
848
|
+
else
|
|
849
|
+
'INTERPOLATION_NOHALO' # Default to NoHalo for quality
|
|
850
|
+
end
|
|
851
|
+
end
|
|
852
|
+
|
|
853
|
+
# Remove background using ImageMagick (fallback for Linux)
|
|
854
|
+
# Uses edge detection and multiple techniques for better results
|
|
855
|
+
def remove_background_imagemagick(input_file, output_file)
|
|
856
|
+
magick_cmd = Platform.imagemagick_convert_cmd
|
|
857
|
+
identify_cmd = Platform.imagemagick_identify_cmd
|
|
858
|
+
|
|
859
|
+
# Get options
|
|
860
|
+
use_fuzzy = options[:fuzzy_select]
|
|
861
|
+
fuzz_percent = options[:bg_threshold] || 15.0
|
|
862
|
+
grow = options[:grow_selection] || 1
|
|
863
|
+
|
|
864
|
+
# Get image dimensions
|
|
865
|
+
stdout, _stderr, status = Open3.capture3("#{identify_cmd} -format '%w %h' #{Utils::PathHelper.quote_path(input_file)}")
|
|
866
|
+
unless status.success?
|
|
867
|
+
raise ProcessingError, "Could not get image dimensions"
|
|
868
|
+
end
|
|
869
|
+
width, height = stdout.strip.split.map(&:to_i)
|
|
870
|
+
|
|
871
|
+
# Sample more points around the border (not just corners)
|
|
872
|
+
sample_points = [
|
|
873
|
+
# Corners
|
|
874
|
+
[0, 0], [width - 1, 0], [0, height - 1], [width - 1, height - 1],
|
|
875
|
+
# Mid-edges
|
|
876
|
+
[width / 2, 0], [width / 2, height - 1],
|
|
877
|
+
[0, height / 2], [width - 1, height / 2]
|
|
878
|
+
]
|
|
879
|
+
|
|
880
|
+
# Get colors from all sample points
|
|
881
|
+
sampled_colors = []
|
|
882
|
+
sample_points.each do |x, y|
|
|
883
|
+
color_stdout, _stderr, status = Open3.capture3("#{identify_cmd} -format '%[pixel:p{#{x},#{y}}]' #{Utils::PathHelper.quote_path(input_file)}")
|
|
884
|
+
sampled_colors << color_stdout.strip if status.success? && !color_stdout.strip.empty?
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
# Find the most common color (likely the background)
|
|
888
|
+
bg_color = sampled_colors.group_by(&:itself).max_by { |_, v| v.size }.first
|
|
889
|
+
|
|
890
|
+
if options[:debug]
|
|
891
|
+
Utils::OutputFormatter.indent("DEBUG: Image size: #{width}x#{height}")
|
|
892
|
+
Utils::OutputFormatter.indent("DEBUG: Sampled #{sampled_colors.size} border colors")
|
|
893
|
+
Utils::OutputFormatter.indent("DEBUG: Unique colors: #{sampled_colors.uniq.size}")
|
|
894
|
+
Utils::OutputFormatter.indent("DEBUG: Using background color: #{bg_color}")
|
|
895
|
+
Utils::OutputFormatter.indent("DEBUG: Fuzz: #{fuzz_percent}%")
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
# Create a multi-pass approach for better results
|
|
899
|
+
# Pass 1: Use floodfill from edges with fuzz tolerance
|
|
900
|
+
temp_file1 = File.join(Dir.tmpdir, "bg_removal_pass1_#{Time.now.to_i}.png")
|
|
901
|
+
|
|
902
|
+
draw_commands = sample_points.map { |x, y| "'color #{x},#{y} floodfill'" }.join(' -draw ')
|
|
903
|
+
|
|
904
|
+
cmd1 = "#{magick_cmd} #{Utils::PathHelper.quote_path(input_file)} -alpha set -fuzz #{fuzz_percent}% -fill none -draw #{draw_commands} #{temp_file1}"
|
|
905
|
+
|
|
906
|
+
if options[:debug]
|
|
907
|
+
Utils::OutputFormatter.indent("DEBUG: Pass 1 - Floodfill from #{sample_points.size} points")
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
stdout, stderr, status = Open3.capture3(cmd1)
|
|
911
|
+
unless status.success?
|
|
912
|
+
File.delete(temp_file1) if File.exist?(temp_file1)
|
|
913
|
+
raise ProcessingError, "Background removal pass 1 failed: #{stderr}"
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
# Pass 2: Remove the detected background color globally with fuzz
|
|
917
|
+
temp_file2 = File.join(Dir.tmpdir, "bg_removal_pass2_#{Time.now.to_i}.png")
|
|
918
|
+
|
|
919
|
+
cmd2 = "#{magick_cmd} #{temp_file1} -fuzz #{fuzz_percent}% -transparent '#{bg_color}' #{temp_file2}"
|
|
920
|
+
|
|
921
|
+
if options[:debug]
|
|
922
|
+
Utils::OutputFormatter.indent("DEBUG: Pass 2 - Remove color #{bg_color} globally")
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
stdout, stderr, status = Open3.capture3(cmd2)
|
|
926
|
+
unless status.success?
|
|
927
|
+
File.delete(temp_file1) if File.exist?(temp_file1)
|
|
928
|
+
File.delete(temp_file2) if File.exist?(temp_file2)
|
|
929
|
+
raise ProcessingError, "Background removal pass 2 failed: #{stderr}"
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
# Pass 3: Minimal cleanup - preserve quality
|
|
933
|
+
cmd3_parts = [
|
|
934
|
+
magick_cmd,
|
|
935
|
+
temp_file2
|
|
936
|
+
]
|
|
937
|
+
|
|
938
|
+
# Only clean up the alpha channel, don't touch the RGB data
|
|
939
|
+
# This preserves sprite quality while cleaning edges
|
|
940
|
+
cmd3_parts += [
|
|
941
|
+
'-channel', 'A',
|
|
942
|
+
# Very gentle cleanup - only remove nearly-transparent pixels
|
|
943
|
+
'-threshold', '5%', # Anything less than 5% alpha becomes fully transparent
|
|
944
|
+
'+channel'
|
|
945
|
+
]
|
|
946
|
+
|
|
947
|
+
# Optionally grow the transparent areas
|
|
948
|
+
if grow > 0
|
|
949
|
+
cmd3_parts += ['-morphology', 'Dilate', "Disk:#{grow}"]
|
|
950
|
+
end
|
|
951
|
+
|
|
952
|
+
cmd3_parts << Utils::PathHelper.quote_path(output_file)
|
|
953
|
+
cmd3 = cmd3_parts.join(' ')
|
|
954
|
+
|
|
955
|
+
if options[:debug]
|
|
956
|
+
Utils::OutputFormatter.indent("DEBUG: Pass 3 - Minimal alpha cleanup (quality-preserving)")
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
stdout, stderr, status = Open3.capture3(cmd3)
|
|
960
|
+
|
|
961
|
+
# Cleanup temp files
|
|
962
|
+
File.delete(temp_file1) if File.exist?(temp_file1)
|
|
963
|
+
File.delete(temp_file2) if File.exist?(temp_file2)
|
|
964
|
+
|
|
965
|
+
unless status.success?
|
|
966
|
+
raise ProcessingError, "Background removal pass 3 failed: #{stderr}"
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
Utils::FileHelper.validate_exists!(output_file)
|
|
970
|
+
size = Utils::FileHelper.format_size(File.size(output_file))
|
|
971
|
+
Utils::OutputFormatter.success("Background removal complete (#{size})\n")
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
# Scale image using ImageMagick (fallback for GIMP 2.x)
|
|
975
|
+
def scale_with_imagemagick(input_file, output_file, percent)
|
|
976
|
+
magick_cmd = Platform.imagemagick_convert_cmd
|
|
977
|
+
|
|
978
|
+
# Map interpolation to ImageMagick filters
|
|
979
|
+
interpolation = options[:scale_interpolation] || 'nohalo'
|
|
980
|
+
filter = case interpolation.to_s.downcase
|
|
981
|
+
when 'none' then 'Point'
|
|
982
|
+
when 'linear' then 'Triangle'
|
|
983
|
+
when 'cubic' then 'Catrom'
|
|
984
|
+
when 'nohalo', 'lohalo' then 'Lanczos' # Best available quality
|
|
985
|
+
else 'Lanczos'
|
|
986
|
+
end
|
|
987
|
+
|
|
988
|
+
cmd = [
|
|
989
|
+
magick_cmd,
|
|
990
|
+
Utils::PathHelper.quote_path(input_file),
|
|
991
|
+
'-filter', filter,
|
|
992
|
+
'-resize', "#{percent}%",
|
|
993
|
+
Utils::PathHelper.quote_path(output_file)
|
|
994
|
+
].join(' ')
|
|
995
|
+
|
|
996
|
+
if options[:debug]
|
|
997
|
+
Utils::OutputFormatter.indent("DEBUG: ImageMagick command: #{cmd}")
|
|
998
|
+
end
|
|
999
|
+
|
|
1000
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
1001
|
+
|
|
1002
|
+
unless status.success?
|
|
1003
|
+
raise ProcessingError, "ImageMagick scaling failed: #{stderr}"
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
Utils::FileHelper.validate_exists!(output_file)
|
|
1007
|
+
size = Utils::FileHelper.format_size(File.size(output_file))
|
|
1008
|
+
Utils::OutputFormatter.success("Scale complete (#{size})\n")
|
|
1009
|
+
end
|
|
1010
|
+
|
|
620
1011
|
# Apply unsharp mask using ImageMagick
|
|
621
1012
|
def apply_sharpen_imagemagick(input_file)
|
|
622
1013
|
radius = options[:sharpen_radius] || 2.0
|
|
@@ -29,10 +29,13 @@ module RubySpriter
|
|
|
29
29
|
'/usr/bin/gimp',
|
|
30
30
|
'/usr/local/bin/gimp',
|
|
31
31
|
'/snap/bin/gimp',
|
|
32
|
-
'/opt/gimp/bin/gimp'
|
|
32
|
+
'/opt/gimp/bin/gimp',
|
|
33
|
+
'flatpak:org.gimp.GIMP' # Flatpak GIMP
|
|
33
34
|
].freeze,
|
|
34
35
|
macos: [
|
|
35
36
|
'/Applications/GIMP.app/Contents/MacOS/gimp',
|
|
37
|
+
'/Applications/GIMP-2.99.app/Contents/MacOS/gimp', # GIMP 3.x dev
|
|
38
|
+
'/Applications/GIMP-3.0.app/Contents/MacOS/gimp', # GIMP 3.x release
|
|
36
39
|
'/Applications/GIMP-2.10.app/Contents/MacOS/gimp'
|
|
37
40
|
].freeze
|
|
38
41
|
}.freeze
|
|
@@ -77,6 +80,58 @@ module RubySpriter
|
|
|
77
80
|
def imagemagick_identify_cmd
|
|
78
81
|
windows? ? 'magick identify' : 'identify'
|
|
79
82
|
end
|
|
83
|
+
|
|
84
|
+
# Detect GIMP version from version string output
|
|
85
|
+
# @param version_output [String] Output from gimp --version command
|
|
86
|
+
# @return [Hash] Version information with :major, :minor, :patch, :full keys, or nil if parse fails
|
|
87
|
+
def detect_gimp_version(version_output)
|
|
88
|
+
return nil if version_output.nil? || version_output.empty?
|
|
89
|
+
|
|
90
|
+
# Match version pattern: "version X.Y.Z" or "version X.Y"
|
|
91
|
+
match = version_output.match(/version\s+(\d+)\.(\d+)(?:\.(\d+))?/i)
|
|
92
|
+
return nil unless match
|
|
93
|
+
|
|
94
|
+
{
|
|
95
|
+
major: match[1].to_i,
|
|
96
|
+
minor: match[2].to_i,
|
|
97
|
+
patch: match[3]&.to_i || 0,
|
|
98
|
+
full: match[1..3].compact.join('.')
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Get GIMP version from executable path
|
|
103
|
+
# @param gimp_path [String] Path to GIMP executable or flatpak:app.id
|
|
104
|
+
# @return [Hash] Version information, or nil if detection fails
|
|
105
|
+
def get_gimp_version(gimp_path)
|
|
106
|
+
return nil if gimp_path.nil? || gimp_path.empty?
|
|
107
|
+
|
|
108
|
+
require 'open3'
|
|
109
|
+
|
|
110
|
+
# Handle Flatpak GIMP
|
|
111
|
+
if gimp_path.start_with?('flatpak:')
|
|
112
|
+
flatpak_app = gimp_path.sub('flatpak:', '')
|
|
113
|
+
stdout, stderr, status = Open3.capture3("flatpak run #{flatpak_app} --version")
|
|
114
|
+
return nil unless status.success?
|
|
115
|
+
return detect_gimp_version(stdout + stderr)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
return nil unless File.exist?(gimp_path)
|
|
119
|
+
|
|
120
|
+
stdout, stderr, status = Open3.capture3("#{quote_path_simple(gimp_path)} --version")
|
|
121
|
+
return nil unless status.success?
|
|
122
|
+
|
|
123
|
+
detect_gimp_version(stdout + stderr)
|
|
124
|
+
rescue StandardError
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
# Simple path quoting helper for Platform module
|
|
131
|
+
def quote_path_simple(path)
|
|
132
|
+
return path unless path.include?(' ')
|
|
133
|
+
windows? ? "\"#{path}\"" : "'#{path}'"
|
|
134
|
+
end
|
|
80
135
|
end
|
|
81
136
|
end
|
|
82
137
|
end
|
|
@@ -25,6 +25,7 @@ module RubySpriter
|
|
|
25
25
|
def initialize(options = {})
|
|
26
26
|
@options = default_options.merge(options)
|
|
27
27
|
@gimp_path = nil
|
|
28
|
+
@gimp_version = nil
|
|
28
29
|
validate_numeric_options!
|
|
29
30
|
validate_split_option!
|
|
30
31
|
validate_extract_option!
|
|
@@ -340,7 +341,10 @@ module RubySpriter
|
|
|
340
341
|
raise DependencyError, "Missing required dependencies: #{missing.join(', ')}"
|
|
341
342
|
end
|
|
342
343
|
|
|
343
|
-
|
|
344
|
+
if results[:gimp][:available]
|
|
345
|
+
@gimp_path = checker.gimp_path
|
|
346
|
+
@gimp_version = checker.gimp_version
|
|
347
|
+
end
|
|
344
348
|
|
|
345
349
|
if options[:debug]
|
|
346
350
|
checker.print_report
|
|
@@ -703,7 +707,8 @@ module RubySpriter
|
|
|
703
707
|
end
|
|
704
708
|
|
|
705
709
|
def process_with_gimp(input_file)
|
|
706
|
-
|
|
710
|
+
gimp_options = options.merge(gimp_version: @gimp_version)
|
|
711
|
+
gimp_processor = GimpProcessor.new(@gimp_path, gimp_options)
|
|
707
712
|
gimp_processor.process(input_file)
|
|
708
713
|
end
|
|
709
714
|
|
data/lib/ruby_spriter/version.rb
CHANGED
|
@@ -71,7 +71,7 @@ RSpec.describe RubySpriter::Platform do
|
|
|
71
71
|
describe '.imagemagick_identify_cmd' do
|
|
72
72
|
it 'returns appropriate command for platform' do
|
|
73
73
|
cmd = described_class.imagemagick_identify_cmd
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
if described_class.windows?
|
|
76
76
|
expect(cmd).to eq('magick identify')
|
|
77
77
|
else
|
|
@@ -79,4 +79,14 @@ RSpec.describe RubySpriter::Platform do
|
|
|
79
79
|
end
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
|
+
|
|
83
|
+
describe '.detect_gimp_version' do
|
|
84
|
+
it 'detects GIMP 3.x version from command output' do
|
|
85
|
+
gimp3_output = "GNU Image Manipulation Program version 3.0.0"
|
|
86
|
+
version = described_class.detect_gimp_version(gimp3_output)
|
|
87
|
+
expect(version[:major]).to eq(3)
|
|
88
|
+
expect(version[:minor]).to eq(0)
|
|
89
|
+
expect(version[:full]).to eq('3.0.0')
|
|
90
|
+
end
|
|
91
|
+
end
|
|
82
92
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_spriter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.7
|
|
4
|
+
version: 0.6.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- scooter-indie
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-10-
|
|
11
|
+
date: 2025-10-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
Ruby Spriter is a cross-platform tool for creating spritesheets from video files
|