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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f794b569ca3be84a3e33923a645ee3f98a8771d12d1e9befc5bc9f63241d079d
4
- data.tar.gz: b8e2078fa01d7095ec071c83e6d0e17683f86b2653a8f610b50925084e9d4984
3
+ metadata.gz: b6e4b3befadf222f9fe4a13b50eb16a5a39124c45f5dad2769f7c4f9153aae4f
4
+ data.tar.gz: 9f13e8d5da4c706a3d834f5a8d125ee46a1d7f04e5a643c90bda451bdb7f6e9c
5
5
  SHA512:
6
- metadata.gz: 7fea2f1769c71ae55b040b41c6f1dd29f8d1c384eda0606464fee0c26892d48463586f541a484395c3368170484028488bc3bc6b7f1820f20132ec004fc5ae81
7
- data.tar.gz: a751cdce97247fc81627bb3a0d4628c5c560885ce4df8eed744c9923bdd8cb86842c07363e02cc66ff94f2792ff8a50504b980ba7035e559014e1bc24f692985
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.5
1
+ # Ruby Spriter v0.6.6
2
2
 
3
3
  [![Ruby](https://img.shields.io/badge/Ruby-2.7+-red.svg)](https://www.ruby-lang.org/)
4
4
  [![License](https://img.shields.io/badge/License-MIT-blue.svg)](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.5.gem
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:
@@ -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
- output_file = Utils::FileHelper.output_filename(input_file, "scaled-#{percent}pct")
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
- output_file = Utils::FileHelper.output_filename(input_file, "nobg-#{method}")
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
- output_file = Utils::FileHelper.output_filename(input_file, "sharpened")
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 image processing
123
- if needs_gimp? && !results[:gimp][:available]
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: Convert video to spritesheet
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
- options[:output] || Utils::FileHelper.spritesheet_filename(options[:video])
252
+ final_output
176
253
  )
177
254
 
178
255
  working_file = result[:output_file]
256
+ intermediate_files = []
179
257
 
180
- # Step 2: Apply GIMP processing if requested
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 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]
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] && working_file != options[:output]
207
- FileUtils.cp(working_file, options[:output])
208
- working_file = options[:output]
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
- output_file = options[:output] || generate_consolidated_filename
225
-
226
- result = consolidator.consolidate(options[:consolidate], output_file)
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
- timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
241
- "consolidated_spritesheet_#{timestamp}.png"
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