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
         |