ruby_spriter 0.6.7 → 0.7.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rspec +3 -3
- data/CHANGELOG.md +1035 -405
- data/Gemfile +17 -17
- data/LICENSE +21 -21
- data/README.md +183 -902
- data/bin/ruby_spriter +20 -20
- data/lib/ruby_spriter/background_sampler.rb +140 -0
- data/lib/ruby_spriter/batch_processor.rb +268 -212
- data/lib/ruby_spriter/cell_cleanup_config.rb +21 -0
- data/lib/ruby_spriter/cell_cleanup_gimp_script.rb +123 -0
- data/lib/ruby_spriter/cell_cleanup_processor.rb +230 -0
- data/lib/ruby_spriter/cli.rb +676 -612
- data/lib/ruby_spriter/compression_manager.rb +101 -101
- data/lib/ruby_spriter/consolidator.rb +179 -179
- data/lib/ruby_spriter/dependency_checker.rb +224 -174
- data/lib/ruby_spriter/ghost_edge_cleaner.rb +164 -0
- data/lib/ruby_spriter/gimp_processor.rb +1188 -667
- data/lib/ruby_spriter/metadata_manager.rb +117 -116
- data/lib/ruby_spriter/platform.rb +137 -82
- data/lib/ruby_spriter/processor.rb +1230 -886
- data/lib/ruby_spriter/smoke_detector.rb +223 -0
- data/lib/ruby_spriter/threshold_stepper.rb +227 -0
- data/lib/ruby_spriter/utils/file_helper.rb +82 -82
- data/lib/ruby_spriter/utils/image_helper.rb +16 -0
- data/lib/ruby_spriter/utils/output_formatter.rb +65 -65
- data/lib/ruby_spriter/utils/path_helper.rb +59 -59
- data/lib/ruby_spriter/utils/spritesheet_splitter.rb +86 -86
- data/lib/ruby_spriter/version.rb +6 -7
- data/lib/ruby_spriter/video_processor.rb +357 -139
- data/lib/ruby_spriter.rb +38 -34
- data/ruby_spriter.gemspec +44 -42
- data/spec/fixtures/centered_sprite_with_inner_bg.png +0 -0
- data/spec/fixtures/centered_sprite_with_inner_bg_inner_removed.png +0 -0
- data/spec/fixtures/centered_sprite_with_inner_bg_threshold_stepped.png +0 -0
- data/spec/fixtures/complex_background_sprite.png +0 -0
- data/spec/fixtures/death - bloodborne rekconing.mp4 +0 -0
- data/spec/fixtures/death - bloodborne rekconing_spritesheet-nobg-global.png +0 -0
- data/spec/fixtures/death - bloodborne rekconing_spritesheet.png +0 -0
- data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185027_431-nobg-global.png +0 -0
- data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185027_431.png +0 -0
- data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185125_738-nobg-global.png +0 -0
- data/spec/fixtures/death - bloodborne rekconing_spritesheet_20251110_185125_738.png +0 -0
- data/spec/fixtures/has_inner_bg.png +0 -0
- data/spec/fixtures/has_small_inner_bg.png +0 -0
- data/spec/fixtures/smoke_effect_sprite.png +0 -0
- data/spec/fixtures/spritesheet_with_metadata.png +0 -0
- data/spec/fixtures/test_sprite.png +0 -0
- data/spec/fixtures/test_sprite_smoke_processed.png +0 -0
- data/spec/fixtures/test_video_spritesheet.png +0 -0
- data/spec/fixtures/transparent_bg_sprite.png +0 -0
- data/spec/fixtures/walk_north_sprite-sheet.png +0 -0
- data/spec/integration/no_fuzzy_mode_spec.rb +100 -0
- data/spec/ruby_spriter/batch_processor_spec.rb +509 -200
- data/spec/ruby_spriter/cli_spec.rb +2026 -1892
- data/spec/ruby_spriter/compression_manager_spec.rb +157 -157
- data/spec/ruby_spriter/consolidator_spec.rb +538 -538
- data/spec/ruby_spriter/gimp_processor_spec.rb +523 -425
- data/spec/ruby_spriter/platform_spec.rb +92 -82
- data/spec/ruby_spriter/processor_spec.rb +911 -735
- data/spec/ruby_spriter/utils/file_helper_spec.rb +150 -150
- data/spec/ruby_spriter/utils/path_helper_spec.rb +78 -78
- data/spec/ruby_spriter/utils/spritesheet_splitter_spec.rb +104 -104
- data/spec/ruby_spriter/video_processor_spec.rb +346 -29
- data/spec/spec_helper.rb +41 -41
- data/spec/tmp/cli_test_output.png +0 -0
- data/spec/tmp/cli_test_output_20251105_114647_395.png +0 -0
- data/spec/tmp/combined_test.png +0 -0
- data/spec/tmp/compat_test.png +0 -0
- data/spec/tmp/compat_test_20251030_174233_361.png +0 -0
- data/spec/tmp/final_all_features.png +0 -0
- data/spec/tmp/final_test_all_features.png +0 -0
- data/spec/tmp/full_pipeline_test.png +0 -0
- data/spec/tmp/inner_test.png +0 -0
- data/spec/tmp/integration_test.png +0 -0
- data/spec/tmp/validation_test.png +0 -0
- data/spec/unit/background_sampler_spec.rb +132 -0
- data/spec/unit/cell_cleanup_config_spec.rb +32 -0
- data/spec/unit/cell_cleanup_gimp_script_spec.rb +59 -0
- data/spec/unit/cell_cleanup_processor_spec.rb +261 -0
- data/spec/unit/ghost_edge_cleaner_spec.rb +223 -0
- data/spec/unit/gimp_processor_single_point_selection_spec.rb +81 -0
- data/spec/unit/smoke_detector_spec.rb +246 -0
- data/spec/unit/threshold_stepper_spec.rb +195 -0
- metadata +56 -10
|
@@ -1,101 +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
|
|
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
|
|
@@ -1,179 +1,179 @@
|
|
|
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
|
-
# 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
|
-
|
|
94
|
-
private
|
|
95
|
-
|
|
96
|
-
def validate_files!(files)
|
|
97
|
-
raise ValidationError, "Need at least 2 files to consolidate" if files.length < 2
|
|
98
|
-
|
|
99
|
-
files.each { |file| Utils::FileHelper.validate_readable!(file) }
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def read_all_metadata(files)
|
|
103
|
-
metadata_list = files.map do |file|
|
|
104
|
-
metadata = MetadataManager.read(file)
|
|
105
|
-
|
|
106
|
-
unless metadata
|
|
107
|
-
raise ValidationError, "File missing metadata: #{file}\nAll files must be Ruby Spriter spritesheets."
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
metadata
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
metadata_list
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def validate_compatibility!(metadata_list)
|
|
117
|
-
columns = metadata_list.first[:columns]
|
|
118
|
-
|
|
119
|
-
incompatible = metadata_list.find { |m| m[:columns] != columns }
|
|
120
|
-
|
|
121
|
-
if incompatible
|
|
122
|
-
raise ValidationError,
|
|
123
|
-
"Column count mismatch: Expected #{columns}, found #{incompatible[:columns]}\n" \
|
|
124
|
-
"Use --no-validate-columns to force consolidation."
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def create_consolidated_image(files, output_file)
|
|
129
|
-
Utils::OutputFormatter.indent("Stacking spritesheets vertically...")
|
|
130
|
-
|
|
131
|
-
# Use ImageMagick to stack images vertically
|
|
132
|
-
magick_cmd = Platform.imagemagick_convert_cmd
|
|
133
|
-
|
|
134
|
-
cmd = [
|
|
135
|
-
magick_cmd,
|
|
136
|
-
*files.map { |f| Utils::PathHelper.quote_path(f) },
|
|
137
|
-
'-append',
|
|
138
|
-
Utils::PathHelper.quote_path(output_file)
|
|
139
|
-
].join(' ')
|
|
140
|
-
|
|
141
|
-
if options[:debug]
|
|
142
|
-
Utils::OutputFormatter.indent("DEBUG: ImageMagick command: #{cmd}")
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
stdout, stderr, status = Open3.capture3(cmd)
|
|
146
|
-
|
|
147
|
-
unless status.success?
|
|
148
|
-
raise ProcessingError, "ImageMagick consolidation failed: #{stderr}"
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
Utils::FileHelper.validate_exists!(output_file)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def display_consolidation_results(output_file, file_size, files, metadata_list, columns, rows, total_frames)
|
|
155
|
-
Utils::OutputFormatter.success("Consolidated spritesheet created")
|
|
156
|
-
Utils::OutputFormatter.indent("Output: #{output_file}")
|
|
157
|
-
Utils::OutputFormatter.indent("Size: #{Utils::FileHelper.format_size(file_size)}")
|
|
158
|
-
Utils::OutputFormatter.note("Combined #{files.length} spritesheets (#{total_frames} total frames)")
|
|
159
|
-
|
|
160
|
-
puts "\n Grid Layout:"
|
|
161
|
-
Utils::OutputFormatter.indent("Columns: #{columns}")
|
|
162
|
-
Utils::OutputFormatter.indent("Rows: #{rows}")
|
|
163
|
-
Utils::OutputFormatter.indent("Total Frames: #{total_frames}")
|
|
164
|
-
|
|
165
|
-
puts "\n 📊 Godot AnimatedSprite2D Settings:"
|
|
166
|
-
Utils::OutputFormatter.indent("HFrames = #{columns}")
|
|
167
|
-
Utils::OutputFormatter.indent("VFrames = #{rows}")
|
|
168
|
-
|
|
169
|
-
puts "\n 📋 Source Breakdown:"
|
|
170
|
-
metadata_list.each_with_index do |meta, index|
|
|
171
|
-
file_basename = File.basename(files[index])
|
|
172
|
-
Utils::OutputFormatter.indent("#{index + 1}. #{file_basename}")
|
|
173
|
-
Utils::OutputFormatter.indent(" └─ #{meta[:columns]}×#{meta[:rows]} grid (#{meta[:frames]} frames)")
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
puts ""
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
end
|
|
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
|
+
# 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
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def validate_files!(files)
|
|
97
|
+
raise ValidationError, "Need at least 2 files to consolidate" if files.length < 2
|
|
98
|
+
|
|
99
|
+
files.each { |file| Utils::FileHelper.validate_readable!(file) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def read_all_metadata(files)
|
|
103
|
+
metadata_list = files.map do |file|
|
|
104
|
+
metadata = MetadataManager.read(file)
|
|
105
|
+
|
|
106
|
+
unless metadata
|
|
107
|
+
raise ValidationError, "File missing metadata: #{file}\nAll files must be Ruby Spriter spritesheets."
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
metadata
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
metadata_list
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def validate_compatibility!(metadata_list)
|
|
117
|
+
columns = metadata_list.first[:columns]
|
|
118
|
+
|
|
119
|
+
incompatible = metadata_list.find { |m| m[:columns] != columns }
|
|
120
|
+
|
|
121
|
+
if incompatible
|
|
122
|
+
raise ValidationError,
|
|
123
|
+
"Column count mismatch: Expected #{columns}, found #{incompatible[:columns]}\n" \
|
|
124
|
+
"Use --no-validate-columns to force consolidation."
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def create_consolidated_image(files, output_file)
|
|
129
|
+
Utils::OutputFormatter.indent("Stacking spritesheets vertically...")
|
|
130
|
+
|
|
131
|
+
# Use ImageMagick to stack images vertically
|
|
132
|
+
magick_cmd = Platform.imagemagick_convert_cmd
|
|
133
|
+
|
|
134
|
+
cmd = [
|
|
135
|
+
magick_cmd,
|
|
136
|
+
*files.map { |f| Utils::PathHelper.quote_path(f) },
|
|
137
|
+
'-append',
|
|
138
|
+
Utils::PathHelper.quote_path(output_file)
|
|
139
|
+
].join(' ')
|
|
140
|
+
|
|
141
|
+
if options[:debug]
|
|
142
|
+
Utils::OutputFormatter.indent("DEBUG: ImageMagick command: #{cmd}")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
146
|
+
|
|
147
|
+
unless status.success?
|
|
148
|
+
raise ProcessingError, "ImageMagick consolidation failed: #{stderr}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
Utils::FileHelper.validate_exists!(output_file)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def display_consolidation_results(output_file, file_size, files, metadata_list, columns, rows, total_frames)
|
|
155
|
+
Utils::OutputFormatter.success("Consolidated spritesheet created")
|
|
156
|
+
Utils::OutputFormatter.indent("Output: #{output_file}")
|
|
157
|
+
Utils::OutputFormatter.indent("Size: #{Utils::FileHelper.format_size(file_size)}")
|
|
158
|
+
Utils::OutputFormatter.note("Combined #{files.length} spritesheets (#{total_frames} total frames)")
|
|
159
|
+
|
|
160
|
+
puts "\n Grid Layout:"
|
|
161
|
+
Utils::OutputFormatter.indent("Columns: #{columns}")
|
|
162
|
+
Utils::OutputFormatter.indent("Rows: #{rows}")
|
|
163
|
+
Utils::OutputFormatter.indent("Total Frames: #{total_frames}")
|
|
164
|
+
|
|
165
|
+
puts "\n 📊 Godot AnimatedSprite2D Settings:"
|
|
166
|
+
Utils::OutputFormatter.indent("HFrames = #{columns}")
|
|
167
|
+
Utils::OutputFormatter.indent("VFrames = #{rows}")
|
|
168
|
+
|
|
169
|
+
puts "\n 📋 Source Breakdown:"
|
|
170
|
+
metadata_list.each_with_index do |meta, index|
|
|
171
|
+
file_basename = File.basename(files[index])
|
|
172
|
+
Utils::OutputFormatter.indent("#{index + 1}. #{file_basename}")
|
|
173
|
+
Utils::OutputFormatter.indent(" └─ #{meta[:columns]}×#{meta[:rows]} grid (#{meta[:frames]} frames)")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
puts ""
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|