ruby_spriter 0.6.6 → 0.6.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +257 -0
- data/README.md +384 -33
- data/lib/ruby_spriter/batch_processor.rb +214 -0
- data/lib/ruby_spriter/cli.rb +355 -8
- data/lib/ruby_spriter/compression_manager.rb +101 -0
- data/lib/ruby_spriter/consolidator.rb +33 -0
- data/lib/ruby_spriter/dependency_checker.rb +65 -15
- data/lib/ruby_spriter/gimp_processor.rb +395 -4
- data/lib/ruby_spriter/platform.rb +56 -1
- data/lib/ruby_spriter/processor.rb +419 -9
- data/lib/ruby_spriter/version.rb +2 -2
- data/lib/ruby_spriter.rb +2 -0
- data/spec/ruby_spriter/batch_processor_spec.rb +200 -0
- data/spec/ruby_spriter/cli_spec.rb +387 -0
- data/spec/ruby_spriter/compression_manager_spec.rb +157 -0
- data/spec/ruby_spriter/consolidator_spec.rb +163 -0
- data/spec/ruby_spriter/platform_spec.rb +11 -1
- data/spec/ruby_spriter/processor_spec.rb +350 -0
- metadata +6 -2
|
@@ -25,8 +25,11 @@ module RubySpriter
|
|
|
25
25
|
def initialize(options = {})
|
|
26
26
|
@options = default_options.merge(options)
|
|
27
27
|
@gimp_path = nil
|
|
28
|
+
@gimp_version = nil
|
|
28
29
|
validate_numeric_options!
|
|
29
30
|
validate_split_option!
|
|
31
|
+
validate_extract_option!
|
|
32
|
+
validate_add_meta_option!
|
|
30
33
|
end
|
|
31
34
|
|
|
32
35
|
# Run the processing workflow
|
|
@@ -74,24 +77,51 @@ module RubySpriter
|
|
|
74
77
|
overwrite: false,
|
|
75
78
|
save_frames: false,
|
|
76
79
|
split: nil,
|
|
77
|
-
override_md: false
|
|
80
|
+
override_md: false,
|
|
81
|
+
extract: nil,
|
|
82
|
+
add_meta: nil,
|
|
83
|
+
overwrite_meta: false
|
|
78
84
|
}
|
|
79
85
|
end
|
|
80
86
|
|
|
81
87
|
def validate_options!
|
|
82
|
-
input_modes = [options[:video], options[:image], options[:
|
|
88
|
+
input_modes = [options[:video], options[:image], options[:consolidate_mode], options[:verify], options[:batch]].compact
|
|
83
89
|
|
|
84
90
|
if input_modes.empty?
|
|
85
|
-
raise ValidationError, "Must specify --video, --image, --consolidate, or --
|
|
91
|
+
raise ValidationError, "Must specify --video, --image, --consolidate, --verify, or --batch"
|
|
86
92
|
end
|
|
87
93
|
|
|
88
94
|
if input_modes.length > 1
|
|
89
95
|
raise ValidationError, "Cannot use multiple input modes together. Choose one."
|
|
90
96
|
end
|
|
91
97
|
|
|
98
|
+
validate_consolidate_options!
|
|
92
99
|
validate_input_files!
|
|
93
100
|
validate_numeric_options!
|
|
94
101
|
validate_split_option!
|
|
102
|
+
validate_extract_option!
|
|
103
|
+
validate_add_meta_option!
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def validate_consolidate_options!
|
|
107
|
+
return unless options[:consolidate_mode]
|
|
108
|
+
|
|
109
|
+
# Check for mutual exclusivity between file list and directory
|
|
110
|
+
if options[:consolidate] && options[:dir]
|
|
111
|
+
raise ValidationError, "Cannot use --dir with comma-separated file list for --consolidate. Choose one method."
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Require either file list or directory
|
|
115
|
+
unless options[:consolidate] || options[:dir]
|
|
116
|
+
raise ValidationError, "--consolidate requires either comma-separated files or --dir option"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Validate directory if using directory mode
|
|
120
|
+
if options[:dir] && !options[:consolidate]
|
|
121
|
+
unless File.directory?(options[:dir])
|
|
122
|
+
raise ValidationError, "Directory not found: #{options[:dir]}"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
95
125
|
end
|
|
96
126
|
|
|
97
127
|
def validate_input_files!
|
|
@@ -177,6 +207,119 @@ module RubySpriter
|
|
|
177
207
|
@split_columns = columns
|
|
178
208
|
end
|
|
179
209
|
|
|
210
|
+
def validate_extract_option!
|
|
211
|
+
return unless options[:extract]
|
|
212
|
+
|
|
213
|
+
# Parse extract format: comma-separated integers (allow negatives for better error messages)
|
|
214
|
+
unless options[:extract] =~ /^-?\d+(,-?\d+)*$/
|
|
215
|
+
raise ValidationError, "Invalid --extract format. Use comma-separated frame numbers (e.g., 1,2,4,5,8)"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Parse frame numbers
|
|
219
|
+
frame_numbers = options[:extract].split(',').map(&:to_i)
|
|
220
|
+
|
|
221
|
+
# Validate minimum 2 frames
|
|
222
|
+
if frame_numbers.length < 2
|
|
223
|
+
raise ValidationError, "--extract requires at least 2 frames, got: #{frame_numbers.length}"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Validate frame numbers are 1-indexed (no 0 or negative)
|
|
227
|
+
invalid_frames = frame_numbers.select { |n| n <= 0 }
|
|
228
|
+
if invalid_frames.any?
|
|
229
|
+
raise ValidationError, "Frame numbers must be 1-indexed (positive integers), got invalid: #{invalid_frames.join(', ')}"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Check for metadata (required for extraction)
|
|
233
|
+
return unless options[:image] # Only validate bounds if we have an image path
|
|
234
|
+
|
|
235
|
+
image_file = options[:image]
|
|
236
|
+
metadata = MetadataManager.read(image_file)
|
|
237
|
+
|
|
238
|
+
unless metadata
|
|
239
|
+
raise ValidationError, "Image has no metadata. Cannot extract frames without knowing the grid layout. Use --add-meta first."
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Validate frame numbers are within bounds
|
|
243
|
+
total_frames = metadata[:frames]
|
|
244
|
+
out_of_bounds = frame_numbers.select { |n| n > total_frames }
|
|
245
|
+
if out_of_bounds.any?
|
|
246
|
+
first_oob = out_of_bounds.first
|
|
247
|
+
raise ValidationError, "Frame #{first_oob} is out of bounds (image only has #{total_frames} frames)"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Set default columns if not specified
|
|
251
|
+
options[:columns] ||= 4
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def validate_add_meta_option!
|
|
255
|
+
return unless options[:add_meta]
|
|
256
|
+
|
|
257
|
+
# Parse add-meta format: R:C
|
|
258
|
+
unless options[:add_meta] =~ /^\d+:\d+$/
|
|
259
|
+
raise ValidationError, "Invalid --add-meta format. Use R:C (e.g., 4:4)"
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
rows, columns = options[:add_meta].split(':').map(&:to_i)
|
|
263
|
+
|
|
264
|
+
# Validate ranges
|
|
265
|
+
if rows < 1 || rows > 99
|
|
266
|
+
raise ValidationError, "rows must be between 1 and 99, got: #{rows}"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
if columns < 1 || columns > 99
|
|
270
|
+
raise ValidationError, "columns must be between 1 and 99, got: #{columns}"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Validate total frames < 1000
|
|
274
|
+
total_frames = rows * columns
|
|
275
|
+
if total_frames >= 1000
|
|
276
|
+
raise ValidationError, "Total frames (#{total_frames}) must be less than 1000"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Check if we need to validate against image file
|
|
280
|
+
return unless options[:image]
|
|
281
|
+
|
|
282
|
+
image_file = options[:image]
|
|
283
|
+
metadata = MetadataManager.read(image_file)
|
|
284
|
+
|
|
285
|
+
# Check for existing metadata
|
|
286
|
+
if metadata && !options[:overwrite_meta]
|
|
287
|
+
raise ValidationError, "Image already has spritesheet metadata. Use --overwrite-meta to replace it."
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Validate image dimensions divide evenly by grid
|
|
291
|
+
dimensions = get_image_dimensions(image_file)
|
|
292
|
+
tile_width = dimensions[:width] / columns.to_f
|
|
293
|
+
tile_height = dimensions[:height] / rows.to_f
|
|
294
|
+
|
|
295
|
+
unless tile_width == tile_width.to_i && tile_height == tile_height.to_i
|
|
296
|
+
raise ValidationError, "Image dimensions (#{dimensions[:width]}x#{dimensions[:height]}) must divide evenly by grid (#{rows}x#{columns}). Expected frame size: #{tile_width}x#{tile_height}"
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Validate custom frame count doesn't exceed grid size
|
|
300
|
+
if options[:frame_count] && options[:frame_count] > total_frames
|
|
301
|
+
raise ValidationError, "Frame count (#{options[:frame_count]}) exceeds grid size (#{total_frames})"
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def get_image_dimensions(image_file)
|
|
306
|
+
cmd = [
|
|
307
|
+
'magick',
|
|
308
|
+
'identify',
|
|
309
|
+
'-format', '%wx%h',
|
|
310
|
+
Utils::PathHelper.quote_path(image_file)
|
|
311
|
+
].join(' ')
|
|
312
|
+
|
|
313
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
314
|
+
|
|
315
|
+
unless status.success?
|
|
316
|
+
raise ProcessingError, "Could not get image dimensions: #{stderr}"
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
width, height = stdout.strip.split('x').map(&:to_i)
|
|
320
|
+
{ width: width, height: height }
|
|
321
|
+
end
|
|
322
|
+
|
|
180
323
|
def check_dependencies!
|
|
181
324
|
checker = DependencyChecker.new(verbose: options[:debug])
|
|
182
325
|
results = checker.check_all
|
|
@@ -198,7 +341,10 @@ module RubySpriter
|
|
|
198
341
|
raise DependencyError, "Missing required dependencies: #{missing.join(', ')}"
|
|
199
342
|
end
|
|
200
343
|
|
|
201
|
-
|
|
344
|
+
if results[:gimp][:available]
|
|
345
|
+
@gimp_path = checker.gimp_path
|
|
346
|
+
@gimp_version = checker.gimp_version
|
|
347
|
+
end
|
|
202
348
|
|
|
203
349
|
if options[:debug]
|
|
204
350
|
checker.print_report
|
|
@@ -231,7 +377,9 @@ module RubySpriter
|
|
|
231
377
|
return { mode: :verify, file: options[:verify] }
|
|
232
378
|
end
|
|
233
379
|
|
|
234
|
-
if options[:
|
|
380
|
+
if options[:batch]
|
|
381
|
+
return execute_batch_workflow
|
|
382
|
+
elsif options[:consolidate_mode]
|
|
235
383
|
return execute_consolidate_workflow
|
|
236
384
|
elsif options[:image]
|
|
237
385
|
return execute_image_workflow
|
|
@@ -277,7 +425,12 @@ module RubySpriter
|
|
|
277
425
|
# Step 5: Clean up intermediate files
|
|
278
426
|
cleanup_intermediate_files(intermediate_files)
|
|
279
427
|
|
|
280
|
-
# Step 6:
|
|
428
|
+
# Step 6: Apply max compression if requested
|
|
429
|
+
if options[:max_compress]
|
|
430
|
+
working_file = apply_max_compression(working_file)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# Step 7: Extract individual frames if requested
|
|
281
434
|
if options[:save_frames]
|
|
282
435
|
split_frames_from_spritesheet(working_file, result[:columns], result[:rows], result[:frames])
|
|
283
436
|
end
|
|
@@ -292,6 +445,16 @@ module RubySpriter
|
|
|
292
445
|
working_file = options[:image]
|
|
293
446
|
intermediate_files = []
|
|
294
447
|
|
|
448
|
+
# Handle metadata addition workflow first
|
|
449
|
+
if options[:add_meta]
|
|
450
|
+
return execute_add_meta_workflow
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Handle frame extraction workflow
|
|
454
|
+
if options[:extract]
|
|
455
|
+
return execute_extract_workflow
|
|
456
|
+
end
|
|
457
|
+
|
|
295
458
|
# Apply GIMP processing if requested (GimpProcessor handles uniqueness)
|
|
296
459
|
if needs_gimp?
|
|
297
460
|
initial_file = working_file
|
|
@@ -317,6 +480,11 @@ module RubySpriter
|
|
|
317
480
|
# Clean up intermediate files
|
|
318
481
|
cleanup_intermediate_files(intermediate_files)
|
|
319
482
|
|
|
483
|
+
# Apply max compression if requested
|
|
484
|
+
if options[:max_compress]
|
|
485
|
+
working_file = apply_max_compression(working_file)
|
|
486
|
+
end
|
|
487
|
+
|
|
320
488
|
# Determine if we should split the image into frames
|
|
321
489
|
should_split = options[:save_frames] || options[:split]
|
|
322
490
|
|
|
@@ -338,13 +506,192 @@ module RubySpriter
|
|
|
338
506
|
}
|
|
339
507
|
end
|
|
340
508
|
|
|
509
|
+
def execute_extract_workflow
|
|
510
|
+
input_file = options[:image]
|
|
511
|
+
metadata = MetadataManager.read(input_file)
|
|
512
|
+
frame_numbers = options[:extract].split(',').map(&:to_i)
|
|
513
|
+
columns = options[:columns]
|
|
514
|
+
|
|
515
|
+
Utils::OutputFormatter.header("Frame Extraction")
|
|
516
|
+
Utils::OutputFormatter.indent("Input: #{input_file}")
|
|
517
|
+
Utils::OutputFormatter.indent("Frames to extract: #{frame_numbers.join(', ')}")
|
|
518
|
+
Utils::OutputFormatter.indent("Output columns: #{columns}")
|
|
519
|
+
|
|
520
|
+
# Step 1: Extract all frames to temp directory
|
|
521
|
+
temp_frames_dir = File.join(options[:temp_dir], 'extracted_frames')
|
|
522
|
+
splitter = Utils::SpritesheetSplitter.new
|
|
523
|
+
splitter.split_into_frames(input_file, temp_frames_dir, metadata[:columns], metadata[:rows], metadata[:frames])
|
|
524
|
+
|
|
525
|
+
# Step 2: Keep only requested frames, delete the rest
|
|
526
|
+
spritesheet_basename = File.basename(input_file, '.*')
|
|
527
|
+
all_frame_files = Dir.glob(File.join(temp_frames_dir, "FR*_#{spritesheet_basename}.png")).sort
|
|
528
|
+
requested_frame_files = frame_numbers.map do |frame_num|
|
|
529
|
+
# Frame files are named FR001, FR002, etc. (1-indexed)
|
|
530
|
+
File.join(temp_frames_dir, "FR#{format('%03d', frame_num)}_#{spritesheet_basename}.png")
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
# Delete unwanted frames
|
|
534
|
+
(all_frame_files - requested_frame_files).each { |f| FileUtils.rm_f(f) }
|
|
535
|
+
|
|
536
|
+
Utils::OutputFormatter.indent("Kept #{requested_frame_files.length} frames, deleted #{all_frame_files.length - requested_frame_files.length} frames")
|
|
537
|
+
|
|
538
|
+
# Step 3: Reassemble into new spritesheet
|
|
539
|
+
Utils::OutputFormatter.header("Reassembling Spritesheet")
|
|
540
|
+
reassembled_file = File.join(options[:temp_dir], "reassembled_#{spritesheet_basename}.png")
|
|
541
|
+
reassemble_frames(requested_frame_files, reassembled_file, columns)
|
|
542
|
+
|
|
543
|
+
working_file = reassembled_file
|
|
544
|
+
intermediate_files = []
|
|
545
|
+
|
|
546
|
+
# Step 4: Apply GIMP processing if requested
|
|
547
|
+
if needs_gimp?
|
|
548
|
+
initial_file = working_file
|
|
549
|
+
working_file = process_with_gimp(working_file)
|
|
550
|
+
|
|
551
|
+
if working_file != initial_file
|
|
552
|
+
intermediate_files = collect_intermediate_files(initial_file, working_file)
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# Step 5: Determine final output filename
|
|
557
|
+
if options[:output]
|
|
558
|
+
final_output = Utils::FileHelper.ensure_unique_output(options[:output], overwrite: options[:overwrite])
|
|
559
|
+
else
|
|
560
|
+
# Auto-generate output filename with _extracted suffix
|
|
561
|
+
base = File.basename(input_file, '.*')
|
|
562
|
+
ext = File.extname(input_file)
|
|
563
|
+
desired_output = File.join(File.dirname(input_file), "#{base}_extracted#{ext}")
|
|
564
|
+
final_output = Utils::FileHelper.ensure_unique_output(desired_output, overwrite: options[:overwrite])
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
# Step 6: Copy to final output
|
|
568
|
+
FileUtils.cp(working_file, final_output)
|
|
569
|
+
working_file = final_output
|
|
570
|
+
|
|
571
|
+
# Step 7: Clean up intermediate files
|
|
572
|
+
cleanup_intermediate_files(intermediate_files)
|
|
573
|
+
|
|
574
|
+
# Step 8: Apply max compression if requested
|
|
575
|
+
if options[:max_compress]
|
|
576
|
+
working_file = apply_max_compression(working_file)
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# Step 9: Optionally save individual frames
|
|
580
|
+
if options[:save_frames]
|
|
581
|
+
frames_output_dir = File.join(File.dirname(working_file), "#{File.basename(working_file, '.*')}_frames")
|
|
582
|
+
FileUtils.mkdir_p(frames_output_dir)
|
|
583
|
+
requested_frame_files.each_with_index do |frame_file, idx|
|
|
584
|
+
frame_num = frame_numbers[idx]
|
|
585
|
+
dest = File.join(frames_output_dir, "FR#{format('%03d', frame_num)}_#{spritesheet_basename}.png")
|
|
586
|
+
FileUtils.cp(frame_file, dest)
|
|
587
|
+
end
|
|
588
|
+
Utils::OutputFormatter.indent("Saved #{requested_frame_files.length} frames to: #{frames_output_dir}")
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
Utils::OutputFormatter.header("SUCCESS!")
|
|
592
|
+
Utils::OutputFormatter.success("Extracted spritesheet: #{working_file}")
|
|
593
|
+
|
|
594
|
+
{
|
|
595
|
+
mode: :extract,
|
|
596
|
+
input_file: input_file,
|
|
597
|
+
output_file: working_file,
|
|
598
|
+
frames_extracted: frame_numbers.length,
|
|
599
|
+
columns: columns
|
|
600
|
+
}
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def execute_add_meta_workflow
|
|
604
|
+
input_file = options[:image]
|
|
605
|
+
rows, columns = options[:add_meta].split(':').map(&:to_i)
|
|
606
|
+
|
|
607
|
+
# Determine frame count
|
|
608
|
+
frame_count = if options[:frame_count]
|
|
609
|
+
options[:frame_count]
|
|
610
|
+
else
|
|
611
|
+
rows * columns
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
Utils::OutputFormatter.header("Adding Metadata")
|
|
615
|
+
Utils::OutputFormatter.indent("Input: #{input_file}")
|
|
616
|
+
Utils::OutputFormatter.indent("Grid: #{rows}×#{columns} (#{frame_count} frames)")
|
|
617
|
+
|
|
618
|
+
# Determine output file
|
|
619
|
+
if options[:output]
|
|
620
|
+
# User specified explicit output
|
|
621
|
+
output_file = Utils::FileHelper.ensure_unique_output(options[:output], overwrite: options[:overwrite])
|
|
622
|
+
|
|
623
|
+
# Copy input to output
|
|
624
|
+
FileUtils.cp(input_file, output_file)
|
|
625
|
+
Utils::OutputFormatter.indent("Copied to: #{output_file}")
|
|
626
|
+
else
|
|
627
|
+
# In-place modification
|
|
628
|
+
if options[:overwrite]
|
|
629
|
+
output_file = input_file
|
|
630
|
+
Utils::OutputFormatter.indent("Modifying in-place (--overwrite specified)")
|
|
631
|
+
else
|
|
632
|
+
# Create unique filename
|
|
633
|
+
output_file = Utils::FileHelper.ensure_unique_output(input_file, overwrite: false)
|
|
634
|
+
FileUtils.cp(input_file, output_file)
|
|
635
|
+
Utils::OutputFormatter.indent("Created: #{output_file}")
|
|
636
|
+
end
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
# Embed metadata
|
|
640
|
+
MetadataManager.embed(output_file, columns, rows, frame_count)
|
|
641
|
+
Utils::OutputFormatter.indent("📝 Metadata embedded: #{columns}×#{rows} grid (#{frame_count} frames)")
|
|
642
|
+
|
|
643
|
+
Utils::OutputFormatter.header("SUCCESS!")
|
|
644
|
+
Utils::OutputFormatter.success("Metadata added to: #{output_file}")
|
|
645
|
+
|
|
646
|
+
{
|
|
647
|
+
mode: :add_meta,
|
|
648
|
+
input_file: input_file,
|
|
649
|
+
output_file: output_file,
|
|
650
|
+
columns: columns,
|
|
651
|
+
rows: rows,
|
|
652
|
+
frames: frame_count
|
|
653
|
+
}
|
|
654
|
+
end
|
|
655
|
+
|
|
341
656
|
def execute_consolidate_workflow
|
|
342
657
|
consolidator = Consolidator.new(options)
|
|
343
658
|
|
|
344
|
-
|
|
659
|
+
# Determine file list: either from command line or from directory
|
|
660
|
+
files_to_consolidate = if options[:dir] && !options[:consolidate]
|
|
661
|
+
# Directory-based consolidation
|
|
662
|
+
consolidator.find_spritesheets_in_directory(options[:dir])
|
|
663
|
+
else
|
|
664
|
+
# File list consolidation
|
|
665
|
+
options[:consolidate]
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
# Determine output filename and directory
|
|
669
|
+
if options[:dir] && !options[:consolidate]
|
|
670
|
+
# Directory mode: output to dir or outputdir
|
|
671
|
+
output_dir = options[:outputdir] || options[:dir]
|
|
672
|
+
desired_output = if options[:output]
|
|
673
|
+
File.join(output_dir, File.basename(options[:output]))
|
|
674
|
+
else
|
|
675
|
+
File.join(output_dir, generate_consolidated_filename)
|
|
676
|
+
end
|
|
677
|
+
else
|
|
678
|
+
# File list mode: use current directory behavior
|
|
679
|
+
if options[:outputdir]
|
|
680
|
+
desired_output = File.join(options[:outputdir], options[:output] || generate_consolidated_filename)
|
|
681
|
+
else
|
|
682
|
+
desired_output = options[:output] || generate_consolidated_filename
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
|
|
345
686
|
final_output = Utils::FileHelper.ensure_unique_output(desired_output, overwrite: options[:overwrite])
|
|
346
687
|
|
|
347
|
-
result = consolidator.consolidate(
|
|
688
|
+
result = consolidator.consolidate(files_to_consolidate, final_output)
|
|
689
|
+
|
|
690
|
+
# Apply max compression if requested
|
|
691
|
+
if options[:max_compress]
|
|
692
|
+
final_output = apply_max_compression(result[:output_file])
|
|
693
|
+
result[:output_file] = final_output
|
|
694
|
+
end
|
|
348
695
|
|
|
349
696
|
Utils::OutputFormatter.header("SUCCESS!")
|
|
350
697
|
Utils::OutputFormatter.success("Final output: #{result[:output_file]}")
|
|
@@ -352,8 +699,16 @@ module RubySpriter
|
|
|
352
699
|
result.merge(mode: :consolidate)
|
|
353
700
|
end
|
|
354
701
|
|
|
702
|
+
def execute_batch_workflow
|
|
703
|
+
batch_processor = BatchProcessor.new(options)
|
|
704
|
+
result = batch_processor.process
|
|
705
|
+
|
|
706
|
+
result.merge(mode: :batch)
|
|
707
|
+
end
|
|
708
|
+
|
|
355
709
|
def process_with_gimp(input_file)
|
|
356
|
-
|
|
710
|
+
gimp_options = options.merge(gimp_version: @gimp_version)
|
|
711
|
+
gimp_processor = GimpProcessor.new(@gimp_path, gimp_options)
|
|
357
712
|
gimp_processor.process(input_file)
|
|
358
713
|
end
|
|
359
714
|
|
|
@@ -371,6 +726,38 @@ module RubySpriter
|
|
|
371
726
|
splitter.split_into_frames(spritesheet_file, frames_dir, columns, rows, frames)
|
|
372
727
|
end
|
|
373
728
|
|
|
729
|
+
def reassemble_frames(frame_files, output_file, columns)
|
|
730
|
+
# Calculate rows needed for the specified columns
|
|
731
|
+
total_frames = frame_files.length
|
|
732
|
+
rows = (total_frames.to_f / columns).ceil
|
|
733
|
+
|
|
734
|
+
Utils::OutputFormatter.indent("Layout: #{columns}×#{rows} grid (#{total_frames} frames)")
|
|
735
|
+
|
|
736
|
+
# Use ImageMagick montage to create spritesheet
|
|
737
|
+
# Montage arranges images in a grid
|
|
738
|
+
cmd = [
|
|
739
|
+
'magick',
|
|
740
|
+
'montage',
|
|
741
|
+
frame_files.map { |f| Utils::PathHelper.quote_path(f) }.join(' '),
|
|
742
|
+
'-tile', "#{columns}x#{rows}",
|
|
743
|
+
'-geometry', '+0+0', # No spacing between tiles
|
|
744
|
+
'-background', 'none', # Transparent background
|
|
745
|
+
Utils::PathHelper.quote_path(output_file)
|
|
746
|
+
].join(' ')
|
|
747
|
+
|
|
748
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
749
|
+
|
|
750
|
+
unless status.success?
|
|
751
|
+
raise ProcessingError, "Failed to reassemble frames: #{stderr}"
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
# Embed metadata in the reassembled spritesheet
|
|
755
|
+
MetadataManager.embed(output_file, columns, rows, total_frames)
|
|
756
|
+
|
|
757
|
+
Utils::OutputFormatter.indent("✅ Reassembled into #{columns}×#{rows} spritesheet")
|
|
758
|
+
Utils::OutputFormatter.indent("📝 Metadata embedded: #{columns}×#{rows} grid (#{total_frames} frames)")
|
|
759
|
+
end
|
|
760
|
+
|
|
374
761
|
def collect_intermediate_files(initial_file, final_file)
|
|
375
762
|
# Find all files that were created during GIMP processing
|
|
376
763
|
# Pattern: initial_file + suffixes like -nobg-fuzzy, -scaled-40pct, etc.
|
|
@@ -477,5 +864,28 @@ module RubySpriter
|
|
|
477
864
|
raise ValidationError, "Image height (#{height}) not evenly divisible by #{rows} rows"
|
|
478
865
|
end
|
|
479
866
|
end
|
|
867
|
+
|
|
868
|
+
def apply_max_compression(file)
|
|
869
|
+
Utils::OutputFormatter.note("Applying maximum compression...")
|
|
870
|
+
|
|
871
|
+
original_size = File.size(file)
|
|
872
|
+
temp_file = file.gsub('.png', '_compressed_temp.png')
|
|
873
|
+
|
|
874
|
+
CompressionManager.compress_with_metadata(file, temp_file, debug: options[:debug])
|
|
875
|
+
|
|
876
|
+
# Show compression stats
|
|
877
|
+
stats = CompressionManager.compression_stats(file, temp_file)
|
|
878
|
+
|
|
879
|
+
if options[:debug] || stats[:saved_bytes] > 0
|
|
880
|
+
Utils::OutputFormatter.indent("Original: #{Utils::FileHelper.format_size(stats[:original_size])}")
|
|
881
|
+
Utils::OutputFormatter.indent("Compressed: #{Utils::FileHelper.format_size(stats[:compressed_size])}")
|
|
882
|
+
Utils::OutputFormatter.indent("Saved: #{Utils::FileHelper.format_size(stats[:saved_bytes])} (#{stats[:reduction_percent].round(1)}% reduction)")
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
# Replace original with compressed
|
|
886
|
+
FileUtils.mv(temp_file, file)
|
|
887
|
+
|
|
888
|
+
file
|
|
889
|
+
end
|
|
480
890
|
end
|
|
481
891
|
end
|
data/lib/ruby_spriter/version.rb
CHANGED
data/lib/ruby_spriter.rb
CHANGED
|
@@ -26,6 +26,8 @@ require_relative 'ruby_spriter/metadata_manager'
|
|
|
26
26
|
require_relative 'ruby_spriter/video_processor'
|
|
27
27
|
require_relative 'ruby_spriter/gimp_processor'
|
|
28
28
|
require_relative 'ruby_spriter/consolidator'
|
|
29
|
+
require_relative 'ruby_spriter/compression_manager'
|
|
30
|
+
require_relative 'ruby_spriter/batch_processor'
|
|
29
31
|
|
|
30
32
|
# Load orchestration
|
|
31
33
|
require_relative 'ruby_spriter/processor'
|