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,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module RubySpriter
|
|
6
|
+
# Manages PNG metadata for spritesheets
|
|
7
|
+
class MetadataManager
|
|
8
|
+
METADATA_PREFIX = 'SPRITESHEET'
|
|
9
|
+
|
|
10
|
+
# Embed metadata into PNG file
|
|
11
|
+
# @param input_file [String] Source PNG file
|
|
12
|
+
# @param output_file [String] Destination PNG file with metadata
|
|
13
|
+
# @param columns [Integer] Number of columns in grid
|
|
14
|
+
# @param rows [Integer] Number of rows in grid
|
|
15
|
+
# @param frames [Integer] Total number of frames
|
|
16
|
+
# @param debug [Boolean] Enable debug output
|
|
17
|
+
def self.embed(input_file, output_file, columns:, rows:, frames:, debug: false)
|
|
18
|
+
Utils::FileHelper.validate_readable!(input_file)
|
|
19
|
+
|
|
20
|
+
metadata_str = build_metadata_string(columns, rows, frames)
|
|
21
|
+
|
|
22
|
+
cmd = build_embed_command(input_file, output_file, metadata_str)
|
|
23
|
+
|
|
24
|
+
if debug
|
|
25
|
+
Utils::OutputFormatter.indent("DEBUG: Metadata command: #{cmd}")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
29
|
+
|
|
30
|
+
unless status.success?
|
|
31
|
+
raise ProcessingError, "Failed to embed metadata: #{stderr}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Utils::FileHelper.validate_exists!(output_file)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Read metadata from PNG file
|
|
38
|
+
# @param file [String] PNG file path
|
|
39
|
+
# @return [Hash, nil] Metadata hash or nil if not found
|
|
40
|
+
def self.read(file)
|
|
41
|
+
Utils::FileHelper.validate_readable!(file)
|
|
42
|
+
|
|
43
|
+
cmd = build_read_command(file)
|
|
44
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
45
|
+
|
|
46
|
+
return nil unless status.success?
|
|
47
|
+
|
|
48
|
+
parse_metadata(stdout)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Verify and print metadata from file
|
|
52
|
+
# @param file [String] PNG file path
|
|
53
|
+
def self.verify(file)
|
|
54
|
+
Utils::OutputFormatter.header("Spritesheet Metadata Verification")
|
|
55
|
+
|
|
56
|
+
puts "File: #{file}"
|
|
57
|
+
puts "Size: #{Utils::FileHelper.format_size(File.size(file))}\n\n"
|
|
58
|
+
|
|
59
|
+
metadata = read(file)
|
|
60
|
+
|
|
61
|
+
if metadata
|
|
62
|
+
Utils::OutputFormatter.success("Metadata Found")
|
|
63
|
+
puts "\n Grid Layout:"
|
|
64
|
+
Utils::OutputFormatter.indent("Columns: #{metadata[:columns]}")
|
|
65
|
+
Utils::OutputFormatter.indent("Rows: #{metadata[:rows]}")
|
|
66
|
+
Utils::OutputFormatter.indent("Total Frames: #{metadata[:frames]}")
|
|
67
|
+
Utils::OutputFormatter.indent("Metadata Version: #{metadata[:version]}")
|
|
68
|
+
else
|
|
69
|
+
Utils::OutputFormatter.warning("No spritesheet metadata found in this file")
|
|
70
|
+
puts "\nThis file may not have been created by Ruby Spriter,"
|
|
71
|
+
puts "or the metadata was stripped during processing."
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
puts "\n" + "=" * 60 + "\n"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private_class_method def self.build_metadata_string(columns, rows, frames)
|
|
78
|
+
"#{METADATA_PREFIX}|columns=#{columns}|rows=#{rows}|frames=#{frames}|version=#{METADATA_VERSION}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private_class_method def self.build_embed_command(input_file, output_file, metadata_str)
|
|
82
|
+
magick_cmd = Platform.imagemagick_convert_cmd
|
|
83
|
+
|
|
84
|
+
[
|
|
85
|
+
magick_cmd,
|
|
86
|
+
Utils::PathHelper.quote_path(input_file),
|
|
87
|
+
'-set', 'comment', Utils::PathHelper.quote_arg(metadata_str),
|
|
88
|
+
Utils::PathHelper.quote_path(output_file)
|
|
89
|
+
].join(' ')
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private_class_method def self.build_read_command(file)
|
|
93
|
+
magick_cmd = Platform.imagemagick_identify_cmd
|
|
94
|
+
|
|
95
|
+
[
|
|
96
|
+
magick_cmd,
|
|
97
|
+
'-format', Utils::PathHelper.quote_arg('%c'),
|
|
98
|
+
Utils::PathHelper.quote_path(file)
|
|
99
|
+
].join(' ')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private_class_method def self.parse_metadata(output)
|
|
103
|
+
# Look for SPRITESHEET metadata pattern
|
|
104
|
+
match = output.match(/#{METADATA_PREFIX}\|columns=(\d+)\|rows=(\d+)\|frames=(\d+)\|version=([\d.]+)/)
|
|
105
|
+
|
|
106
|
+
return nil unless match
|
|
107
|
+
|
|
108
|
+
{
|
|
109
|
+
columns: match[1].to_i,
|
|
110
|
+
rows: match[2].to_i,
|
|
111
|
+
frames: match[3].to_i,
|
|
112
|
+
version: match[4]
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubySpriter
|
|
4
|
+
# Platform detection and configuration
|
|
5
|
+
class Platform
|
|
6
|
+
PLATFORM_TYPE = case RUBY_PLATFORM
|
|
7
|
+
when /mingw|mswin|windows/i then :windows
|
|
8
|
+
when /linux/i then :linux
|
|
9
|
+
when /darwin/i then :macos
|
|
10
|
+
else :unknown
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# GIMP executable paths by platform
|
|
14
|
+
GIMP_DEFAULT_PATHS = {
|
|
15
|
+
windows: 'C:\\Program Files\\GIMP 3\\bin\\gimp-console-3.0.exe',
|
|
16
|
+
linux: '/usr/bin/gimp',
|
|
17
|
+
macos: '/Applications/GIMP.app/Contents/MacOS/gimp'
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
# Alternative GIMP paths to search
|
|
21
|
+
GIMP_ALTERNATIVE_PATHS = {
|
|
22
|
+
windows: [
|
|
23
|
+
'C:\\Program Files\\GIMP 3\\bin\\gimp-console-3.0.exe',
|
|
24
|
+
'C:\\Program Files (x86)\\GIMP 3\\bin\\gimp-console-3.0.exe',
|
|
25
|
+
'C:\\Program Files\\GIMP 2\\bin\\gimp-console-2.10.exe',
|
|
26
|
+
'C:\\Program Files (x86)\\GIMP 2\\bin\\gimp-console-2.10.exe'
|
|
27
|
+
].freeze,
|
|
28
|
+
linux: [
|
|
29
|
+
'/usr/bin/gimp',
|
|
30
|
+
'/usr/local/bin/gimp',
|
|
31
|
+
'/snap/bin/gimp',
|
|
32
|
+
'/opt/gimp/bin/gimp'
|
|
33
|
+
].freeze,
|
|
34
|
+
macos: [
|
|
35
|
+
'/Applications/GIMP.app/Contents/MacOS/gimp',
|
|
36
|
+
'/Applications/GIMP-2.10.app/Contents/MacOS/gimp'
|
|
37
|
+
].freeze
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
class << self
|
|
41
|
+
# Get the current platform type
|
|
42
|
+
def current
|
|
43
|
+
PLATFORM_TYPE
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Check if running on Windows
|
|
47
|
+
def windows?
|
|
48
|
+
PLATFORM_TYPE == :windows
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Check if running on Linux
|
|
52
|
+
def linux?
|
|
53
|
+
PLATFORM_TYPE == :linux
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Check if running on macOS
|
|
57
|
+
def macos?
|
|
58
|
+
PLATFORM_TYPE == :macos
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Get default GIMP path for current platform
|
|
62
|
+
def default_gimp_path
|
|
63
|
+
GIMP_DEFAULT_PATHS[PLATFORM_TYPE]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get alternative GIMP paths for current platform
|
|
67
|
+
def alternative_gimp_paths
|
|
68
|
+
GIMP_ALTERNATIVE_PATHS[PLATFORM_TYPE] || []
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get ImageMagick convert command name
|
|
72
|
+
def imagemagick_convert_cmd
|
|
73
|
+
windows? ? 'magick convert' : 'convert'
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Get ImageMagick identify command name
|
|
77
|
+
def imagemagick_identify_cmd
|
|
78
|
+
windows? ? 'magick identify' : 'identify'
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
|
|
6
|
+
module RubySpriter
|
|
7
|
+
# Main orchestration processor
|
|
8
|
+
class Processor
|
|
9
|
+
attr_reader :options, :gimp_path
|
|
10
|
+
|
|
11
|
+
def initialize(options = {})
|
|
12
|
+
@options = default_options.merge(options)
|
|
13
|
+
@gimp_path = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Run the processing workflow
|
|
17
|
+
def run
|
|
18
|
+
validate_options!
|
|
19
|
+
check_dependencies!
|
|
20
|
+
setup_temp_directory
|
|
21
|
+
|
|
22
|
+
result = execute_workflow
|
|
23
|
+
|
|
24
|
+
cleanup unless options[:keep_temp]
|
|
25
|
+
|
|
26
|
+
result
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def default_options
|
|
32
|
+
{
|
|
33
|
+
video: nil,
|
|
34
|
+
image: nil,
|
|
35
|
+
consolidate: nil,
|
|
36
|
+
verify: nil,
|
|
37
|
+
output: nil,
|
|
38
|
+
frame_count: 16,
|
|
39
|
+
columns: 4,
|
|
40
|
+
max_width: 320,
|
|
41
|
+
padding: 0,
|
|
42
|
+
bg_color: 'black',
|
|
43
|
+
scale_percent: nil,
|
|
44
|
+
scale_interpolation: 'nohalo',
|
|
45
|
+
sharpen: false,
|
|
46
|
+
sharpen_radius: 2.0,
|
|
47
|
+
sharpen_gain: 0.5,
|
|
48
|
+
sharpen_threshold: 0.03,
|
|
49
|
+
remove_bg: false,
|
|
50
|
+
bg_threshold: 0.0,
|
|
51
|
+
grow_selection: 1,
|
|
52
|
+
fuzzy_select: true,
|
|
53
|
+
operation_order: :scale_then_remove_bg,
|
|
54
|
+
validate_columns: true,
|
|
55
|
+
temp_dir: nil,
|
|
56
|
+
keep_temp: false,
|
|
57
|
+
debug: false
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def validate_options!
|
|
62
|
+
input_modes = [options[:video], options[:image], options[:consolidate], options[:verify]].compact
|
|
63
|
+
|
|
64
|
+
if input_modes.empty?
|
|
65
|
+
raise ValidationError, "Must specify --video, --image, --consolidate, or --verify"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if input_modes.length > 1
|
|
69
|
+
raise ValidationError, "Cannot use multiple input modes together. Choose one."
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
validate_input_files!
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def validate_input_files!
|
|
76
|
+
if options[:video]
|
|
77
|
+
Utils::FileHelper.validate_exists!(options[:video])
|
|
78
|
+
validate_file_extension!(options[:video], ['.mp4'], '--video')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if options[:image]
|
|
82
|
+
Utils::FileHelper.validate_exists!(options[:image])
|
|
83
|
+
validate_file_extension!(options[:image], ['.png'], '--image')
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
if options[:consolidate]
|
|
87
|
+
if options[:consolidate].length < 2
|
|
88
|
+
raise ValidationError, "--consolidate requires at least 2 files"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
options[:consolidate].each do |file|
|
|
92
|
+
Utils::FileHelper.validate_exists!(file)
|
|
93
|
+
validate_file_extension!(file, ['.png'], '--consolidate')
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if options[:verify]
|
|
98
|
+
Utils::FileHelper.validate_exists!(options[:verify])
|
|
99
|
+
validate_file_extension!(options[:verify], ['.png'], '--verify')
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def validate_file_extension!(file_path, valid_extensions, flag_name)
|
|
104
|
+
ext = File.extname(file_path).downcase
|
|
105
|
+
unless valid_extensions.include?(ext)
|
|
106
|
+
expected = valid_extensions.join(', ')
|
|
107
|
+
raise ValidationError, "#{flag_name} expects #{expected} file, got: #{ext || '(no extension)'}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def check_dependencies!
|
|
112
|
+
checker = DependencyChecker.new(verbose: options[:debug])
|
|
113
|
+
results = checker.check_all
|
|
114
|
+
|
|
115
|
+
# Check required tools
|
|
116
|
+
missing = []
|
|
117
|
+
|
|
118
|
+
[:ffmpeg, :ffprobe, :imagemagick].each do |tool|
|
|
119
|
+
missing << tool unless results[tool][:available]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# GIMP only needed for image processing
|
|
123
|
+
if needs_gimp? && !results[:gimp][:available]
|
|
124
|
+
missing << :gimp
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
if missing.any?
|
|
128
|
+
checker.print_report
|
|
129
|
+
raise DependencyError, "Missing required dependencies: #{missing.join(', ')}"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
@gimp_path = checker.gimp_path if results[:gimp][:available]
|
|
133
|
+
|
|
134
|
+
if options[:debug]
|
|
135
|
+
checker.print_report
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def needs_gimp?
|
|
140
|
+
options[:scale_percent] || options[:remove_bg]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def setup_temp_directory
|
|
144
|
+
@options[:temp_dir] = Dir.mktmpdir('ruby_spriter_')
|
|
145
|
+
|
|
146
|
+
if options[:debug]
|
|
147
|
+
Utils::OutputFormatter.indent("Temp directory: #{options[:temp_dir]}")
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def execute_workflow
|
|
152
|
+
Utils::OutputFormatter.header("Ruby Spriter v#{VERSION}")
|
|
153
|
+
puts "Platform: #{Platform.current.to_s.capitalize}"
|
|
154
|
+
puts "Date: #{VERSION_DATE}\n\n"
|
|
155
|
+
|
|
156
|
+
if options[:verify]
|
|
157
|
+
MetadataManager.verify(options[:verify])
|
|
158
|
+
return { mode: :verify, file: options[:verify] }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if options[:consolidate]
|
|
162
|
+
return execute_consolidate_workflow
|
|
163
|
+
elsif options[:image]
|
|
164
|
+
return execute_image_workflow
|
|
165
|
+
else
|
|
166
|
+
return execute_video_workflow
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def execute_video_workflow
|
|
171
|
+
# Step 1: Convert video to spritesheet
|
|
172
|
+
video_processor = VideoProcessor.new(options)
|
|
173
|
+
result = video_processor.create_spritesheet(
|
|
174
|
+
options[:video],
|
|
175
|
+
options[:output] || Utils::FileHelper.spritesheet_filename(options[:video])
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
working_file = result[:output_file]
|
|
179
|
+
|
|
180
|
+
# Step 2: Apply GIMP processing if requested
|
|
181
|
+
if needs_gimp?
|
|
182
|
+
working_file = process_with_gimp(working_file)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Step 3: Move to final output location if different
|
|
186
|
+
if options[:output] && working_file != options[:output]
|
|
187
|
+
FileUtils.cp(working_file, options[:output])
|
|
188
|
+
working_file = options[:output]
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
Utils::OutputFormatter.header("SUCCESS!")
|
|
192
|
+
Utils::OutputFormatter.success("Final output: #{working_file}")
|
|
193
|
+
|
|
194
|
+
result.merge(final_output: working_file)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def execute_image_workflow
|
|
198
|
+
working_file = options[:image]
|
|
199
|
+
|
|
200
|
+
# Apply GIMP processing if requested
|
|
201
|
+
if needs_gimp?
|
|
202
|
+
working_file = process_with_gimp(working_file)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Move to final output location if specified
|
|
206
|
+
if options[:output] && working_file != options[:output]
|
|
207
|
+
FileUtils.cp(working_file, options[:output])
|
|
208
|
+
working_file = options[:output]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
Utils::OutputFormatter.header("SUCCESS!")
|
|
212
|
+
Utils::OutputFormatter.success("Final output: #{working_file}")
|
|
213
|
+
|
|
214
|
+
{
|
|
215
|
+
mode: :image,
|
|
216
|
+
input_file: options[:image],
|
|
217
|
+
output_file: working_file
|
|
218
|
+
}
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def execute_consolidate_workflow
|
|
222
|
+
consolidator = Consolidator.new(options)
|
|
223
|
+
|
|
224
|
+
output_file = options[:output] || generate_consolidated_filename
|
|
225
|
+
|
|
226
|
+
result = consolidator.consolidate(options[:consolidate], output_file)
|
|
227
|
+
|
|
228
|
+
Utils::OutputFormatter.header("SUCCESS!")
|
|
229
|
+
Utils::OutputFormatter.success("Final output: #{result[:output_file]}")
|
|
230
|
+
|
|
231
|
+
result.merge(mode: :consolidate)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def process_with_gimp(input_file)
|
|
235
|
+
gimp_processor = GimpProcessor.new(@gimp_path, options)
|
|
236
|
+
gimp_processor.process(input_file)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def generate_consolidated_filename
|
|
240
|
+
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
|
|
241
|
+
"consolidated_spritesheet_#{timestamp}.png"
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def cleanup
|
|
245
|
+
if options[:temp_dir] && Dir.exist?(options[:temp_dir])
|
|
246
|
+
FileUtils.rm_rf(options[:temp_dir])
|
|
247
|
+
Utils::OutputFormatter.note("Cleaned up temporary files") if options[:debug]
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubySpriter
|
|
4
|
+
module Utils
|
|
5
|
+
# File naming and size utilities
|
|
6
|
+
class FileHelper
|
|
7
|
+
class << self
|
|
8
|
+
# Generate spritesheet filename from video file
|
|
9
|
+
# @param video_file [String] Path to video file
|
|
10
|
+
# @return [String] Generated spritesheet filename
|
|
11
|
+
def spritesheet_filename(video_file)
|
|
12
|
+
dir = File.dirname(video_file)
|
|
13
|
+
basename = File.basename(video_file, '.*')
|
|
14
|
+
File.join(dir, "#{basename}_spritesheet.png")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Generate output filename with suffix
|
|
18
|
+
# @param input_file [String] Original input file
|
|
19
|
+
# @param suffix [String] Suffix to add to filename
|
|
20
|
+
# @return [String] Generated output filename
|
|
21
|
+
def output_filename(input_file, suffix)
|
|
22
|
+
dir = File.dirname(input_file)
|
|
23
|
+
basename = File.basename(input_file, '.*')
|
|
24
|
+
File.join(dir, "#{basename}-#{suffix}.png")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Format file size in human-readable format
|
|
28
|
+
# @param bytes [Integer] File size in bytes
|
|
29
|
+
# @return [String] Formatted file size
|
|
30
|
+
def format_size(bytes)
|
|
31
|
+
if bytes >= 1024 * 1024
|
|
32
|
+
"#{(bytes / (1024.0 * 1024.0)).round(2)} MB"
|
|
33
|
+
elsif bytes >= 1024
|
|
34
|
+
"#{(bytes / 1024.0).round(2)} KB"
|
|
35
|
+
else
|
|
36
|
+
"#{bytes} bytes"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Validate file exists
|
|
41
|
+
# @param path [String] File path to validate
|
|
42
|
+
# @raise [ValidationError] if file doesn't exist
|
|
43
|
+
def validate_exists!(path)
|
|
44
|
+
raise ValidationError, "File not found: #{path}" unless File.exist?(path)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Validate file is readable
|
|
48
|
+
# @param path [String] File path to validate
|
|
49
|
+
# @raise [ValidationError] if file isn't readable
|
|
50
|
+
def validate_readable!(path)
|
|
51
|
+
validate_exists!(path)
|
|
52
|
+
raise ValidationError, "File not readable: #{path}" unless File.readable?(path)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubySpriter
|
|
4
|
+
module Utils
|
|
5
|
+
# Console output formatting utilities
|
|
6
|
+
class OutputFormatter
|
|
7
|
+
ICONS = {
|
|
8
|
+
success: '✅',
|
|
9
|
+
error: '❌',
|
|
10
|
+
warning: '⚠️',
|
|
11
|
+
info: 'ℹ️',
|
|
12
|
+
clean: '🧹',
|
|
13
|
+
note: '📝'
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# Print section header
|
|
18
|
+
# @param title [String] Section title
|
|
19
|
+
# @param width [Integer] Header width
|
|
20
|
+
def header(title, width = 60)
|
|
21
|
+
puts "\n" + "=" * width
|
|
22
|
+
puts title
|
|
23
|
+
puts "=" * width + "\n"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Print success message
|
|
27
|
+
# @param message [String] Message to print
|
|
28
|
+
def success(message)
|
|
29
|
+
puts "#{ICONS[:success]} #{message}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Print error message
|
|
33
|
+
# @param message [String] Message to print
|
|
34
|
+
def error(message)
|
|
35
|
+
puts "#{ICONS[:error]} #{message}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Print warning message
|
|
39
|
+
# @param message [String] Message to print
|
|
40
|
+
def warning(message)
|
|
41
|
+
puts "#{ICONS[:warning]} #{message}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Print info message
|
|
45
|
+
# @param message [String] Message to print
|
|
46
|
+
def info(message)
|
|
47
|
+
puts "#{ICONS[:info]} #{message}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Print note message
|
|
51
|
+
# @param message [String] Message to print
|
|
52
|
+
def note(message)
|
|
53
|
+
puts "#{ICONS[:note]} #{message}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Print indented message
|
|
57
|
+
# @param message [String] Message to print
|
|
58
|
+
# @param indent [Integer] Number of spaces to indent
|
|
59
|
+
def indent(message, indent = 6)
|
|
60
|
+
puts " " * indent + message
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubySpriter
|
|
4
|
+
module Utils
|
|
5
|
+
# Cross-platform path handling utilities
|
|
6
|
+
class PathHelper
|
|
7
|
+
class << self
|
|
8
|
+
# Quote a file path for shell execution
|
|
9
|
+
# @param path [String] The path to quote
|
|
10
|
+
# @return [String] Properly quoted path
|
|
11
|
+
def quote_path(path)
|
|
12
|
+
if Platform.windows?
|
|
13
|
+
"\"#{path}\""
|
|
14
|
+
else
|
|
15
|
+
# Need 4 backslashes because gsub interprets backslashes in replacement string
|
|
16
|
+
"'#{path.gsub("'", "\\\\'")}'"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Quote a command argument for shell execution
|
|
21
|
+
# @param arg [String] The argument to quote
|
|
22
|
+
# @return [String] Properly quoted argument
|
|
23
|
+
def quote_arg(arg)
|
|
24
|
+
if Platform.windows?
|
|
25
|
+
"\"#{arg}\""
|
|
26
|
+
else
|
|
27
|
+
# Need 4 backslashes because gsub interprets backslashes in replacement string
|
|
28
|
+
"'#{arg.gsub("'", "\\\\'")}'"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Normalize path for Python scripts (GIMP)
|
|
33
|
+
# @param path [String] The path to normalize
|
|
34
|
+
# @return [String] Normalized path with forward slashes
|
|
35
|
+
def normalize_for_python(path)
|
|
36
|
+
abs_path = File.absolute_path(path)
|
|
37
|
+
|
|
38
|
+
if Platform.windows?
|
|
39
|
+
# Use forward slashes for Python raw strings
|
|
40
|
+
abs_path.gsub('\\', '/')
|
|
41
|
+
else
|
|
42
|
+
abs_path
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Convert path to native format
|
|
47
|
+
# @param path [String] The path to convert
|
|
48
|
+
# @return [String] Path with platform-appropriate separators
|
|
49
|
+
def to_native(path)
|
|
50
|
+
if Platform.windows?
|
|
51
|
+
path.gsub('/', '\\')
|
|
52
|
+
else
|
|
53
|
+
path.gsub('\\', '/')
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|