ruby_spriter 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +217 -0
- data/Gemfile +17 -0
- data/LICENSE +21 -0
- data/README.md +561 -0
- data/bin/ruby_spriter +20 -0
- data/lib/ruby_spriter/cli.rb +249 -0
- data/lib/ruby_spriter/consolidator.rb +146 -0
- data/lib/ruby_spriter/dependency_checker.rb +174 -0
- data/lib/ruby_spriter/gimp_processor.rb +664 -0
- data/lib/ruby_spriter/metadata_manager.rb +116 -0
- data/lib/ruby_spriter/platform.rb +82 -0
- data/lib/ruby_spriter/processor.rb +251 -0
- data/lib/ruby_spriter/utils/file_helper.rb +57 -0
- data/lib/ruby_spriter/utils/output_formatter.rb +65 -0
- data/lib/ruby_spriter/utils/path_helper.rb +59 -0
- data/lib/ruby_spriter/version.rb +7 -0
- data/lib/ruby_spriter/video_processor.rb +139 -0
- data/lib/ruby_spriter.rb +31 -0
- data/ruby_spriter.gemspec +42 -0
- data/spec/fixtures/image_without_metadata.png +0 -0
- data/spec/fixtures/spritesheet_4x2.png +0 -0
- data/spec/fixtures/spritesheet_4x4.png +0 -0
- data/spec/fixtures/spritesheet_6x2.png +0 -0
- data/spec/fixtures/spritesheet_with_metadata.png +0 -0
- data/spec/fixtures/test_video.mp4 +0 -0
- data/spec/ruby_spriter/cli_spec.rb +1142 -0
- data/spec/ruby_spriter/consolidator_spec.rb +375 -0
- data/spec/ruby_spriter/dependency_checker_spec.rb +0 -0
- data/spec/ruby_spriter/gimp_processor_spec.rb +425 -0
- data/spec/ruby_spriter/metadata_manager_spec.rb +0 -0
- data/spec/ruby_spriter/platform_spec.rb +82 -0
- data/spec/ruby_spriter/processor_spec.rb +0 -0
- data/spec/ruby_spriter/utils/file_helper_spec.rb +71 -0
- data/spec/ruby_spriter/utils/output_formatter_spec.rb +0 -0
- data/spec/ruby_spriter/utils/path_helper_spec.rb +78 -0
- data/spec/ruby_spriter/video_processor_spec.rb +0 -0
- data/spec/spec_helper.rb +41 -0
- metadata +88 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
|
|
5
|
+
module RubySpriter
|
|
6
|
+
# Command-line interface
|
|
7
|
+
class CLI
|
|
8
|
+
PRESETS = {
|
|
9
|
+
thumbnail: { columns: 3, frame_count: 9, max_width: 240 },
|
|
10
|
+
preview: { columns: 4, frame_count: 16, max_width: 400 },
|
|
11
|
+
detailed: { columns: 10, frame_count: 50, max_width: 320 },
|
|
12
|
+
contact: { columns: 8, frame_count: 64, max_width: 160 }
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
def self.start(args)
|
|
16
|
+
new.parse_and_run(args)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def parse_and_run(args)
|
|
20
|
+
options = {}
|
|
21
|
+
parser = build_option_parser(options)
|
|
22
|
+
|
|
23
|
+
parser.parse!(args)
|
|
24
|
+
|
|
25
|
+
# Handle special commands that don't need full processing
|
|
26
|
+
if options[:check_dependencies]
|
|
27
|
+
checker = DependencyChecker.new(verbose: true)
|
|
28
|
+
checker.print_report
|
|
29
|
+
exit(checker.all_satisfied? ? 0 : 1)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Run processor
|
|
33
|
+
processor = Processor.new(options)
|
|
34
|
+
processor.run
|
|
35
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
|
36
|
+
puts "Error: #{e.message}"
|
|
37
|
+
puts "\nUse --help for usage information"
|
|
38
|
+
exit 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def build_option_parser(options)
|
|
44
|
+
OptionParser.new do |opts|
|
|
45
|
+
opts.banner = "Usage: ruby_spriter [options]"
|
|
46
|
+
|
|
47
|
+
add_header(opts)
|
|
48
|
+
add_input_options(opts, options)
|
|
49
|
+
add_spritesheet_options(opts, options)
|
|
50
|
+
add_gimp_options(opts, options)
|
|
51
|
+
add_bg_removal_options(opts, options)
|
|
52
|
+
add_consolidation_options(opts, options)
|
|
53
|
+
add_preset_options(opts, options)
|
|
54
|
+
add_other_options(opts, options)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def add_header(opts)
|
|
59
|
+
opts.separator ""
|
|
60
|
+
opts.separator "Ruby Spriter v#{VERSION} - MP4 to Spritesheet + GIMP Processing"
|
|
61
|
+
opts.separator "Platform: #{Platform.current.to_s.capitalize}"
|
|
62
|
+
opts.separator ""
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def add_input_options(opts, options)
|
|
66
|
+
opts.separator "Input Options:"
|
|
67
|
+
|
|
68
|
+
opts.on("-v", "--video FILE", "Input video file (MP4)") do |v|
|
|
69
|
+
options[:video] = v
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
opts.on("-i", "--image FILE", "Input image file (PNG) for direct processing") do |i|
|
|
73
|
+
options[:image] = i
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
opts.on("--consolidate FILES", Array, "Consolidate multiple spritesheets (comma-separated)") do |c|
|
|
77
|
+
options[:consolidate] = c
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
opts.on("--verify FILE", "Verify spritesheet metadata") do |v|
|
|
81
|
+
options[:verify] = v
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
opts.separator ""
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def add_spritesheet_options(opts, options)
|
|
88
|
+
opts.separator "Spritesheet Options:"
|
|
89
|
+
|
|
90
|
+
opts.on("-o", "--output FILE", "Output file path") do |o|
|
|
91
|
+
options[:output] = o
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
opts.on("-f", "--frames COUNT", Integer, "Number of frames to extract (default: 16)") do |f|
|
|
95
|
+
options[:frame_count] = f
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
opts.on("-c", "--columns COUNT", Integer, "Grid columns (default: 4)") do |c|
|
|
99
|
+
options[:columns] = c
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
opts.on("-w", "--width PIXELS", Integer, "Max frame width (default: 320)") do |w|
|
|
103
|
+
options[:max_width] = w
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
opts.on("-b", "--background COLOR", "Tile background: black, white (default: black)") do |b|
|
|
107
|
+
options[:bg_color] = b
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
opts.separator ""
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def add_gimp_options(opts, options)
|
|
114
|
+
opts.separator "GIMP Processing Options:"
|
|
115
|
+
|
|
116
|
+
opts.on("-s", "--scale PERCENT", Integer, "Scale image by percentage") do |s|
|
|
117
|
+
options[:scale_percent] = s
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
opts.on("--interpolation METHOD", [:none, :linear, :cubic, :nohalo, :lohalo],
|
|
121
|
+
"Interpolation method: none, linear, cubic, nohalo, lohalo (default: nohalo)") do |i|
|
|
122
|
+
options[:scale_interpolation] = i.to_s
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
opts.on("--sharpen", "Apply unsharp mask after scaling (enhances edges)") do
|
|
126
|
+
options[:sharpen] = true
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
opts.on("--sharpen-radius VALUE", Float, "Sharpen radius in pixels (default: 2.0)") do |r|
|
|
130
|
+
options[:sharpen_radius] = r
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
opts.on("--sharpen-gain VALUE", Float, "Sharpen gain/strength (default: 0.5, range: 0.0-2.0+)") do |g|
|
|
134
|
+
options[:sharpen_gain] = g
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
opts.on("--sharpen-threshold VALUE", Float, "Sharpen threshold as fraction (default: 0.03, range: 0.0-1.0)") do |t|
|
|
138
|
+
options[:sharpen_threshold] = t
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
opts.on("-r", "--remove-bg", "Remove background from spritesheet using GIMP") do
|
|
142
|
+
options[:remove_bg] = true
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
opts.on("-t", "--threshold VALUE", Float, "Feather radius (default: 0.0 = no feathering)") do |t|
|
|
146
|
+
options[:bg_threshold] = t
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
opts.on("-g", "--grow PIXELS", Integer, "Pixels to grow selection (default: 1)") do |g|
|
|
150
|
+
options[:grow_selection] = g
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
opts.separator ""
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def add_bg_removal_options(opts, options)
|
|
157
|
+
opts.separator "Background Removal Method:"
|
|
158
|
+
|
|
159
|
+
opts.on("--fuzzy", "Use fuzzy select (contiguous regions) - DEFAULT") do
|
|
160
|
+
options[:fuzzy_select] = true
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
opts.on("--no-fuzzy", "Use global color select (all matching pixels)") do
|
|
164
|
+
options[:fuzzy_select] = false
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
opts.separator ""
|
|
168
|
+
opts.separator "Operation Order:"
|
|
169
|
+
|
|
170
|
+
opts.on("--order ORDER", [:scale_first, :bg_first],
|
|
171
|
+
"Operation order: scale_first or bg_first (default: scale_first)") do |order|
|
|
172
|
+
options[:operation_order] = order == :scale_first ? :scale_then_remove_bg : :remove_bg_then_scale
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
opts.separator ""
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def add_consolidation_options(opts, options)
|
|
179
|
+
opts.separator "Consolidation Options:"
|
|
180
|
+
|
|
181
|
+
opts.on("--[no-]validate-columns", "Abort if column counts don't match (default: true)") do |v|
|
|
182
|
+
options[:validate_columns] = v
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
opts.separator ""
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def add_preset_options(opts, options)
|
|
189
|
+
opts.separator "Preset Configurations:"
|
|
190
|
+
|
|
191
|
+
preset_descriptions = PRESETS.map do |name, config|
|
|
192
|
+
" #{name}: #{config[:columns]}×? grid, #{config[:frame_count]} frames, #{config[:max_width]}px wide"
|
|
193
|
+
end.join("\n")
|
|
194
|
+
|
|
195
|
+
opts.on("--preset NAME", String, "Apply preset configuration:",
|
|
196
|
+
*preset_descriptions.split("\n")) do |preset_name|
|
|
197
|
+
preset_key = preset_name.to_sym
|
|
198
|
+
unless PRESETS.key?(preset_key)
|
|
199
|
+
valid_presets = PRESETS.keys.join(', ')
|
|
200
|
+
raise OptionParser::InvalidArgument, "Unknown preset: #{preset_name}. Valid options: #{valid_presets}"
|
|
201
|
+
end
|
|
202
|
+
options.merge!(PRESETS[preset_key])
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
opts.separator ""
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def add_other_options(opts, options)
|
|
209
|
+
opts.separator "Other Options:"
|
|
210
|
+
|
|
211
|
+
opts.on("--keep-temp", "Keep temporary files for debugging") do
|
|
212
|
+
options[:keep_temp] = true
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
opts.on("--debug", "Enable debug mode (verbose output + keep temp files)") do
|
|
216
|
+
options[:debug] = true
|
|
217
|
+
options[:keep_temp] = true
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
opts.on("-h", "--help", "Show this help message") do
|
|
221
|
+
puts opts
|
|
222
|
+
exit
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
opts.on("--version", "Show version information") do
|
|
226
|
+
puts "Ruby Spriter v#{VERSION}"
|
|
227
|
+
puts "Platform: #{Platform.current.to_s.capitalize}"
|
|
228
|
+
puts "Date: #{VERSION_DATE}"
|
|
229
|
+
exit
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
opts.on("--check-dependencies", "Check if all required external tools are installed") do
|
|
233
|
+
options[:check_dependencies] = true
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
opts.separator ""
|
|
237
|
+
opts.separator "Examples:"
|
|
238
|
+
opts.separator " ruby_spriter --check-dependencies"
|
|
239
|
+
opts.separator " ruby_spriter --video input.mp4"
|
|
240
|
+
opts.separator " ruby_spriter --video input.mp4 --remove-bg --scale 50"
|
|
241
|
+
opts.separator " ruby_spriter --video input.mp4 --scale 50 --interpolation nohalo --sharpen"
|
|
242
|
+
opts.separator " ruby_spriter --image sprite.png --scale 50 --sharpen --sharpen-gain 1.5"
|
|
243
|
+
opts.separator " ruby_spriter --image sprite.png --remove-bg --fuzzy"
|
|
244
|
+
opts.separator " ruby_spriter --consolidate file1.png,file2.png,file3.png"
|
|
245
|
+
opts.separator " ruby_spriter --verify spritesheet.png"
|
|
246
|
+
opts.separator ""
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module RubySpriter
|
|
6
|
+
# Consolidates multiple spritesheets vertically
|
|
7
|
+
class Consolidator
|
|
8
|
+
attr_reader :options
|
|
9
|
+
|
|
10
|
+
def initialize(options = {})
|
|
11
|
+
@options = options
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Consolidate multiple spritesheets into one
|
|
15
|
+
# @param files [Array<String>] Array of spritesheet file paths
|
|
16
|
+
# @param output_file [String] Output consolidated file path
|
|
17
|
+
# @return [Hash] Processing results
|
|
18
|
+
def consolidate(files, output_file)
|
|
19
|
+
validate_files!(files)
|
|
20
|
+
|
|
21
|
+
Utils::OutputFormatter.header("Consolidating Spritesheets")
|
|
22
|
+
|
|
23
|
+
metadata_list = read_all_metadata(files)
|
|
24
|
+
validate_compatibility!(metadata_list) if options[:validate_columns]
|
|
25
|
+
|
|
26
|
+
create_consolidated_image(files, output_file)
|
|
27
|
+
|
|
28
|
+
total_frames = metadata_list.sum { |m| m[:frames] }
|
|
29
|
+
columns = metadata_list.first[:columns]
|
|
30
|
+
rows = (total_frames.to_f / columns).ceil
|
|
31
|
+
|
|
32
|
+
# Embed consolidated metadata
|
|
33
|
+
temp_file = output_file.sub('.png', '_temp.png')
|
|
34
|
+
File.rename(output_file, temp_file)
|
|
35
|
+
|
|
36
|
+
MetadataManager.embed(
|
|
37
|
+
temp_file,
|
|
38
|
+
output_file,
|
|
39
|
+
columns: columns,
|
|
40
|
+
rows: rows,
|
|
41
|
+
frames: total_frames,
|
|
42
|
+
debug: options[:debug]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
File.delete(temp_file) if File.exist?(temp_file)
|
|
46
|
+
|
|
47
|
+
file_size = File.size(output_file)
|
|
48
|
+
|
|
49
|
+
# Display results with Godot instructions
|
|
50
|
+
display_consolidation_results(output_file, file_size, files, metadata_list, columns, rows, total_frames)
|
|
51
|
+
|
|
52
|
+
{
|
|
53
|
+
output_file: output_file,
|
|
54
|
+
columns: columns,
|
|
55
|
+
rows: rows,
|
|
56
|
+
frames: total_frames,
|
|
57
|
+
size: file_size
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def validate_files!(files)
|
|
64
|
+
raise ValidationError, "Need at least 2 files to consolidate" if files.length < 2
|
|
65
|
+
|
|
66
|
+
files.each { |file| Utils::FileHelper.validate_readable!(file) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def read_all_metadata(files)
|
|
70
|
+
metadata_list = files.map do |file|
|
|
71
|
+
metadata = MetadataManager.read(file)
|
|
72
|
+
|
|
73
|
+
unless metadata
|
|
74
|
+
raise ValidationError, "File missing metadata: #{file}\nAll files must be Ruby Spriter spritesheets."
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
metadata
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
metadata_list
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def validate_compatibility!(metadata_list)
|
|
84
|
+
columns = metadata_list.first[:columns]
|
|
85
|
+
|
|
86
|
+
incompatible = metadata_list.find { |m| m[:columns] != columns }
|
|
87
|
+
|
|
88
|
+
if incompatible
|
|
89
|
+
raise ValidationError,
|
|
90
|
+
"Column count mismatch: Expected #{columns}, found #{incompatible[:columns]}\n" \
|
|
91
|
+
"Use --no-validate-columns to force consolidation."
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def create_consolidated_image(files, output_file)
|
|
96
|
+
Utils::OutputFormatter.indent("Stacking spritesheets vertically...")
|
|
97
|
+
|
|
98
|
+
# Use ImageMagick to stack images vertically
|
|
99
|
+
magick_cmd = Platform.imagemagick_convert_cmd
|
|
100
|
+
|
|
101
|
+
cmd = [
|
|
102
|
+
magick_cmd,
|
|
103
|
+
*files.map { |f| Utils::PathHelper.quote_path(f) },
|
|
104
|
+
'-append',
|
|
105
|
+
Utils::PathHelper.quote_path(output_file)
|
|
106
|
+
].join(' ')
|
|
107
|
+
|
|
108
|
+
if options[:debug]
|
|
109
|
+
Utils::OutputFormatter.indent("DEBUG: ImageMagick command: #{cmd}")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
113
|
+
|
|
114
|
+
unless status.success?
|
|
115
|
+
raise ProcessingError, "ImageMagick consolidation failed: #{stderr}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
Utils::FileHelper.validate_exists!(output_file)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def display_consolidation_results(output_file, file_size, files, metadata_list, columns, rows, total_frames)
|
|
122
|
+
Utils::OutputFormatter.success("Consolidated spritesheet created")
|
|
123
|
+
Utils::OutputFormatter.indent("Output: #{output_file}")
|
|
124
|
+
Utils::OutputFormatter.indent("Size: #{Utils::FileHelper.format_size(file_size)}")
|
|
125
|
+
Utils::OutputFormatter.note("Combined #{files.length} spritesheets (#{total_frames} total frames)")
|
|
126
|
+
|
|
127
|
+
puts "\n Grid Layout:"
|
|
128
|
+
Utils::OutputFormatter.indent("Columns: #{columns}")
|
|
129
|
+
Utils::OutputFormatter.indent("Rows: #{rows}")
|
|
130
|
+
Utils::OutputFormatter.indent("Total Frames: #{total_frames}")
|
|
131
|
+
|
|
132
|
+
puts "\n 📊 Godot AnimatedSprite2D Settings:"
|
|
133
|
+
Utils::OutputFormatter.indent("HFrames = #{columns}")
|
|
134
|
+
Utils::OutputFormatter.indent("VFrames = #{rows}")
|
|
135
|
+
|
|
136
|
+
puts "\n 📋 Source Breakdown:"
|
|
137
|
+
metadata_list.each_with_index do |meta, index|
|
|
138
|
+
file_basename = File.basename(files[index])
|
|
139
|
+
Utils::OutputFormatter.indent("#{index + 1}. #{file_basename}")
|
|
140
|
+
Utils::OutputFormatter.indent(" └─ #{meta[:columns]}×#{meta[:rows]} grid (#{meta[:frames]} frames)")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
puts ""
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,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
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
def initialize(verbose: false)
|
|
39
|
+
@verbose = verbose
|
|
40
|
+
@gimp_path = nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Check all dependencies
|
|
44
|
+
# @return [Hash] Results of dependency checks
|
|
45
|
+
def check_all
|
|
46
|
+
results = {}
|
|
47
|
+
|
|
48
|
+
REQUIRED_TOOLS.each do |tool, config|
|
|
49
|
+
results[tool] = check_tool(tool, config)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
results[:gimp] = check_gimp
|
|
53
|
+
|
|
54
|
+
results
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Check if all dependencies are satisfied
|
|
58
|
+
# @return [Boolean] true if all dependencies are available
|
|
59
|
+
def all_satisfied?
|
|
60
|
+
results = check_all
|
|
61
|
+
results.all? { |_tool, status| status[:available] }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Print dependency status report
|
|
65
|
+
def print_report
|
|
66
|
+
results = check_all
|
|
67
|
+
|
|
68
|
+
puts "\n" + "=" * 60
|
|
69
|
+
puts "Dependency Check"
|
|
70
|
+
puts "=" * 60
|
|
71
|
+
|
|
72
|
+
results.each do |tool, status|
|
|
73
|
+
icon = status[:available] ? "✅" : "❌"
|
|
74
|
+
puts "\n#{icon} #{tool.to_s.upcase}"
|
|
75
|
+
|
|
76
|
+
if status[:available]
|
|
77
|
+
puts " Found: #{status[:path] || status[:version]}"
|
|
78
|
+
else
|
|
79
|
+
puts " Status: NOT FOUND"
|
|
80
|
+
puts " Install: #{status[:install_cmd]}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
puts "\n" + "=" * 60 + "\n"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Get the found GIMP executable path
|
|
88
|
+
attr_reader :gimp_path
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def check_tool(tool, config)
|
|
93
|
+
stdout, stderr, status = Open3.capture3(config[:command])
|
|
94
|
+
output = stdout + stderr
|
|
95
|
+
|
|
96
|
+
if status.success? && output.match?(config[:pattern])
|
|
97
|
+
version = extract_version(output)
|
|
98
|
+
{
|
|
99
|
+
available: true,
|
|
100
|
+
version: version,
|
|
101
|
+
install_cmd: nil
|
|
102
|
+
}
|
|
103
|
+
else
|
|
104
|
+
{
|
|
105
|
+
available: false,
|
|
106
|
+
version: nil,
|
|
107
|
+
install_cmd: config[:install][Platform.current]
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
puts "DEBUG: Error checking #{tool}: #{e.message}" if @verbose
|
|
112
|
+
{
|
|
113
|
+
available: false,
|
|
114
|
+
version: nil,
|
|
115
|
+
install_cmd: config[:install][Platform.current]
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def check_gimp
|
|
120
|
+
# Try default path first
|
|
121
|
+
default_path = Platform.default_gimp_path
|
|
122
|
+
if gimp_exists?(default_path)
|
|
123
|
+
@gimp_path = default_path
|
|
124
|
+
return gimp_status(true, default_path)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Try alternative paths
|
|
128
|
+
Platform.alternative_gimp_paths.each do |path|
|
|
129
|
+
if gimp_exists?(path)
|
|
130
|
+
@gimp_path = path
|
|
131
|
+
return gimp_status(true, path)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Not found
|
|
136
|
+
gimp_status(false, nil)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def gimp_exists?(path)
|
|
140
|
+
return false if path.nil? || path.empty?
|
|
141
|
+
File.exist?(path)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def gimp_status(available, path)
|
|
145
|
+
{
|
|
146
|
+
available: available,
|
|
147
|
+
path: path,
|
|
148
|
+
install_cmd: gimp_install_command
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def gimp_install_command
|
|
153
|
+
case Platform.current
|
|
154
|
+
when :windows
|
|
155
|
+
'Download from https://www.gimp.org/downloads/ or use: choco install gimp'
|
|
156
|
+
when :linux
|
|
157
|
+
'sudo apt install gimp'
|
|
158
|
+
when :macos
|
|
159
|
+
'brew install gimp'
|
|
160
|
+
else
|
|
161
|
+
'Visit https://www.gimp.org/downloads/'
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def extract_version(output)
|
|
166
|
+
# Try to extract version number from output
|
|
167
|
+
if output =~ /version\s+(\d+\.\d+[\.\d]*)/i
|
|
168
|
+
$1
|
|
169
|
+
else
|
|
170
|
+
output.lines.first&.strip || 'Unknown'
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|