ruby_spriter 0.6.6 → 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 +257 -0
- data/README.md +384 -33
- data/lib/ruby_spriter/batch_processor.rb +214 -0
- data/lib/ruby_spriter/cli.rb +355 -8
- data/lib/ruby_spriter/compression_manager.rb +101 -0
- data/lib/ruby_spriter/consolidator.rb +33 -0
- 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 +419 -9
- data/lib/ruby_spriter/version.rb +2 -2
- data/lib/ruby_spriter.rb +2 -0
- data/spec/ruby_spriter/batch_processor_spec.rb +200 -0
- data/spec/ruby_spriter/cli_spec.rb +387 -0
- data/spec/ruby_spriter/compression_manager_spec.rb +157 -0
- data/spec/ruby_spriter/consolidator_spec.rb +163 -0
- data/spec/ruby_spriter/platform_spec.rb +11 -1
- data/spec/ruby_spriter/processor_spec.rb +350 -0
- metadata +6 -2
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module RubySpriter
|
|
7
|
+
# Manages PNG compression with metadata preservation
|
|
8
|
+
class CompressionManager
|
|
9
|
+
# Compress PNG file using ImageMagick with maximum compression
|
|
10
|
+
# @param input_file [String] Source PNG file
|
|
11
|
+
# @param output_file [String] Destination PNG file
|
|
12
|
+
# @param debug [Boolean] Enable debug output
|
|
13
|
+
def self.compress(input_file, output_file, debug: false)
|
|
14
|
+
Utils::FileHelper.validate_readable!(input_file)
|
|
15
|
+
|
|
16
|
+
cmd = build_compression_command(input_file, output_file)
|
|
17
|
+
|
|
18
|
+
if debug
|
|
19
|
+
Utils::OutputFormatter.indent("DEBUG: Compression command: #{cmd}")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
23
|
+
|
|
24
|
+
unless status.success?
|
|
25
|
+
raise ProcessingError, "Failed to compress PNG: #{stderr}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
Utils::FileHelper.validate_exists!(output_file)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Compress PNG file while preserving embedded metadata
|
|
32
|
+
# @param input_file [String] Source PNG file
|
|
33
|
+
# @param output_file [String] Destination PNG file
|
|
34
|
+
# @param debug [Boolean] Enable debug output
|
|
35
|
+
def self.compress_with_metadata(input_file, output_file, debug: false)
|
|
36
|
+
# Read metadata before compression
|
|
37
|
+
metadata = MetadataManager.read(input_file)
|
|
38
|
+
|
|
39
|
+
# Compress the file
|
|
40
|
+
temp_file = output_file.gsub('.png', '_compress_temp.png')
|
|
41
|
+
compress(input_file, temp_file, debug: debug)
|
|
42
|
+
|
|
43
|
+
# Re-embed metadata if it existed
|
|
44
|
+
if metadata
|
|
45
|
+
MetadataManager.embed(
|
|
46
|
+
temp_file,
|
|
47
|
+
output_file,
|
|
48
|
+
columns: metadata[:columns],
|
|
49
|
+
rows: metadata[:rows],
|
|
50
|
+
frames: metadata[:frames],
|
|
51
|
+
debug: debug
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Clean up temp file
|
|
55
|
+
FileUtils.rm_f(temp_file) if File.exist?(temp_file)
|
|
56
|
+
else
|
|
57
|
+
# No metadata, just move temp to output
|
|
58
|
+
FileUtils.mv(temp_file, output_file)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get compression statistics
|
|
63
|
+
# @param original_file [String] Original file path
|
|
64
|
+
# @param compressed_file [String] Compressed file path
|
|
65
|
+
# @return [Hash] Statistics including sizes and reduction percentage
|
|
66
|
+
def self.compression_stats(original_file, compressed_file)
|
|
67
|
+
original_size = File.size(original_file)
|
|
68
|
+
compressed_size = File.size(compressed_file)
|
|
69
|
+
saved_bytes = original_size - compressed_size
|
|
70
|
+
reduction_percent = (saved_bytes.to_f / original_size * 100.0)
|
|
71
|
+
|
|
72
|
+
{
|
|
73
|
+
original_size: original_size,
|
|
74
|
+
compressed_size: compressed_size,
|
|
75
|
+
saved_bytes: saved_bytes,
|
|
76
|
+
reduction_percent: reduction_percent
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private_class_method def self.build_compression_command(input_file, output_file)
|
|
81
|
+
magick_cmd = Platform.imagemagick_convert_cmd
|
|
82
|
+
|
|
83
|
+
# Use maximum PNG compression settings:
|
|
84
|
+
# - compression-level=9: Maximum zlib compression
|
|
85
|
+
# - compression-filter=5: Paeth filter (best for most images)
|
|
86
|
+
# - compression-strategy=1: Filtered strategy
|
|
87
|
+
# - quality=95: High quality
|
|
88
|
+
# - strip: Remove all metadata (we'll re-add it later)
|
|
89
|
+
[
|
|
90
|
+
magick_cmd,
|
|
91
|
+
Utils::PathHelper.quote_path(input_file),
|
|
92
|
+
'-strip',
|
|
93
|
+
'-define', 'png:compression-level=9',
|
|
94
|
+
'-define', 'png:compression-filter=5',
|
|
95
|
+
'-define', 'png:compression-strategy=1',
|
|
96
|
+
'-quality', '95',
|
|
97
|
+
Utils::PathHelper.quote_path(output_file)
|
|
98
|
+
].join(' ')
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -58,6 +58,39 @@ module RubySpriter
|
|
|
58
58
|
}
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
# Find all PNG files with spritesheet metadata in a directory
|
|
62
|
+
# @param directory [String] Directory path to scan
|
|
63
|
+
# @return [Array<String>] Sorted array of spritesheet file paths
|
|
64
|
+
def find_spritesheets_in_directory(directory)
|
|
65
|
+
# Validate directory exists
|
|
66
|
+
unless File.directory?(directory)
|
|
67
|
+
raise ValidationError, "Directory not found: #{directory}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Find all PNG files
|
|
71
|
+
pattern = File.join(directory, '*.png')
|
|
72
|
+
png_files = Dir.glob(pattern)
|
|
73
|
+
|
|
74
|
+
# Filter to only files with metadata
|
|
75
|
+
spritesheets = png_files.select do |file|
|
|
76
|
+
metadata = MetadataManager.read(file)
|
|
77
|
+
!metadata.nil?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Validate we found at least one
|
|
81
|
+
if spritesheets.empty?
|
|
82
|
+
raise ValidationError, "No PNG files with spritesheet metadata found in directory: #{directory}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Validate we have at least 2
|
|
86
|
+
if spritesheets.length < 2
|
|
87
|
+
raise ValidationError, "Found only #{spritesheets.length} spritesheet, need at least 2 for consolidation"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Sort alphabetically by filename
|
|
91
|
+
spritesheets.sort
|
|
92
|
+
end
|
|
93
|
+
|
|
61
94
|
private
|
|
62
95
|
|
|
63
96
|
def validate_files!(files)
|
|
@@ -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
|