ruby_spriter 0.6.5

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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +217 -0
  4. data/Gemfile +17 -0
  5. data/LICENSE +21 -0
  6. data/README.md +561 -0
  7. data/bin/ruby_spriter +20 -0
  8. data/lib/ruby_spriter/cli.rb +249 -0
  9. data/lib/ruby_spriter/consolidator.rb +146 -0
  10. data/lib/ruby_spriter/dependency_checker.rb +174 -0
  11. data/lib/ruby_spriter/gimp_processor.rb +664 -0
  12. data/lib/ruby_spriter/metadata_manager.rb +116 -0
  13. data/lib/ruby_spriter/platform.rb +82 -0
  14. data/lib/ruby_spriter/processor.rb +251 -0
  15. data/lib/ruby_spriter/utils/file_helper.rb +57 -0
  16. data/lib/ruby_spriter/utils/output_formatter.rb +65 -0
  17. data/lib/ruby_spriter/utils/path_helper.rb +59 -0
  18. data/lib/ruby_spriter/version.rb +7 -0
  19. data/lib/ruby_spriter/video_processor.rb +139 -0
  20. data/lib/ruby_spriter.rb +31 -0
  21. data/ruby_spriter.gemspec +42 -0
  22. data/spec/fixtures/image_without_metadata.png +0 -0
  23. data/spec/fixtures/spritesheet_4x2.png +0 -0
  24. data/spec/fixtures/spritesheet_4x4.png +0 -0
  25. data/spec/fixtures/spritesheet_6x2.png +0 -0
  26. data/spec/fixtures/spritesheet_with_metadata.png +0 -0
  27. data/spec/fixtures/test_video.mp4 +0 -0
  28. data/spec/ruby_spriter/cli_spec.rb +1142 -0
  29. data/spec/ruby_spriter/consolidator_spec.rb +375 -0
  30. data/spec/ruby_spriter/dependency_checker_spec.rb +0 -0
  31. data/spec/ruby_spriter/gimp_processor_spec.rb +425 -0
  32. data/spec/ruby_spriter/metadata_manager_spec.rb +0 -0
  33. data/spec/ruby_spriter/platform_spec.rb +82 -0
  34. data/spec/ruby_spriter/processor_spec.rb +0 -0
  35. data/spec/ruby_spriter/utils/file_helper_spec.rb +71 -0
  36. data/spec/ruby_spriter/utils/output_formatter_spec.rb +0 -0
  37. data/spec/ruby_spriter/utils/path_helper_spec.rb +78 -0
  38. data/spec/ruby_spriter/video_processor_spec.rb +0 -0
  39. data/spec/spec_helper.rb +41 -0
  40. metadata +88 -0
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module RubySpriter
6
+ # Manages PNG metadata for spritesheets
7
+ class MetadataManager
8
+ METADATA_PREFIX = 'SPRITESHEET'
9
+
10
+ # Embed metadata into PNG file
11
+ # @param input_file [String] Source PNG file
12
+ # @param output_file [String] Destination PNG file with metadata
13
+ # @param columns [Integer] Number of columns in grid
14
+ # @param rows [Integer] Number of rows in grid
15
+ # @param frames [Integer] Total number of frames
16
+ # @param debug [Boolean] Enable debug output
17
+ def self.embed(input_file, output_file, columns:, rows:, frames:, debug: false)
18
+ Utils::FileHelper.validate_readable!(input_file)
19
+
20
+ metadata_str = build_metadata_string(columns, rows, frames)
21
+
22
+ cmd = build_embed_command(input_file, output_file, metadata_str)
23
+
24
+ if debug
25
+ Utils::OutputFormatter.indent("DEBUG: Metadata command: #{cmd}")
26
+ end
27
+
28
+ stdout, stderr, status = Open3.capture3(cmd)
29
+
30
+ unless status.success?
31
+ raise ProcessingError, "Failed to embed metadata: #{stderr}"
32
+ end
33
+
34
+ Utils::FileHelper.validate_exists!(output_file)
35
+ end
36
+
37
+ # Read metadata from PNG file
38
+ # @param file [String] PNG file path
39
+ # @return [Hash, nil] Metadata hash or nil if not found
40
+ def self.read(file)
41
+ Utils::FileHelper.validate_readable!(file)
42
+
43
+ cmd = build_read_command(file)
44
+ stdout, stderr, status = Open3.capture3(cmd)
45
+
46
+ return nil unless status.success?
47
+
48
+ parse_metadata(stdout)
49
+ end
50
+
51
+ # Verify and print metadata from file
52
+ # @param file [String] PNG file path
53
+ def self.verify(file)
54
+ Utils::OutputFormatter.header("Spritesheet Metadata Verification")
55
+
56
+ puts "File: #{file}"
57
+ puts "Size: #{Utils::FileHelper.format_size(File.size(file))}\n\n"
58
+
59
+ metadata = read(file)
60
+
61
+ if metadata
62
+ Utils::OutputFormatter.success("Metadata Found")
63
+ puts "\n Grid Layout:"
64
+ Utils::OutputFormatter.indent("Columns: #{metadata[:columns]}")
65
+ Utils::OutputFormatter.indent("Rows: #{metadata[:rows]}")
66
+ Utils::OutputFormatter.indent("Total Frames: #{metadata[:frames]}")
67
+ Utils::OutputFormatter.indent("Metadata Version: #{metadata[:version]}")
68
+ else
69
+ Utils::OutputFormatter.warning("No spritesheet metadata found in this file")
70
+ puts "\nThis file may not have been created by Ruby Spriter,"
71
+ puts "or the metadata was stripped during processing."
72
+ end
73
+
74
+ puts "\n" + "=" * 60 + "\n"
75
+ end
76
+
77
+ private_class_method def self.build_metadata_string(columns, rows, frames)
78
+ "#{METADATA_PREFIX}|columns=#{columns}|rows=#{rows}|frames=#{frames}|version=#{METADATA_VERSION}"
79
+ end
80
+
81
+ private_class_method def self.build_embed_command(input_file, output_file, metadata_str)
82
+ magick_cmd = Platform.imagemagick_convert_cmd
83
+
84
+ [
85
+ magick_cmd,
86
+ Utils::PathHelper.quote_path(input_file),
87
+ '-set', 'comment', Utils::PathHelper.quote_arg(metadata_str),
88
+ Utils::PathHelper.quote_path(output_file)
89
+ ].join(' ')
90
+ end
91
+
92
+ private_class_method def self.build_read_command(file)
93
+ magick_cmd = Platform.imagemagick_identify_cmd
94
+
95
+ [
96
+ magick_cmd,
97
+ '-format', Utils::PathHelper.quote_arg('%c'),
98
+ Utils::PathHelper.quote_path(file)
99
+ ].join(' ')
100
+ end
101
+
102
+ private_class_method def self.parse_metadata(output)
103
+ # Look for SPRITESHEET metadata pattern
104
+ match = output.match(/#{METADATA_PREFIX}\|columns=(\d+)\|rows=(\d+)\|frames=(\d+)\|version=([\d.]+)/)
105
+
106
+ return nil unless match
107
+
108
+ {
109
+ columns: match[1].to_i,
110
+ rows: match[2].to_i,
111
+ frames: match[3].to_i,
112
+ version: match[4]
113
+ }
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubySpriter
4
+ # Platform detection and configuration
5
+ class Platform
6
+ PLATFORM_TYPE = case RUBY_PLATFORM
7
+ when /mingw|mswin|windows/i then :windows
8
+ when /linux/i then :linux
9
+ when /darwin/i then :macos
10
+ else :unknown
11
+ end
12
+
13
+ # GIMP executable paths by platform
14
+ GIMP_DEFAULT_PATHS = {
15
+ windows: 'C:\\Program Files\\GIMP 3\\bin\\gimp-console-3.0.exe',
16
+ linux: '/usr/bin/gimp',
17
+ macos: '/Applications/GIMP.app/Contents/MacOS/gimp'
18
+ }.freeze
19
+
20
+ # Alternative GIMP paths to search
21
+ GIMP_ALTERNATIVE_PATHS = {
22
+ windows: [
23
+ 'C:\\Program Files\\GIMP 3\\bin\\gimp-console-3.0.exe',
24
+ 'C:\\Program Files (x86)\\GIMP 3\\bin\\gimp-console-3.0.exe',
25
+ 'C:\\Program Files\\GIMP 2\\bin\\gimp-console-2.10.exe',
26
+ 'C:\\Program Files (x86)\\GIMP 2\\bin\\gimp-console-2.10.exe'
27
+ ].freeze,
28
+ linux: [
29
+ '/usr/bin/gimp',
30
+ '/usr/local/bin/gimp',
31
+ '/snap/bin/gimp',
32
+ '/opt/gimp/bin/gimp'
33
+ ].freeze,
34
+ macos: [
35
+ '/Applications/GIMP.app/Contents/MacOS/gimp',
36
+ '/Applications/GIMP-2.10.app/Contents/MacOS/gimp'
37
+ ].freeze
38
+ }.freeze
39
+
40
+ class << self
41
+ # Get the current platform type
42
+ def current
43
+ PLATFORM_TYPE
44
+ end
45
+
46
+ # Check if running on Windows
47
+ def windows?
48
+ PLATFORM_TYPE == :windows
49
+ end
50
+
51
+ # Check if running on Linux
52
+ def linux?
53
+ PLATFORM_TYPE == :linux
54
+ end
55
+
56
+ # Check if running on macOS
57
+ def macos?
58
+ PLATFORM_TYPE == :macos
59
+ end
60
+
61
+ # Get default GIMP path for current platform
62
+ def default_gimp_path
63
+ GIMP_DEFAULT_PATHS[PLATFORM_TYPE]
64
+ end
65
+
66
+ # Get alternative GIMP paths for current platform
67
+ def alternative_gimp_paths
68
+ GIMP_ALTERNATIVE_PATHS[PLATFORM_TYPE] || []
69
+ end
70
+
71
+ # Get ImageMagick convert command name
72
+ def imagemagick_convert_cmd
73
+ windows? ? 'magick convert' : 'convert'
74
+ end
75
+
76
+ # Get ImageMagick identify command name
77
+ def imagemagick_identify_cmd
78
+ windows? ? 'magick identify' : 'identify'
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tmpdir'
5
+
6
+ module RubySpriter
7
+ # Main orchestration processor
8
+ class Processor
9
+ attr_reader :options, :gimp_path
10
+
11
+ def initialize(options = {})
12
+ @options = default_options.merge(options)
13
+ @gimp_path = nil
14
+ end
15
+
16
+ # Run the processing workflow
17
+ def run
18
+ validate_options!
19
+ check_dependencies!
20
+ setup_temp_directory
21
+
22
+ result = execute_workflow
23
+
24
+ cleanup unless options[:keep_temp]
25
+
26
+ result
27
+ end
28
+
29
+ private
30
+
31
+ def default_options
32
+ {
33
+ video: nil,
34
+ image: nil,
35
+ consolidate: nil,
36
+ verify: nil,
37
+ output: nil,
38
+ frame_count: 16,
39
+ columns: 4,
40
+ max_width: 320,
41
+ padding: 0,
42
+ bg_color: 'black',
43
+ scale_percent: nil,
44
+ scale_interpolation: 'nohalo',
45
+ sharpen: false,
46
+ sharpen_radius: 2.0,
47
+ sharpen_gain: 0.5,
48
+ sharpen_threshold: 0.03,
49
+ remove_bg: false,
50
+ bg_threshold: 0.0,
51
+ grow_selection: 1,
52
+ fuzzy_select: true,
53
+ operation_order: :scale_then_remove_bg,
54
+ validate_columns: true,
55
+ temp_dir: nil,
56
+ keep_temp: false,
57
+ debug: false
58
+ }
59
+ end
60
+
61
+ def validate_options!
62
+ input_modes = [options[:video], options[:image], options[:consolidate], options[:verify]].compact
63
+
64
+ if input_modes.empty?
65
+ raise ValidationError, "Must specify --video, --image, --consolidate, or --verify"
66
+ end
67
+
68
+ if input_modes.length > 1
69
+ raise ValidationError, "Cannot use multiple input modes together. Choose one."
70
+ end
71
+
72
+ validate_input_files!
73
+ end
74
+
75
+ def validate_input_files!
76
+ if options[:video]
77
+ Utils::FileHelper.validate_exists!(options[:video])
78
+ validate_file_extension!(options[:video], ['.mp4'], '--video')
79
+ end
80
+
81
+ if options[:image]
82
+ Utils::FileHelper.validate_exists!(options[:image])
83
+ validate_file_extension!(options[:image], ['.png'], '--image')
84
+ end
85
+
86
+ if options[:consolidate]
87
+ if options[:consolidate].length < 2
88
+ raise ValidationError, "--consolidate requires at least 2 files"
89
+ end
90
+
91
+ options[:consolidate].each do |file|
92
+ Utils::FileHelper.validate_exists!(file)
93
+ validate_file_extension!(file, ['.png'], '--consolidate')
94
+ end
95
+ end
96
+
97
+ if options[:verify]
98
+ Utils::FileHelper.validate_exists!(options[:verify])
99
+ validate_file_extension!(options[:verify], ['.png'], '--verify')
100
+ end
101
+ end
102
+
103
+ def validate_file_extension!(file_path, valid_extensions, flag_name)
104
+ ext = File.extname(file_path).downcase
105
+ unless valid_extensions.include?(ext)
106
+ expected = valid_extensions.join(', ')
107
+ raise ValidationError, "#{flag_name} expects #{expected} file, got: #{ext || '(no extension)'}"
108
+ end
109
+ end
110
+
111
+ def check_dependencies!
112
+ checker = DependencyChecker.new(verbose: options[:debug])
113
+ results = checker.check_all
114
+
115
+ # Check required tools
116
+ missing = []
117
+
118
+ [:ffmpeg, :ffprobe, :imagemagick].each do |tool|
119
+ missing << tool unless results[tool][:available]
120
+ end
121
+
122
+ # GIMP only needed for image processing
123
+ if needs_gimp? && !results[:gimp][:available]
124
+ missing << :gimp
125
+ end
126
+
127
+ if missing.any?
128
+ checker.print_report
129
+ raise DependencyError, "Missing required dependencies: #{missing.join(', ')}"
130
+ end
131
+
132
+ @gimp_path = checker.gimp_path if results[:gimp][:available]
133
+
134
+ if options[:debug]
135
+ checker.print_report
136
+ end
137
+ end
138
+
139
+ def needs_gimp?
140
+ options[:scale_percent] || options[:remove_bg]
141
+ end
142
+
143
+ def setup_temp_directory
144
+ @options[:temp_dir] = Dir.mktmpdir('ruby_spriter_')
145
+
146
+ if options[:debug]
147
+ Utils::OutputFormatter.indent("Temp directory: #{options[:temp_dir]}")
148
+ end
149
+ end
150
+
151
+ def execute_workflow
152
+ Utils::OutputFormatter.header("Ruby Spriter v#{VERSION}")
153
+ puts "Platform: #{Platform.current.to_s.capitalize}"
154
+ puts "Date: #{VERSION_DATE}\n\n"
155
+
156
+ if options[:verify]
157
+ MetadataManager.verify(options[:verify])
158
+ return { mode: :verify, file: options[:verify] }
159
+ end
160
+
161
+ if options[:consolidate]
162
+ return execute_consolidate_workflow
163
+ elsif options[:image]
164
+ return execute_image_workflow
165
+ else
166
+ return execute_video_workflow
167
+ end
168
+ end
169
+
170
+ def execute_video_workflow
171
+ # Step 1: Convert video to spritesheet
172
+ video_processor = VideoProcessor.new(options)
173
+ result = video_processor.create_spritesheet(
174
+ options[:video],
175
+ options[:output] || Utils::FileHelper.spritesheet_filename(options[:video])
176
+ )
177
+
178
+ working_file = result[:output_file]
179
+
180
+ # Step 2: Apply GIMP processing if requested
181
+ if needs_gimp?
182
+ working_file = process_with_gimp(working_file)
183
+ end
184
+
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]
189
+ end
190
+
191
+ Utils::OutputFormatter.header("SUCCESS!")
192
+ Utils::OutputFormatter.success("Final output: #{working_file}")
193
+
194
+ result.merge(final_output: working_file)
195
+ end
196
+
197
+ def execute_image_workflow
198
+ working_file = options[:image]
199
+
200
+ # Apply GIMP processing if requested
201
+ if needs_gimp?
202
+ working_file = process_with_gimp(working_file)
203
+ end
204
+
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]
209
+ end
210
+
211
+ Utils::OutputFormatter.header("SUCCESS!")
212
+ Utils::OutputFormatter.success("Final output: #{working_file}")
213
+
214
+ {
215
+ mode: :image,
216
+ input_file: options[:image],
217
+ output_file: working_file
218
+ }
219
+ end
220
+
221
+ def execute_consolidate_workflow
222
+ consolidator = Consolidator.new(options)
223
+
224
+ output_file = options[:output] || generate_consolidated_filename
225
+
226
+ result = consolidator.consolidate(options[:consolidate], output_file)
227
+
228
+ Utils::OutputFormatter.header("SUCCESS!")
229
+ Utils::OutputFormatter.success("Final output: #{result[:output_file]}")
230
+
231
+ result.merge(mode: :consolidate)
232
+ end
233
+
234
+ def process_with_gimp(input_file)
235
+ gimp_processor = GimpProcessor.new(@gimp_path, options)
236
+ gimp_processor.process(input_file)
237
+ end
238
+
239
+ def generate_consolidated_filename
240
+ timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
241
+ "consolidated_spritesheet_#{timestamp}.png"
242
+ end
243
+
244
+ def cleanup
245
+ if options[:temp_dir] && Dir.exist?(options[:temp_dir])
246
+ FileUtils.rm_rf(options[:temp_dir])
247
+ Utils::OutputFormatter.note("Cleaned up temporary files") if options[:debug]
248
+ end
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubySpriter
4
+ module Utils
5
+ # File naming and size utilities
6
+ class FileHelper
7
+ class << self
8
+ # Generate spritesheet filename from video file
9
+ # @param video_file [String] Path to video file
10
+ # @return [String] Generated spritesheet filename
11
+ def spritesheet_filename(video_file)
12
+ dir = File.dirname(video_file)
13
+ basename = File.basename(video_file, '.*')
14
+ File.join(dir, "#{basename}_spritesheet.png")
15
+ end
16
+
17
+ # Generate output filename with suffix
18
+ # @param input_file [String] Original input file
19
+ # @param suffix [String] Suffix to add to filename
20
+ # @return [String] Generated output filename
21
+ def output_filename(input_file, suffix)
22
+ dir = File.dirname(input_file)
23
+ basename = File.basename(input_file, '.*')
24
+ File.join(dir, "#{basename}-#{suffix}.png")
25
+ end
26
+
27
+ # Format file size in human-readable format
28
+ # @param bytes [Integer] File size in bytes
29
+ # @return [String] Formatted file size
30
+ def format_size(bytes)
31
+ if bytes >= 1024 * 1024
32
+ "#{(bytes / (1024.0 * 1024.0)).round(2)} MB"
33
+ elsif bytes >= 1024
34
+ "#{(bytes / 1024.0).round(2)} KB"
35
+ else
36
+ "#{bytes} bytes"
37
+ end
38
+ end
39
+
40
+ # Validate file exists
41
+ # @param path [String] File path to validate
42
+ # @raise [ValidationError] if file doesn't exist
43
+ def validate_exists!(path)
44
+ raise ValidationError, "File not found: #{path}" unless File.exist?(path)
45
+ end
46
+
47
+ # Validate file is readable
48
+ # @param path [String] File path to validate
49
+ # @raise [ValidationError] if file isn't readable
50
+ def validate_readable!(path)
51
+ validate_exists!(path)
52
+ raise ValidationError, "File not readable: #{path}" unless File.readable?(path)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubySpriter
4
+ module Utils
5
+ # Console output formatting utilities
6
+ class OutputFormatter
7
+ ICONS = {
8
+ success: '✅',
9
+ error: '❌',
10
+ warning: '⚠️',
11
+ info: 'ℹ️',
12
+ clean: '🧹',
13
+ note: '📝'
14
+ }.freeze
15
+
16
+ class << self
17
+ # Print section header
18
+ # @param title [String] Section title
19
+ # @param width [Integer] Header width
20
+ def header(title, width = 60)
21
+ puts "\n" + "=" * width
22
+ puts title
23
+ puts "=" * width + "\n"
24
+ end
25
+
26
+ # Print success message
27
+ # @param message [String] Message to print
28
+ def success(message)
29
+ puts "#{ICONS[:success]} #{message}"
30
+ end
31
+
32
+ # Print error message
33
+ # @param message [String] Message to print
34
+ def error(message)
35
+ puts "#{ICONS[:error]} #{message}"
36
+ end
37
+
38
+ # Print warning message
39
+ # @param message [String] Message to print
40
+ def warning(message)
41
+ puts "#{ICONS[:warning]} #{message}"
42
+ end
43
+
44
+ # Print info message
45
+ # @param message [String] Message to print
46
+ def info(message)
47
+ puts "#{ICONS[:info]} #{message}"
48
+ end
49
+
50
+ # Print note message
51
+ # @param message [String] Message to print
52
+ def note(message)
53
+ puts "#{ICONS[:note]} #{message}"
54
+ end
55
+
56
+ # Print indented message
57
+ # @param message [String] Message to print
58
+ # @param indent [Integer] Number of spaces to indent
59
+ def indent(message, indent = 6)
60
+ puts " " * indent + message
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubySpriter
4
+ module Utils
5
+ # Cross-platform path handling utilities
6
+ class PathHelper
7
+ class << self
8
+ # Quote a file path for shell execution
9
+ # @param path [String] The path to quote
10
+ # @return [String] Properly quoted path
11
+ def quote_path(path)
12
+ if Platform.windows?
13
+ "\"#{path}\""
14
+ else
15
+ # Need 4 backslashes because gsub interprets backslashes in replacement string
16
+ "'#{path.gsub("'", "\\\\'")}'"
17
+ end
18
+ end
19
+
20
+ # Quote a command argument for shell execution
21
+ # @param arg [String] The argument to quote
22
+ # @return [String] Properly quoted argument
23
+ def quote_arg(arg)
24
+ if Platform.windows?
25
+ "\"#{arg}\""
26
+ else
27
+ # Need 4 backslashes because gsub interprets backslashes in replacement string
28
+ "'#{arg.gsub("'", "\\\\'")}'"
29
+ end
30
+ end
31
+
32
+ # Normalize path for Python scripts (GIMP)
33
+ # @param path [String] The path to normalize
34
+ # @return [String] Normalized path with forward slashes
35
+ def normalize_for_python(path)
36
+ abs_path = File.absolute_path(path)
37
+
38
+ if Platform.windows?
39
+ # Use forward slashes for Python raw strings
40
+ abs_path.gsub('\\', '/')
41
+ else
42
+ abs_path
43
+ end
44
+ end
45
+
46
+ # Convert path to native format
47
+ # @param path [String] The path to convert
48
+ # @return [String] Path with platform-appropriate separators
49
+ def to_native(path)
50
+ if Platform.windows?
51
+ path.gsub('/', '\\')
52
+ else
53
+ path.gsub('\\', '/')
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubySpriter
4
+ VERSION = '0.6.5'
5
+ VERSION_DATE = '2025-10-23'
6
+ METADATA_VERSION = '0.6'
7
+ end