the-architect-sprite_generator 0.1.11
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.
- data/README.txt +33 -0
- data/lib/sprite_batch_generator.rb +37 -0
- data/lib/sprite_generator.rb +190 -0
- data/test/units/test_sprite_batch_generator.rb +29 -0
- data/test/units/test_sprite_generator.rb +122 -0
- metadata +75 -0
data/README.txt
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
= SpriteGenerator
|
2
|
+
|
3
|
+
Automatically generate Sprite Images and the corresponding CSS using RMagick and Liquid.
|
4
|
+
|
5
|
+
== Dependencies
|
6
|
+
|
7
|
+
* RMagick 2.2.2
|
8
|
+
* Liquid 1.7.0
|
9
|
+
|
10
|
+
== LICENSE:
|
11
|
+
|
12
|
+
(The MIT License)
|
13
|
+
|
14
|
+
Copyright (c) 2009 Marcel Scherf
|
15
|
+
|
16
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
17
|
+
a copy of this software and associated documentation files (the
|
18
|
+
'Software'), to deal in the Software without restriction, including
|
19
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
20
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
21
|
+
permit persons to whom the Software is furnished to do so, subject to
|
22
|
+
the following conditions:
|
23
|
+
|
24
|
+
The above copyright notice and this permission notice shall be
|
25
|
+
included in all copies or substantial portions of the Software.
|
26
|
+
|
27
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
28
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
29
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
30
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
31
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
32
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
33
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'pathname'
|
4
|
+
require File.expand_path(File.dirname(__FILE__) + '/sprite_generator')
|
5
|
+
|
6
|
+
class SpriteBatchGenerator
|
7
|
+
attr_reader :batches
|
8
|
+
|
9
|
+
def initialize(filename)
|
10
|
+
config = YAML.load(File.read(filename))
|
11
|
+
@batches = config.inject([]) do |arr, pair|
|
12
|
+
if !!defined?(RAILS_ROOT)
|
13
|
+
pair.last.merge!(:config_root => RAILS_ROOT)
|
14
|
+
elsif pair.last[:root]
|
15
|
+
root = File.expand_path(pair.last[:root], file_name)
|
16
|
+
pair.last.merge!(:config_root => root)
|
17
|
+
end
|
18
|
+
arr.push OpenStruct.new(pair.last)
|
19
|
+
arr
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def generate
|
25
|
+
@batches.each do |batch|
|
26
|
+
generator = SpriteGenerator.new(batch.files, batch.output, batch.config_root, batch.options || {})
|
27
|
+
css = generator.create
|
28
|
+
# only write output if css_output is specified
|
29
|
+
unless css.nil? || css.empty? || batch.css_output.nil?
|
30
|
+
output = batch.css_template.nil? ? css : Liquid::Template.parse(File.open(batch.css_template).read).render('css' => css)
|
31
|
+
File.open(batch.css_output, 'w+'){|f| f.puts output }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'RMagick'
|
3
|
+
require 'liquid'
|
4
|
+
|
5
|
+
class SpriteGenerator
|
6
|
+
VERSION = '0.1.10'
|
7
|
+
|
8
|
+
include Magick
|
9
|
+
|
10
|
+
# parameters:
|
11
|
+
# files_or_path: can be Dir.glob-able paths or an array of filenames
|
12
|
+
# ie: ['../images/icons/icon1_out.png', '../images/icons/icon1_over.png'] or '../images/icons/icon*.png'
|
13
|
+
# output: filename where the generated sprite will be stored
|
14
|
+
# ie: '../images/sprites/icons.png'
|
15
|
+
# options: set a variety of options
|
16
|
+
# - delimiter: characters by which multiple variations of images will be found
|
17
|
+
# ie: '_' for 'icon_hover.png'
|
18
|
+
# ie: '-' for 'icon-hover.png'
|
19
|
+
# if icon is the basename
|
20
|
+
# - align:
|
21
|
+
# - sprite_location: will be available as variable in the liquid template, if this is not set, the template will use output as sprite_location
|
22
|
+
# - tile: if set to ie. '100x100' it will center every image on a 100 by 100 tile
|
23
|
+
# - template: Liquid template for each sprite, use this to build the css for your sprites
|
24
|
+
# these variables are available:
|
25
|
+
# - top: distance to top border of sprite in pixels
|
26
|
+
# - left: distance to left border of sprite in pixels
|
27
|
+
# - width: width of current image in pixels
|
28
|
+
# - height: height of current image in pixels
|
29
|
+
# - basename: filename or basename of variations
|
30
|
+
# ie: with variations: icon_out.png, icon_over.png => icon
|
31
|
+
# ie: without variations: icon.png => icon.png
|
32
|
+
# - file_basename: always the name of the current image without extension
|
33
|
+
# ie: icon_over
|
34
|
+
# - filename: icon_over.png
|
35
|
+
# - full_filename: ../images/icons/icon_over.ong
|
36
|
+
# - variations: number of variations as number
|
37
|
+
# - variation: the current variation as zero based number
|
38
|
+
# - sprite_location: path to sprite
|
39
|
+
def initialize(files_or_paths, output, root, options = {})
|
40
|
+
@files = find_files(files_or_paths)
|
41
|
+
return if @files.nil? || @files.empty?
|
42
|
+
@root = root || ''
|
43
|
+
@output = output
|
44
|
+
@delimiter = options[:delimiter] || '-'
|
45
|
+
@analyzed = analyze_filenames(@files, @delimiter)
|
46
|
+
@template = Liquid::Template.parse(options[:template] || '')
|
47
|
+
|
48
|
+
@sprite_location = options[:sprite_location] || @output
|
49
|
+
@background = options[:background] || '#FFFFFF00'
|
50
|
+
@tile_size = options[:tile]
|
51
|
+
@alignment = options[:alignment] ? Magick.const_get("#{camelize(options[:alignment])}Gravity") : Magick::CenterGravity
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
def create
|
56
|
+
raise 'No files found.' if @files.nil? || @files.empty?
|
57
|
+
background = @background
|
58
|
+
unless @tile_size.nil?
|
59
|
+
size_x, size_y = @tile_size.split('x').first(2).map{|dim| dim.to_i}
|
60
|
+
tile = Magick::Image.new(size_x, size_y){ self.background_color = background }
|
61
|
+
tile.format = "PNG"
|
62
|
+
end
|
63
|
+
destination = @root.nil? || @root.empty? ? @output : File.join(@root, @output)
|
64
|
+
image, css = build(tile)
|
65
|
+
image.write(destination){ self.background_color = background }
|
66
|
+
css
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
# simplyfied version of active_supports camelize version
|
73
|
+
def camelize(lower_case_and_underscored_word)
|
74
|
+
lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
75
|
+
end
|
76
|
+
|
77
|
+
def build(tile = nil)
|
78
|
+
background = @background
|
79
|
+
images = ImageList.new{ self.background_color = background }
|
80
|
+
context = { 'sprite_location' => @sprite_location, 'tile' => tile }
|
81
|
+
css = []
|
82
|
+
|
83
|
+
@analyzed.each do |key, value|
|
84
|
+
if tile
|
85
|
+
context['left'] = images.length > 0 ? tile.columns : 0
|
86
|
+
else
|
87
|
+
context['left'] = images.length > 0 ? images.append(false).columns : 0
|
88
|
+
end
|
89
|
+
context['top'] = 0
|
90
|
+
context['basename'] = key
|
91
|
+
context['overall'] = images.length
|
92
|
+
|
93
|
+
if value.size > 1
|
94
|
+
image_list = ImageList.new(*value){ self.background_color = background }
|
95
|
+
if tile
|
96
|
+
tiles = ImageList.new{ self.background_color = background }
|
97
|
+
image_list.each do |image|
|
98
|
+
tiles << tile.composite(image, @alignment, Magick::OverCompositeOp)
|
99
|
+
end
|
100
|
+
images.from_blob(tiles.append(true).to_blob){ self.background_color = background }
|
101
|
+
else
|
102
|
+
images.from_blob(image_list.append(true).to_blob){ self.background_color = background }
|
103
|
+
end
|
104
|
+
context['variations'] = image_list.length
|
105
|
+
context['type'] = :list
|
106
|
+
context['images'] = image_list
|
107
|
+
context['filenames'] = value
|
108
|
+
else
|
109
|
+
image = Image.read(value.flatten.first){ self.background_color = background }
|
110
|
+
context['variations'] = 0
|
111
|
+
context['variation_name'] = ''
|
112
|
+
context['variation_number'] = 0
|
113
|
+
context['type'] = :image
|
114
|
+
|
115
|
+
if tile
|
116
|
+
images.from_blob(tile.composite(image.first, @alignment, Magick::OverCompositeOp).to_blob){ self.background_color = background }
|
117
|
+
else
|
118
|
+
images.from_blob(image.first.to_blob){ self.background_color = background }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
css << build_css(context)
|
122
|
+
end
|
123
|
+
|
124
|
+
[images.append(false), css.join("\n")]
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
def build_css(context = {})
|
129
|
+
type = context.delete('type')
|
130
|
+
case type
|
131
|
+
when :list
|
132
|
+
css = build_css_for_list(context)
|
133
|
+
when :image
|
134
|
+
# render template if there is only one image
|
135
|
+
css = @template.render(context)
|
136
|
+
end
|
137
|
+
css
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
def build_css_for_list(context)
|
142
|
+
new_context = context.dup
|
143
|
+
tile = new_context.delete('tile')
|
144
|
+
image_list = context.delete('images')
|
145
|
+
new_context['type'] = :image
|
146
|
+
css = image_list.inject([]) do |css, image|
|
147
|
+
new_context['width'] = tile ? tile.columns : image.columns
|
148
|
+
new_context['height'] = tile ? tile.rows : image.rows
|
149
|
+
new_context['variation_number'] = css.size
|
150
|
+
|
151
|
+
new_context['full_filename'] = context['filenames'].shift
|
152
|
+
new_context['filename'] = File.basename(new_context['full_filename'])
|
153
|
+
new_context['file_basename'] = File.basename(new_context['full_filename'], '.*')
|
154
|
+
new_context['variation_name'] = new_context['file_basename'].gsub(/^#{new_context['basename']}#{@delimiter}/, '')
|
155
|
+
|
156
|
+
css << build_css(new_context.dup)
|
157
|
+
new_context['top'] += tile ? tile.columns : new_context['height']
|
158
|
+
css
|
159
|
+
end.join("\n")
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
# gather files that will be used to create the sprite
|
164
|
+
def find_files(*args)
|
165
|
+
args.inject([]) do |files, arg|
|
166
|
+
found_files = Dir.glob(arg)
|
167
|
+
if found_files.empty?
|
168
|
+
files << arg if File.exists?(arg) rescue raise arg.inspect
|
169
|
+
else
|
170
|
+
files << found_files.flatten
|
171
|
+
end
|
172
|
+
files.flatten.compact.uniq
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
|
177
|
+
# gather information about the selected files
|
178
|
+
# check for variations by using a delimiter as an indicator
|
179
|
+
def analyze_filenames(file_names, delimiter = '_')
|
180
|
+
file_names.inject(Hash.new{|hash, key| hash[key] = Array.new;}) do |h, file|
|
181
|
+
basename = File.basename(file).split('.').first
|
182
|
+
without_variation = basename.split(delimiter)[0..-2].join(delimiter)
|
183
|
+
basename = without_variation.nil? || without_variation == '' ? basename : without_variation
|
184
|
+
h[basename] << file
|
185
|
+
h
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../lib/sprite_batch_generator')
|
3
|
+
|
4
|
+
class SpriteBatchGeneratorTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@config = 'test/config/batch.yml'
|
8
|
+
end
|
9
|
+
|
10
|
+
def teardown
|
11
|
+
# delete test output
|
12
|
+
Dir.glob('test/output/*').each{|f| File.delete f }
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_should_read_config_file
|
16
|
+
@batch = SpriteBatchGenerator.new(@config)
|
17
|
+
assert_not_nil @batch
|
18
|
+
assert_equal 2, @batch.batches.size
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_should_create_files_from_config
|
22
|
+
@batch = SpriteBatchGenerator.new(@config)
|
23
|
+
css = @batch.generate
|
24
|
+
assert_not_nil css
|
25
|
+
output_files = Dir.glob('test/output/*')
|
26
|
+
assert 6, output_files.size
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'liquid'
|
3
|
+
require 'test/unit'
|
4
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../lib/sprite_generator')
|
5
|
+
|
6
|
+
class SpriteGeneratorTest < Test::Unit::TestCase
|
7
|
+
|
8
|
+
def setup
|
9
|
+
@template = '.{{basename}}_{{variation}}{ background:transparent url({{sprite_location}}) -{{left}}px -{{top}}px no-repeat; width:{{width}}px; height:{{height}}px; }'
|
10
|
+
@all_images_path = 'test/images/*.png'
|
11
|
+
@output = 'test/output/sprite_by_create.png'
|
12
|
+
@page_path = 'test/output/test.html'
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def teardown
|
17
|
+
# delete test output
|
18
|
+
Dir.glob('test/output/*').each{|f| File.delete f }
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def test_should_create_correct_context
|
23
|
+
template = %q{
|
24
|
+
basename: {{basename}}
|
25
|
+
variation: {{variation}}
|
26
|
+
sprite_location: {{sprite_location}}
|
27
|
+
left: {{left}}
|
28
|
+
top: {{top}}
|
29
|
+
width: {{width}}
|
30
|
+
height: {{height}}
|
31
|
+
filename: {{filename}}
|
32
|
+
file_basename: {{file_basename}}
|
33
|
+
full_filename: {{full_filename}}
|
34
|
+
variations: {{variations}}
|
35
|
+
variation_number: {{variation_number}}
|
36
|
+
variation_name: {{variation_name}}
|
37
|
+
}
|
38
|
+
@generator = SpriteGenerator.new(@all_images_path, @output, nil, {:template => template})
|
39
|
+
css = @generator.create
|
40
|
+
assert css.include?('basename: emoticon-evilgrin')
|
41
|
+
assert css.include?('variation_name: evilgrin')
|
42
|
+
assert css.include?('file_basename: emoticon-evilgrin')
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_should_use_alignment_option
|
46
|
+
assert_nothing_raised do
|
47
|
+
SpriteGenerator.new(@all_images_path, @output, nil, {:template => @template, :tile => '100x100', :background => '#FFFFFF', :alignment => 'west'})
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_should_complain_over_unknown_alignment_option
|
52
|
+
assert_raise NameError do
|
53
|
+
SpriteGenerator.new(@all_images_path, @output, nil, {:template => @template, :tile => '100x100', :background => '#FFFFFF', :alignment => 'somewhere'})
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_should_center_images_on_tiles
|
58
|
+
@generator = SpriteGenerator.new(@all_images_path, @output, nil, {:template => @template, :tile => '100x100', :background => '#FFFFFF00'})
|
59
|
+
css = @generator.create
|
60
|
+
page = Liquid::Template.parse(File.open('test/templates/test.html').read).render('css' => css)
|
61
|
+
assert page.include?("{ background:")
|
62
|
+
assert page.include?("width:100px")
|
63
|
+
assert page.include?("height:100px")
|
64
|
+
assert page.include?("-300px")
|
65
|
+
assert page.include?("-800px")
|
66
|
+
File.open(@page_path, 'w+'){|f| f.puts page }
|
67
|
+
assert File.exists?(@page_path)
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
def test_should_create_correct_css
|
72
|
+
@generator = SpriteGenerator.new(@all_images_path, @output, nil, :template => @template)
|
73
|
+
css = @generator.create()
|
74
|
+
page = Liquid::Template.parse(File.open('test/templates/test.html').read).render('css' => css)
|
75
|
+
assert page.include?("{ background:")
|
76
|
+
assert page.include?("width:16px")
|
77
|
+
assert page.include?("height:16px")
|
78
|
+
assert page.include?("-16px")
|
79
|
+
assert page.include?("-96px")
|
80
|
+
File.open(@page_path, 'w+'){|f| f.puts page }
|
81
|
+
assert File.exists?(@page_path)
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
def test_should_generate_sprite_file
|
86
|
+
@generator = SpriteGenerator.new(@all_images_path, @output, nil, :template => @template)
|
87
|
+
css = @generator.create
|
88
|
+
assert !(css.nil? || css.empty?)
|
89
|
+
assert File.exists?(@output)
|
90
|
+
end
|
91
|
+
|
92
|
+
# bad, testing internal state
|
93
|
+
def test_should_find_versions_of_emoticons
|
94
|
+
files = Dir.glob(@all_images_path)
|
95
|
+
@generator = SpriteGenerator.new(files, @output, nil, {})
|
96
|
+
analyzed = @generator.instance_variable_get(:@analyzed)
|
97
|
+
assert_equal 9, analyzed['emoticon'].size
|
98
|
+
end
|
99
|
+
|
100
|
+
# bad, using internal state
|
101
|
+
def test_should_find_files_for_glob_path
|
102
|
+
@generator = SpriteGenerator.new(@all_images_path, @output, nil, {})
|
103
|
+
files = @generator.instance_variable_get(:@files)
|
104
|
+
assert_equal 16, files.size
|
105
|
+
end
|
106
|
+
|
107
|
+
# bad, using internal state
|
108
|
+
def test_should_find_files
|
109
|
+
@generator = SpriteGenerator.new(['test/images/emoticon-evilgrin.png', 'test/images/emoticon-grin.png'], @output, nil, {})
|
110
|
+
files = @generator.instance_variable_get(:@files)
|
111
|
+
assert_equal 2, files.size
|
112
|
+
end
|
113
|
+
|
114
|
+
# bad, using internal state
|
115
|
+
def test_should_not_find_anything
|
116
|
+
@generator = SpriteGenerator.new('test/blalala/*.hurz', @output, nil, {})
|
117
|
+
files = @generator.instance_variable_get(:@files)
|
118
|
+
assert_equal 0, files.size
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
end
|
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: the-architect-sprite_generator
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.11
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Marcel Scherf
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-04-21 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: liquid
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rmagick
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
description:
|
36
|
+
email: marcel.scherf@gmail.com
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- README.txt
|
43
|
+
files:
|
44
|
+
- lib/sprite_batch_generator.rb
|
45
|
+
- lib/sprite_generator.rb
|
46
|
+
- README.txt
|
47
|
+
has_rdoc: true
|
48
|
+
homepage: http://github.com/the-architect/spritegenerator
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options:
|
51
|
+
- --charset=UTF-8
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
version:
|
66
|
+
requirements: []
|
67
|
+
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 1.2.0
|
70
|
+
signing_key:
|
71
|
+
specification_version: 2
|
72
|
+
summary: Automatically generate Sprite Images and the corresponding CSS using RMagick and Liquid.
|
73
|
+
test_files:
|
74
|
+
- test/units/test_sprite_batch_generator.rb
|
75
|
+
- test/units/test_sprite_generator.rb
|