ruby_spriter 0.6.5 โ 0.6.6
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 +50 -0
- data/README.md +40 -2
- data/lib/ruby_spriter/cli.rb +16 -0
- data/lib/ruby_spriter/gimp_processor.rb +6 -3
- data/lib/ruby_spriter/processor.rb +253 -23
- data/lib/ruby_spriter/utils/file_helper.rb +25 -0
- data/lib/ruby_spriter/utils/spritesheet_splitter.rb +86 -0
- data/lib/ruby_spriter/version.rb +1 -1
- data/lib/ruby_spriter/video_processor.rb +7 -7
- data/lib/ruby_spriter.rb +1 -0
- data/spec/ruby_spriter/cli_spec.rb +363 -0
- data/spec/ruby_spriter/processor_spec.rb +385 -0
- data/spec/ruby_spriter/utils/file_helper_spec.rb +80 -1
- data/spec/ruby_spriter/utils/spritesheet_splitter_spec.rb +104 -0
- data/spec/ruby_spriter/video_processor_spec.rb +29 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b6e4b3befadf222f9fe4a13b50eb16a5a39124c45f5dad2769f7c4f9153aae4f
|
|
4
|
+
data.tar.gz: 9f13e8d5da4c706a3d834f5a8d125ee46a1d7f04e5a643c90bda451bdb7f6e9c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8b05aec242fb65fb3f8745e276f7ec35ce2d05884f62e9b5af8fcef896a8f66ba2050c48d9c6e6c7400ad4ba93215225e5624c04bba25417e32dfff2e255b345
|
|
7
|
+
data.tar.gz: 97631e37ef5cbe5bb544526d1e696a870dc9cb26759d2838b1af4f77d81e03696f717225f3858ac55bd2a2ecdd3e86dbe6a4e8c91dc4ac8c75f52f7ff1df68e3
|
data/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
+
## [0.6.6] - 2025-10-23
|
|
16
|
+
|
|
17
|
+
### ๐ File Protection & Frame Extraction Release
|
|
18
|
+
|
|
19
|
+
#### Added
|
|
20
|
+
- **Automatic Unique Filenames**: By default, generates timestamped filenames to prevent accidental overwrites
|
|
21
|
+
- Format: `filename_YYYYMMDD_HHMMSS_mmm.ext` (includes milliseconds)
|
|
22
|
+
- Applies to all output modes: `--video`, `--image`, `--consolidate`
|
|
23
|
+
- Works with both auto-generated and `--output` specified filenames
|
|
24
|
+
- **`--overwrite` Flag**: Optional flag to explicitly allow overwriting existing files
|
|
25
|
+
- **`--split R:C` Option**: Split spritesheets into individual frames for `--image` workflow
|
|
26
|
+
- Format: `--split 4:4` (rows:columns, e.g., 4 rows ร 4 columns)
|
|
27
|
+
- Validation: Rows and columns must be 1-99, total frames < 1000
|
|
28
|
+
- Frame naming: `FRddd_filename.png` (3-digit zero-padded format: FR001, FR002, ..., FR999)
|
|
29
|
+
- Output directory: `filename_frames/`
|
|
30
|
+
- Metadata priority: Uses embedded metadata if available, unless `--override-md` flag is provided
|
|
31
|
+
- Dimension validation: Image dimensions must divide evenly by specified rows and columns
|
|
32
|
+
- **`--override-md` Flag**: Override embedded metadata when using `--split` with images that have metadata
|
|
33
|
+
- **Intermediate File Cleanup**: Fixed cleanup of intermediate files from GIMP processing
|
|
34
|
+
- Now correctly removes files with dash separator (e.g., `file-nobg-fuzzy.png`, `file-scaled-40pct.png`)
|
|
35
|
+
- Added Windows-compatible path normalization for file comparison
|
|
36
|
+
- **Frame Extraction Tests**: 17 new comprehensive tests for split functionality
|
|
37
|
+
- CLI option tests for `--split` and `--override-md`
|
|
38
|
+
- Format and range validation tests (10 tests)
|
|
39
|
+
- Metadata priority logic tests (5 tests)
|
|
40
|
+
- Updated SpritesheetSplitter tests for FR%03d format
|
|
41
|
+
|
|
42
|
+
#### Changed
|
|
43
|
+
- **Default Behavior**: Changed from overwriting to creating unique files (breaking change, but safer)
|
|
44
|
+
- **GimpProcessor**: Now respects `--overwrite` flag for scaled and background-removed images
|
|
45
|
+
- **Consolidate Workflow**: Default filename changed from `consolidated_spritesheet_TIMESTAMP.png` to `consolidated_spritesheet.png` (uniqueness handled by flag)
|
|
46
|
+
- **Frame Naming Format**: Changed from FR%02d (2 digits) to FR%03d (3 digits) to support up to 999 frames
|
|
47
|
+
- **Intermediate File Pattern**: Fixed glob pattern from underscore to dash separator for GIMP output files
|
|
48
|
+
|
|
49
|
+
#### Technical Details
|
|
50
|
+
- New utility methods in `Utils::FileHelper`:
|
|
51
|
+
- `unique_filename(path)` - Generates timestamped filename if file exists
|
|
52
|
+
- `ensure_unique_output(path, overwrite:)` - Applies overwrite logic
|
|
53
|
+
- New methods in `Processor`:
|
|
54
|
+
- `validate_split_option!` - Validates split format and ranges during initialization
|
|
55
|
+
- `determine_split_parameters(image_file)` - Implements metadata priority logic
|
|
56
|
+
- `validate_image_dimensions(image_file, rows, columns)` - Validates even division
|
|
57
|
+
- Processor workflows updated to use `ensure_unique_output` for all output paths
|
|
58
|
+
- `SpritesheetSplitter` updated to use 3-digit frame format (FR%03d)
|
|
59
|
+
- Test coverage increased to 72.27% (688/952 lines)
|
|
60
|
+
|
|
61
|
+
Closes #17, #19, #30
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
15
65
|
## [0.6.5] - 2025-10-23
|
|
16
66
|
|
|
17
67
|
### ๐ฆ Distribution & Packaging Release
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Ruby Spriter v0.6.
|
|
1
|
+
# Ruby Spriter v0.6.6
|
|
2
2
|
|
|
3
3
|
[](https://www.ruby-lang.org/)
|
|
4
4
|
[](LICENSE)
|
|
@@ -18,6 +18,7 @@ A powerful cross-platform Ruby tool for creating high-quality spritesheets from
|
|
|
18
18
|
- ๐จ **Quality Enhancement** - 5 interpolation methods and configurable unsharp masking
|
|
19
19
|
- ๐ **Spritesheet Consolidation** - Merge multiple spritesheets vertically
|
|
20
20
|
- ๐ **Metadata Management** - Embed and verify grid information in PNG files
|
|
21
|
+
- ๐ **Automatic File Protection** - Unique timestamped filenames prevent accidental overwrites (v0.6.6+)
|
|
21
22
|
- ๐ **Cross-Platform** - Works seamlessly on Windows, Linux, and macOS
|
|
22
23
|
- ๐งช **Production Ready** - Comprehensive RSpec test coverage
|
|
23
24
|
|
|
@@ -134,7 +135,7 @@ bundle install
|
|
|
134
135
|
|
|
135
136
|
# Build and install gem locally
|
|
136
137
|
gem build ruby_spriter.gemspec
|
|
137
|
-
gem install ruby_spriter-0.6.
|
|
138
|
+
gem install ruby_spriter-0.6.6.gem
|
|
138
139
|
```
|
|
139
140
|
|
|
140
141
|
**Best for**: Contributors, developers wanting latest code
|
|
@@ -286,6 +287,7 @@ ruby_spriter --consolidate file1.png,file2.png,file3.png \
|
|
|
286
287
|
|
|
287
288
|
#### **Other Options**
|
|
288
289
|
```bash
|
|
290
|
+
--overwrite Overwrite existing output files (default: create unique filenames)
|
|
289
291
|
--keep-temp Keep temporary files for debugging
|
|
290
292
|
--debug Enable verbose output + keep temp files
|
|
291
293
|
--check-dependencies Check if all required external tools are installed
|
|
@@ -340,6 +342,42 @@ ruby_spriter --image 4k_sprite.png \
|
|
|
340
342
|
|
|
341
343
|
## ๐ง Advanced Features
|
|
342
344
|
|
|
345
|
+
### File Protection with Unique Filenames (v0.6.6+)
|
|
346
|
+
|
|
347
|
+
By default, Ruby Spriter protects your existing files by generating unique timestamped filenames when output files already exist:
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
# First run - creates new file
|
|
351
|
+
ruby_spriter --image sprite.png --remove-bg
|
|
352
|
+
# Output: sprite-nobg-fuzzy.png
|
|
353
|
+
|
|
354
|
+
# Second run - creates unique file instead of overwriting
|
|
355
|
+
ruby_spriter --image sprite.png --remove-bg
|
|
356
|
+
# Output: sprite-nobg-fuzzy_20251023_170542_123.png
|
|
357
|
+
|
|
358
|
+
# Third run - another unique file
|
|
359
|
+
ruby_spriter --image sprite.png --remove-bg
|
|
360
|
+
# Output: sprite-nobg-fuzzy_20251023_170545_456.png
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
#### Overwrite Mode
|
|
364
|
+
|
|
365
|
+
Use `--overwrite` to replace existing files instead:
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
# Always overwrites sprite-nobg-fuzzy.png
|
|
369
|
+
ruby_spriter --image sprite.png --remove-bg --overwrite
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
#### Behavior by Mode
|
|
373
|
+
|
|
374
|
+
| Mode | Default Filename | Unique on Collision |
|
|
375
|
+
|------|------------------|---------------------|
|
|
376
|
+
| `--video` | `input_spritesheet.png` | โ
Yes |
|
|
377
|
+
| `--image` (with processing) | `input-scaled-50pct.png` | โ
Yes |
|
|
378
|
+
| `--consolidate` | `consolidated_spritesheet.png` | โ
Yes |
|
|
379
|
+
| Any with `--output` | Your specified name | โ
Yes (unless `--overwrite`) |
|
|
380
|
+
|
|
343
381
|
### Metadata Management
|
|
344
382
|
|
|
345
383
|
Ruby Spriter embeds grid information directly into PNG files:
|
data/lib/ruby_spriter/cli.rb
CHANGED
|
@@ -81,6 +81,14 @@ module RubySpriter
|
|
|
81
81
|
options[:verify] = v
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
opts.on("--split R:C", "Split image into frames (rows:columns, e.g., 4:4)") do |s|
|
|
85
|
+
options[:split] = s
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
opts.on("--override-md", "Override embedded metadata when using --split") do
|
|
89
|
+
options[:override_md] = true
|
|
90
|
+
end
|
|
91
|
+
|
|
84
92
|
opts.separator ""
|
|
85
93
|
end
|
|
86
94
|
|
|
@@ -107,6 +115,10 @@ module RubySpriter
|
|
|
107
115
|
options[:bg_color] = b
|
|
108
116
|
end
|
|
109
117
|
|
|
118
|
+
opts.on("--save-frames", "Save individual frames to disk (video only)") do
|
|
119
|
+
options[:save_frames] = true
|
|
120
|
+
end
|
|
121
|
+
|
|
110
122
|
opts.separator ""
|
|
111
123
|
end
|
|
112
124
|
|
|
@@ -208,6 +220,10 @@ module RubySpriter
|
|
|
208
220
|
def add_other_options(opts, options)
|
|
209
221
|
opts.separator "Other Options:"
|
|
210
222
|
|
|
223
|
+
opts.on("--overwrite", "Overwrite existing output files (default: create unique filenames)") do
|
|
224
|
+
options[:overwrite] = true
|
|
225
|
+
end
|
|
226
|
+
|
|
211
227
|
opts.on("--keep-temp", "Keep temporary files for debugging") do
|
|
212
228
|
options[:keep_temp] = true
|
|
213
229
|
end
|
|
@@ -68,7 +68,8 @@ module RubySpriter
|
|
|
68
68
|
|
|
69
69
|
def scale_image(input_file)
|
|
70
70
|
percent = options[:scale_percent]
|
|
71
|
-
|
|
71
|
+
desired_output = Utils::FileHelper.output_filename(input_file, "scaled-#{percent}pct")
|
|
72
|
+
output_file = Utils::FileHelper.ensure_unique_output(desired_output, overwrite: options[:overwrite])
|
|
72
73
|
|
|
73
74
|
Utils::OutputFormatter.indent("Scaling to #{percent}%...")
|
|
74
75
|
|
|
@@ -85,7 +86,8 @@ module RubySpriter
|
|
|
85
86
|
|
|
86
87
|
def remove_background(input_file)
|
|
87
88
|
method = options[:fuzzy_select] ? 'fuzzy' : 'global'
|
|
88
|
-
|
|
89
|
+
desired_output = Utils::FileHelper.output_filename(input_file, "nobg-#{method}")
|
|
90
|
+
output_file = Utils::FileHelper.ensure_unique_output(desired_output, overwrite: options[:overwrite])
|
|
89
91
|
|
|
90
92
|
Utils::OutputFormatter.indent("Removing background (#{method} select)...")
|
|
91
93
|
|
|
@@ -621,7 +623,8 @@ module RubySpriter
|
|
|
621
623
|
gain = options[:sharpen_gain] || 0.5
|
|
622
624
|
threshold = options[:sharpen_threshold] || 0.03
|
|
623
625
|
|
|
624
|
-
|
|
626
|
+
desired_output = Utils::FileHelper.output_filename(input_file, "sharpened")
|
|
627
|
+
output_file = Utils::FileHelper.ensure_unique_output(desired_output, overwrite: options[:overwrite])
|
|
625
628
|
|
|
626
629
|
Utils::OutputFormatter.indent("Applying unsharp mask (ImageMagick)...")
|
|
627
630
|
Utils::OutputFormatter.indent(" radius=#{radius}, gain=#{gain}, threshold=#{threshold}")
|
|
@@ -2,15 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require 'tmpdir'
|
|
5
|
+
require 'open3'
|
|
5
6
|
|
|
6
7
|
module RubySpriter
|
|
7
8
|
# Main orchestration processor
|
|
8
9
|
class Processor
|
|
9
|
-
attr_reader :options, :gimp_path
|
|
10
|
+
attr_reader :options, :gimp_path, :split_rows, :split_columns
|
|
11
|
+
|
|
12
|
+
# Valid ranges for numeric options
|
|
13
|
+
VALID_RANGES = {
|
|
14
|
+
frame_count: { min: 1, max: 10000, type: Integer },
|
|
15
|
+
columns: { min: 1, max: 100, type: Integer },
|
|
16
|
+
max_width: { min: 1, max: 1920, type: Integer },
|
|
17
|
+
scale_percent: { min: 1, max: 500, type: Integer },
|
|
18
|
+
grow_selection: { min: 0, max: 100, type: Integer },
|
|
19
|
+
sharpen_radius: { min: 0.1, max: 100.0, type: Float },
|
|
20
|
+
sharpen_gain: { min: 0.0, max: 10.0, type: Float },
|
|
21
|
+
sharpen_threshold: { min: 0.0, max: 1.0, type: Float },
|
|
22
|
+
bg_threshold: { min: 0.0, max: 100.0, type: Float }
|
|
23
|
+
}.freeze
|
|
10
24
|
|
|
11
25
|
def initialize(options = {})
|
|
12
26
|
@options = default_options.merge(options)
|
|
13
27
|
@gimp_path = nil
|
|
28
|
+
validate_numeric_options!
|
|
29
|
+
validate_split_option!
|
|
14
30
|
end
|
|
15
31
|
|
|
16
32
|
# Run the processing workflow
|
|
@@ -54,13 +70,17 @@ module RubySpriter
|
|
|
54
70
|
validate_columns: true,
|
|
55
71
|
temp_dir: nil,
|
|
56
72
|
keep_temp: false,
|
|
57
|
-
debug: false
|
|
73
|
+
debug: false,
|
|
74
|
+
overwrite: false,
|
|
75
|
+
save_frames: false,
|
|
76
|
+
split: nil,
|
|
77
|
+
override_md: false
|
|
58
78
|
}
|
|
59
79
|
end
|
|
60
80
|
|
|
61
81
|
def validate_options!
|
|
62
82
|
input_modes = [options[:video], options[:image], options[:consolidate], options[:verify]].compact
|
|
63
|
-
|
|
83
|
+
|
|
64
84
|
if input_modes.empty?
|
|
65
85
|
raise ValidationError, "Must specify --video, --image, --consolidate, or --verify"
|
|
66
86
|
end
|
|
@@ -70,6 +90,8 @@ module RubySpriter
|
|
|
70
90
|
end
|
|
71
91
|
|
|
72
92
|
validate_input_files!
|
|
93
|
+
validate_numeric_options!
|
|
94
|
+
validate_split_option!
|
|
73
95
|
end
|
|
74
96
|
|
|
75
97
|
def validate_input_files!
|
|
@@ -108,6 +130,53 @@ module RubySpriter
|
|
|
108
130
|
end
|
|
109
131
|
end
|
|
110
132
|
|
|
133
|
+
def validate_numeric_options!
|
|
134
|
+
VALID_RANGES.each do |option_name, range_config|
|
|
135
|
+
value = options[option_name]
|
|
136
|
+
|
|
137
|
+
# Skip validation if option is not set (nil)
|
|
138
|
+
next if value.nil?
|
|
139
|
+
|
|
140
|
+
min = range_config[:min]
|
|
141
|
+
max = range_config[:max]
|
|
142
|
+
|
|
143
|
+
# Validate that value is within range
|
|
144
|
+
if value < min || value > max
|
|
145
|
+
raise ValidationError, "#{option_name} must be between #{min} and #{max}, got: #{value}"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def validate_split_option!
|
|
151
|
+
return unless options[:split]
|
|
152
|
+
|
|
153
|
+
# Parse split format: R:C
|
|
154
|
+
unless options[:split] =~ /^\d+:\d+$/
|
|
155
|
+
raise ValidationError, "Invalid --split format. Use R:C (e.g., 4:4)"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
rows, columns = options[:split].split(':').map(&:to_i)
|
|
159
|
+
|
|
160
|
+
# Validate ranges
|
|
161
|
+
if rows < 1 || rows > 99
|
|
162
|
+
raise ValidationError, "rows must be between 1 and 99, got: #{rows}"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
if columns < 1 || columns > 99
|
|
166
|
+
raise ValidationError, "columns must be between 1 and 99, got: #{columns}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Validate total frames < 1000
|
|
170
|
+
total_frames = rows * columns
|
|
171
|
+
if total_frames >= 1000
|
|
172
|
+
raise ValidationError, "Total frames (#{total_frames}) must be less than 1000"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Store parsed values for later use
|
|
176
|
+
@split_rows = rows
|
|
177
|
+
@split_columns = columns
|
|
178
|
+
end
|
|
179
|
+
|
|
111
180
|
def check_dependencies!
|
|
112
181
|
checker = DependencyChecker.new(verbose: options[:debug])
|
|
113
182
|
results = checker.check_all
|
|
@@ -119,8 +188,8 @@ module RubySpriter
|
|
|
119
188
|
missing << tool unless results[tool][:available]
|
|
120
189
|
end
|
|
121
190
|
|
|
122
|
-
# GIMP only needed for
|
|
123
|
-
if
|
|
191
|
+
# GIMP only needed for scaling and background removal (not for sharpen-only)
|
|
192
|
+
if needs_gimp_specifically? && !results[:gimp][:available]
|
|
124
193
|
missing << :gimp
|
|
125
194
|
end
|
|
126
195
|
|
|
@@ -137,6 +206,10 @@ module RubySpriter
|
|
|
137
206
|
end
|
|
138
207
|
|
|
139
208
|
def needs_gimp?
|
|
209
|
+
options[:scale_percent] || options[:remove_bg] || options[:sharpen]
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def needs_gimp_specifically?
|
|
140
213
|
options[:scale_percent] || options[:remove_bg]
|
|
141
214
|
end
|
|
142
215
|
|
|
@@ -168,24 +241,45 @@ module RubySpriter
|
|
|
168
241
|
end
|
|
169
242
|
|
|
170
243
|
def execute_video_workflow
|
|
171
|
-
# Step 1:
|
|
244
|
+
# Step 1: Determine output filename
|
|
245
|
+
desired_output = options[:output] || Utils::FileHelper.spritesheet_filename(options[:video])
|
|
246
|
+
final_output = Utils::FileHelper.ensure_unique_output(desired_output, overwrite: options[:overwrite])
|
|
247
|
+
|
|
248
|
+
# Step 2: Convert video to spritesheet
|
|
172
249
|
video_processor = VideoProcessor.new(options)
|
|
173
250
|
result = video_processor.create_spritesheet(
|
|
174
251
|
options[:video],
|
|
175
|
-
|
|
252
|
+
final_output
|
|
176
253
|
)
|
|
177
254
|
|
|
178
255
|
working_file = result[:output_file]
|
|
256
|
+
intermediate_files = []
|
|
179
257
|
|
|
180
|
-
# Step
|
|
258
|
+
# Step 3: Apply GIMP processing if requested
|
|
181
259
|
if needs_gimp?
|
|
260
|
+
initial_file = working_file
|
|
182
261
|
working_file = process_with_gimp(working_file)
|
|
262
|
+
|
|
263
|
+
# Track intermediate files for cleanup (everything except initial and final)
|
|
264
|
+
if working_file != initial_file
|
|
265
|
+
intermediate_files = collect_intermediate_files(initial_file, working_file)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Step 4: Move to final output location if different
|
|
270
|
+
if final_output != working_file
|
|
271
|
+
FileUtils.cp(working_file, final_output)
|
|
272
|
+
# Add the GIMP output to intermediates if it's different from final
|
|
273
|
+
intermediate_files << working_file unless intermediate_files.include?(working_file)
|
|
274
|
+
working_file = final_output
|
|
183
275
|
end
|
|
184
276
|
|
|
185
|
-
# Step
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
277
|
+
# Step 5: Clean up intermediate files
|
|
278
|
+
cleanup_intermediate_files(intermediate_files)
|
|
279
|
+
|
|
280
|
+
# Step 6: Extract individual frames if requested
|
|
281
|
+
if options[:save_frames]
|
|
282
|
+
split_frames_from_spritesheet(working_file, result[:columns], result[:rows], result[:frames])
|
|
189
283
|
end
|
|
190
284
|
|
|
191
285
|
Utils::OutputFormatter.header("SUCCESS!")
|
|
@@ -196,16 +290,42 @@ module RubySpriter
|
|
|
196
290
|
|
|
197
291
|
def execute_image_workflow
|
|
198
292
|
working_file = options[:image]
|
|
293
|
+
intermediate_files = []
|
|
199
294
|
|
|
200
|
-
# Apply GIMP processing if requested
|
|
295
|
+
# Apply GIMP processing if requested (GimpProcessor handles uniqueness)
|
|
201
296
|
if needs_gimp?
|
|
297
|
+
initial_file = working_file
|
|
202
298
|
working_file = process_with_gimp(working_file)
|
|
299
|
+
|
|
300
|
+
# Track intermediate files for cleanup (everything except initial and final)
|
|
301
|
+
if working_file != initial_file
|
|
302
|
+
intermediate_files = collect_intermediate_files(initial_file, working_file)
|
|
303
|
+
end
|
|
203
304
|
end
|
|
204
305
|
|
|
205
|
-
# Move to final output location if specified
|
|
206
|
-
if options[:output]
|
|
207
|
-
|
|
208
|
-
working_file
|
|
306
|
+
# Move to final output location if user specified explicit --output
|
|
307
|
+
if options[:output]
|
|
308
|
+
final_output = Utils::FileHelper.ensure_unique_output(options[:output], overwrite: options[:overwrite])
|
|
309
|
+
if working_file != final_output
|
|
310
|
+
FileUtils.cp(working_file, final_output)
|
|
311
|
+
# Add the GIMP output to intermediates if it's different from final
|
|
312
|
+
intermediate_files << working_file unless intermediate_files.include?(working_file)
|
|
313
|
+
working_file = final_output
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Clean up intermediate files
|
|
318
|
+
cleanup_intermediate_files(intermediate_files)
|
|
319
|
+
|
|
320
|
+
# Determine if we should split the image into frames
|
|
321
|
+
should_split = options[:save_frames] || options[:split]
|
|
322
|
+
|
|
323
|
+
if should_split
|
|
324
|
+
# Determine rows, columns, and frames to use
|
|
325
|
+
rows, columns, frames = determine_split_parameters(working_file)
|
|
326
|
+
|
|
327
|
+
# Split the image into frames
|
|
328
|
+
split_frames_from_spritesheet(working_file, columns, rows, frames)
|
|
209
329
|
end
|
|
210
330
|
|
|
211
331
|
Utils::OutputFormatter.header("SUCCESS!")
|
|
@@ -220,10 +340,11 @@ module RubySpriter
|
|
|
220
340
|
|
|
221
341
|
def execute_consolidate_workflow
|
|
222
342
|
consolidator = Consolidator.new(options)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
343
|
+
|
|
344
|
+
desired_output = options[:output] || generate_consolidated_filename
|
|
345
|
+
final_output = Utils::FileHelper.ensure_unique_output(desired_output, overwrite: options[:overwrite])
|
|
346
|
+
|
|
347
|
+
result = consolidator.consolidate(options[:consolidate], final_output)
|
|
227
348
|
|
|
228
349
|
Utils::OutputFormatter.header("SUCCESS!")
|
|
229
350
|
Utils::OutputFormatter.success("Final output: #{result[:output_file]}")
|
|
@@ -237,8 +358,57 @@ module RubySpriter
|
|
|
237
358
|
end
|
|
238
359
|
|
|
239
360
|
def generate_consolidated_filename
|
|
240
|
-
|
|
241
|
-
|
|
361
|
+
"consolidated_spritesheet.png"
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def split_frames_from_spritesheet(spritesheet_file, columns, rows, frames)
|
|
365
|
+
# Determine frames directory based on spritesheet filename
|
|
366
|
+
spritesheet_basename = File.basename(spritesheet_file, '.*')
|
|
367
|
+
frames_dir = File.join(File.dirname(spritesheet_file), "#{spritesheet_basename}_frames")
|
|
368
|
+
|
|
369
|
+
# Split the spritesheet into individual frames
|
|
370
|
+
splitter = Utils::SpritesheetSplitter.new
|
|
371
|
+
splitter.split_into_frames(spritesheet_file, frames_dir, columns, rows, frames)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def collect_intermediate_files(initial_file, final_file)
|
|
375
|
+
# Find all files that were created during GIMP processing
|
|
376
|
+
# Pattern: initial_file + suffixes like -nobg-fuzzy, -scaled-40pct, etc.
|
|
377
|
+
# Note: output_filename uses DASH separator, not underscore
|
|
378
|
+
dir = File.dirname(initial_file)
|
|
379
|
+
basename = File.basename(initial_file, '.*')
|
|
380
|
+
ext = File.extname(initial_file)
|
|
381
|
+
|
|
382
|
+
# Get all PNG files in the directory that start with the basename and have a dash
|
|
383
|
+
pattern = File.join(dir, "#{basename}-*#{ext}")
|
|
384
|
+
intermediate_files = Dir.glob(pattern)
|
|
385
|
+
|
|
386
|
+
# Normalize paths for comparison (Windows compatibility)
|
|
387
|
+
initial_normalized = File.expand_path(initial_file)
|
|
388
|
+
final_normalized = File.expand_path(final_file)
|
|
389
|
+
|
|
390
|
+
# Exclude the initial and final files
|
|
391
|
+
intermediate_files.reject do |f|
|
|
392
|
+
f_normalized = File.expand_path(f)
|
|
393
|
+
f_normalized == initial_normalized || f_normalized == final_normalized
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def cleanup_intermediate_files(files)
|
|
398
|
+
return if files.empty?
|
|
399
|
+
|
|
400
|
+
if options[:debug]
|
|
401
|
+
Utils::OutputFormatter.note("Cleaning up #{files.length} intermediate file(s):")
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
files.each do |file|
|
|
405
|
+
if File.exist?(file)
|
|
406
|
+
File.delete(file)
|
|
407
|
+
if options[:debug]
|
|
408
|
+
Utils::OutputFormatter.indent("Deleted: #{File.basename(file)}")
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|
|
242
412
|
end
|
|
243
413
|
|
|
244
414
|
def cleanup
|
|
@@ -247,5 +417,65 @@ module RubySpriter
|
|
|
247
417
|
Utils::OutputFormatter.note("Cleaned up temporary files") if options[:debug]
|
|
248
418
|
end
|
|
249
419
|
end
|
|
420
|
+
|
|
421
|
+
def determine_split_parameters(image_file)
|
|
422
|
+
metadata = MetadataManager.read(image_file)
|
|
423
|
+
|
|
424
|
+
# Check if we have metadata
|
|
425
|
+
if metadata && metadata[:columns] && metadata[:rows] && metadata[:frames]
|
|
426
|
+
# Metadata exists
|
|
427
|
+
if options[:split] && !options[:override_md]
|
|
428
|
+
# Warn user that split values will be ignored
|
|
429
|
+
Utils::OutputFormatter.note("Image has metadata (#{metadata[:rows]}ร#{metadata[:columns]}). Your --split values will be ignored. Use --override-md to override.")
|
|
430
|
+
return [metadata[:rows], metadata[:columns], metadata[:frames]]
|
|
431
|
+
elsif options[:split] && options[:override_md]
|
|
432
|
+
# Use user's split values
|
|
433
|
+
frames = @split_rows * @split_columns
|
|
434
|
+
validate_image_dimensions(image_file, @split_rows, @split_columns)
|
|
435
|
+
return [@split_rows, @split_columns, frames]
|
|
436
|
+
else
|
|
437
|
+
# Use metadata
|
|
438
|
+
return [metadata[:rows], metadata[:columns], metadata[:frames]]
|
|
439
|
+
end
|
|
440
|
+
else
|
|
441
|
+
# No metadata
|
|
442
|
+
if options[:split]
|
|
443
|
+
# Use user's split values
|
|
444
|
+
frames = @split_rows * @split_columns
|
|
445
|
+
validate_image_dimensions(image_file, @split_rows, @split_columns)
|
|
446
|
+
return [@split_rows, @split_columns, frames]
|
|
447
|
+
else
|
|
448
|
+
# Error: no metadata and no split option
|
|
449
|
+
raise ValidationError, "Image has no metadata. Please provide --split R:C"
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def validate_image_dimensions(image_file, rows, columns)
|
|
455
|
+
# Get image dimensions using ImageMagick
|
|
456
|
+
cmd = [
|
|
457
|
+
'magick',
|
|
458
|
+
'identify',
|
|
459
|
+
'-format', '%wx%h',
|
|
460
|
+
Utils::PathHelper.quote_path(image_file)
|
|
461
|
+
].join(' ')
|
|
462
|
+
|
|
463
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
464
|
+
|
|
465
|
+
unless status.success?
|
|
466
|
+
raise ProcessingError, "Could not get image dimensions: #{stderr}"
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
width, height = stdout.strip.split('x').map(&:to_i)
|
|
470
|
+
|
|
471
|
+
# Check if dimensions divide evenly
|
|
472
|
+
unless width % columns == 0
|
|
473
|
+
raise ValidationError, "Image width (#{width}) not evenly divisible by #{columns} columns"
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
unless height % rows == 0
|
|
477
|
+
raise ValidationError, "Image height (#{height}) not evenly divisible by #{rows} rows"
|
|
478
|
+
end
|
|
479
|
+
end
|
|
250
480
|
end
|
|
251
481
|
end
|
|
@@ -51,6 +51,31 @@ module RubySpriter
|
|
|
51
51
|
validate_exists!(path)
|
|
52
52
|
raise ValidationError, "File not readable: #{path}" unless File.readable?(path)
|
|
53
53
|
end
|
|
54
|
+
|
|
55
|
+
# Generate unique filename by adding timestamp if file exists
|
|
56
|
+
# @param path [String] Original file path
|
|
57
|
+
# @return [String] Unique file path (adds timestamp if original exists)
|
|
58
|
+
def unique_filename(path)
|
|
59
|
+
return path unless File.exist?(path)
|
|
60
|
+
|
|
61
|
+
dir = File.dirname(path)
|
|
62
|
+
ext = File.extname(path)
|
|
63
|
+
basename = File.basename(path, ext)
|
|
64
|
+
timestamp = Time.now.strftime('%Y%m%d_%H%M%S_%3N') # Include milliseconds
|
|
65
|
+
|
|
66
|
+
File.join(dir, "#{basename}_#{timestamp}#{ext}")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Ensure output filename is unique based on overwrite option
|
|
70
|
+
# @param path [String] Desired output path
|
|
71
|
+
# @param overwrite [Boolean] If true, return original path; if false, make unique
|
|
72
|
+
# @return [String] Output path (unique if overwrite is false and file exists)
|
|
73
|
+
def ensure_unique_output(path, overwrite: false)
|
|
74
|
+
return path if overwrite
|
|
75
|
+
return path unless File.exist?(path)
|
|
76
|
+
|
|
77
|
+
unique_filename(path)
|
|
78
|
+
end
|
|
54
79
|
end
|
|
55
80
|
end
|
|
56
81
|
end
|