sprite_generator 0.2.5 → 0.3.0
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/lib/sprites.rb +24 -0
- data/lib/sprites/batch.rb +34 -0
- data/lib/sprites/generator.rb +244 -0
- data/test/test_helper.rb +9 -0
- data/test/units/{test_sprite_batch_generator.rb → batch_test.rb} +7 -8
- data/test/units/{test_sprite_generator.rb → generator_test.rb} +42 -67
- metadata +78 -19
- data/lib/sprite_batch_generator.rb +0 -37
- data/lib/sprite_generator.rb +0 -264
data/lib/sprites.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "bundler"
|
3
|
+
Bundler.setup(:default)
|
4
|
+
require 'yaml'
|
5
|
+
require 'ostruct'
|
6
|
+
require 'RMagick'
|
7
|
+
require 'liquid'
|
8
|
+
|
9
|
+
require File.expand_path('../sprites/generator', __FILE__)
|
10
|
+
require File.expand_path('../sprites/batch', __FILE__)
|
11
|
+
|
12
|
+
module Sprites
|
13
|
+
class Config
|
14
|
+
class << self
|
15
|
+
def root=(root = nil)
|
16
|
+
@root = root
|
17
|
+
end
|
18
|
+
|
19
|
+
def root
|
20
|
+
@root || File.expand_path('../../', __FILE__)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Sprites
|
2
|
+
class Batch
|
3
|
+
attr_reader :batches
|
4
|
+
|
5
|
+
def initialize(filename)
|
6
|
+
config = YAML.load(File.read(filename))
|
7
|
+
@batches = config.inject([]) do |arr, pair|
|
8
|
+
if !!defined?(RAILS_ROOT)
|
9
|
+
pair.last.merge!(:config_root => RAILS_ROOT)
|
10
|
+
elsif pair.last[:root]
|
11
|
+
root = File.expand_path(pair.last[:root], file_name)
|
12
|
+
pair.last.merge!(:config_root => root)
|
13
|
+
end
|
14
|
+
arr.push OpenStruct.new(pair.last)
|
15
|
+
arr
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def generate
|
20
|
+
@batches.each do |batch|
|
21
|
+
generator = Sprites::Generator.new(batch.files, batch.output, batch.config_root, batch.options || {})
|
22
|
+
css = generator.create
|
23
|
+
# only write output if css_output is specified
|
24
|
+
unless css.nil? || css.empty? || batch.css_output.nil?
|
25
|
+
output = batch.css_template.nil? ? css : Liquid::Template.parse(File.open(batch.css_template).read).render('css' => css)
|
26
|
+
File.open(batch.css_output, 'w+'){|f| f.puts output }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
|
@@ -0,0 +1,244 @@
|
|
1
|
+
module Sprites
|
2
|
+
class Generator
|
3
|
+
include Magick
|
4
|
+
|
5
|
+
# parameters:
|
6
|
+
# files_or_path: can be Dir.glob-able paths or an array of filenames
|
7
|
+
# ie: ['../images/icons/icon1_out.png', '../images/icons/icon1_over.png'] or '../images/icons/icon*.png'
|
8
|
+
# output: filename where the generated sprite will be stored
|
9
|
+
# ie: '../images/sprites/icons.png'
|
10
|
+
# options: set a variety of options
|
11
|
+
# - delimiter: characters by which multiple variations of images will be found
|
12
|
+
# ie: '_' for 'icon_hover.png'
|
13
|
+
# ie: '-' for 'icon-hover.png'
|
14
|
+
# if icon is the basename
|
15
|
+
# - align:
|
16
|
+
# - sprite_location: will be available as variable in the liquid template, if this is not set, the template will use output as sprite_location
|
17
|
+
# - tile: if set to ie. '100x100' it will center every image on a 100 by 100 tile
|
18
|
+
# - template: Liquid template for each sprite, use this to build the css for your sprites
|
19
|
+
# these variables are available:
|
20
|
+
# - top: distance to top border of sprite in pixels
|
21
|
+
# - left: distance to left border of sprite in pixels
|
22
|
+
# - width: width of current image in pixels
|
23
|
+
# - height: height of current image in pixels
|
24
|
+
# - basename: filename or basename of variations
|
25
|
+
# ie: with variations: icon_out.png, icon_over.png => icon
|
26
|
+
# ie: without variations: icon.png => icon.png
|
27
|
+
# - file_basename: always the name of the current image without extension
|
28
|
+
# ie: icon_over
|
29
|
+
# - filename: icon_over.png
|
30
|
+
# - full_filename: ../images/icons/icon_over.ong
|
31
|
+
# - variations: number of variations as number
|
32
|
+
# - variation: the current variation as zero based number
|
33
|
+
# - sprite_location: path to sprite
|
34
|
+
def initialize(files_or_paths, output, root, options = {})
|
35
|
+
@files = find_files(files_or_paths)
|
36
|
+
return if @files.nil? || @files.empty?
|
37
|
+
@root = root || ''
|
38
|
+
@output = output
|
39
|
+
@delimiter = options[:delimiter] || '-'
|
40
|
+
@distribution = (options[:distribution] || :smart).to_sym
|
41
|
+
@analyzed = find_files_for_mode
|
42
|
+
@template = Liquid::Template.parse(options[:template] || '')
|
43
|
+
@sprite_location = options[:sprite_location] || @output
|
44
|
+
@background = options[:background] || '#FFFFFF00'
|
45
|
+
@tile_size = options[:tile]
|
46
|
+
@alignment = options[:alignment] ? Magick.const_get("#{camelize(options[:alignment])}Gravity") : Magick::CenterGravity
|
47
|
+
end
|
48
|
+
|
49
|
+
def create
|
50
|
+
raise 'No files found.' if @files.nil? || @files.empty?
|
51
|
+
build_tile
|
52
|
+
destination = @root.nil? || @root.empty? ? @output : File.join(@root, @output)
|
53
|
+
image, css = build_sprite_and_css
|
54
|
+
background = @background
|
55
|
+
image.write(destination){ self.background_color = background }
|
56
|
+
css
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
def build_sprite_and_css
|
62
|
+
background = @background
|
63
|
+
@images = ImageList.new{ self.background_color = background }
|
64
|
+
context = { 'sprite_location' => @sprite_location }
|
65
|
+
@css = []
|
66
|
+
|
67
|
+
@analyzed.keys.sort.each do |key|
|
68
|
+
value = @analyzed[key]
|
69
|
+
|
70
|
+
context['top'] = 0
|
71
|
+
context['basename'] = key
|
72
|
+
context['overall'] = @images.length
|
73
|
+
if value.size > 1
|
74
|
+
context = build_image_list(context, value.sort)
|
75
|
+
else
|
76
|
+
context = build_single_image(context, value.flatten.first)
|
77
|
+
end
|
78
|
+
@css << build_css(context)
|
79
|
+
end
|
80
|
+
[@images.append(false), @css.join("\n")]
|
81
|
+
end
|
82
|
+
|
83
|
+
def build_single_image(context, filename)
|
84
|
+
background = @background
|
85
|
+
image = Image.read(filename){ self.background_color = background }.flatten.first
|
86
|
+
|
87
|
+
context.merge!(build_context_for_single_image(image, filename, context['basename']))
|
88
|
+
if @tile
|
89
|
+
@images.from_blob(@tile.composite(image, @alignment, Magick::OverCompositeOp).to_blob){ self.background_color = background }
|
90
|
+
else
|
91
|
+
@images.from_blob(image.to_blob){ self.background_color = background }
|
92
|
+
end
|
93
|
+
context
|
94
|
+
end
|
95
|
+
|
96
|
+
def build_context_for_single_image(image, filename, basename)
|
97
|
+
file_basename = File.basename(filename, '.*')
|
98
|
+
{
|
99
|
+
'variations' => 0,
|
100
|
+
'variation_number' => 0,
|
101
|
+
'full_filename' => filename,
|
102
|
+
'file_basename' => file_basename,
|
103
|
+
'variation_name' => file_basename.gsub("#{basename}#{@delimiter}", ''),
|
104
|
+
'width' => @tile ? @tile.columns : image.columns,
|
105
|
+
'height' => @tile ? @tile.rows : image.rows,
|
106
|
+
'type' => :image,
|
107
|
+
'left' => left_value_for_context
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
def left_value_for_context
|
112
|
+
@tile ? (@images.length * @tile.columns) : (@images.any? ? @images.append(false).columns : 0)
|
113
|
+
end
|
114
|
+
|
115
|
+
def build_image_list(context, image_filenames)
|
116
|
+
background = @background
|
117
|
+
image_list = ImageList.new(*image_filenames){ self.background_color = background }
|
118
|
+
context.merge!(build_context_for_image_list(image_list, image_filenames))
|
119
|
+
if @tile
|
120
|
+
tiles = ImageList.new{ self.background_color = background }
|
121
|
+
image_list.each do |image|
|
122
|
+
tiles << @tile.composite(image, @alignment, Magick::OverCompositeOp)
|
123
|
+
end
|
124
|
+
append_to_sprite(tiles)
|
125
|
+
else
|
126
|
+
append_to_sprite(image_list)
|
127
|
+
end
|
128
|
+
context
|
129
|
+
end
|
130
|
+
|
131
|
+
def append_to_sprite(images)
|
132
|
+
background = @background
|
133
|
+
@images.from_blob(images.append(true).to_blob){ self.background_color = background }
|
134
|
+
end
|
135
|
+
|
136
|
+
def build_context_for_image_list(image_list, image_filenames)
|
137
|
+
{
|
138
|
+
'variations' => image_list.length,
|
139
|
+
'type' => :list,
|
140
|
+
'images' => image_list,
|
141
|
+
'filenames' => image_filenames,
|
142
|
+
'left' => left_value_for_context
|
143
|
+
}
|
144
|
+
end
|
145
|
+
|
146
|
+
def build_tile
|
147
|
+
return if @tile_size.nil?
|
148
|
+
background = @background
|
149
|
+
size_x, size_y = @tile_size.split('x').first(2).map{|dim| dim.to_i}
|
150
|
+
@tile = Magick::Image.new(size_x, size_y){ self.background_color = background }
|
151
|
+
@tile.format = "PNG"
|
152
|
+
end
|
153
|
+
|
154
|
+
def build_css(context = {})
|
155
|
+
type = context['type']
|
156
|
+
case type
|
157
|
+
when :list
|
158
|
+
css = build_css_for_list(context)
|
159
|
+
when :image
|
160
|
+
# render template if there is only one image
|
161
|
+
css = @template.render(context)
|
162
|
+
end
|
163
|
+
css
|
164
|
+
end
|
165
|
+
|
166
|
+
def build_css_for_list(context)
|
167
|
+
new_context = context.dup
|
168
|
+
image_list = context.delete('images')
|
169
|
+
new_context['type'] = :image
|
170
|
+
css = image_list.inject([]) do |css, image|
|
171
|
+
new_context['width'] = @tile ? @tile.columns : image.columns
|
172
|
+
new_context['height'] = @tile ? @tile.rows : image.rows
|
173
|
+
new_context['variation_number'] = css.size
|
174
|
+
new_context['full_filename'] = context['filenames'].shift
|
175
|
+
new_context['filename'] = File.basename(new_context['full_filename'])
|
176
|
+
new_context['file_basename'] = File.basename(new_context['full_filename'], '.*')
|
177
|
+
new_context['variation_name'] = new_context['file_basename'].gsub(/^#{new_context['basename']}#{@delimiter}/, '')
|
178
|
+
css << build_css(new_context.dup)
|
179
|
+
|
180
|
+
new_context['top'] += @tile ? @tile.rows : new_context['height']
|
181
|
+
css
|
182
|
+
end.join("\n")
|
183
|
+
end
|
184
|
+
|
185
|
+
# gather files that will be used to create the sprite
|
186
|
+
def find_files(*args)
|
187
|
+
args.inject([]) do |files, arg|
|
188
|
+
found_files = Dir.glob(arg)
|
189
|
+
if found_files.empty?
|
190
|
+
files << arg if File.exists?(arg) rescue raise arg.inspect
|
191
|
+
else
|
192
|
+
files << found_files.flatten
|
193
|
+
end
|
194
|
+
files.flatten.compact.uniq
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# put filenames in format for each mode
|
199
|
+
def find_files_for_mode
|
200
|
+
case @distribution
|
201
|
+
when :smart
|
202
|
+
smart_distribution
|
203
|
+
when :horizontal
|
204
|
+
horizontal_distribution
|
205
|
+
when :vertical
|
206
|
+
vertical_distribution
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# gather information about the selected files
|
211
|
+
# check for variations by using a delimiter as an indicator
|
212
|
+
def smart_distribution
|
213
|
+
@files.inject(Hash.new{|hash, key| hash[key] = Array.new;}) do |h, file|
|
214
|
+
basename = File.basename(file, '.*')
|
215
|
+
without_variation = basename.split(@delimiter)[0..-2].join(@delimiter)
|
216
|
+
basename = without_variation.nil? || without_variation == '' ? basename : without_variation
|
217
|
+
h[basename] << file
|
218
|
+
h
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def horizontal_distribution
|
223
|
+
@files.inject(Hash.new{|hash, key| hash[key] = Array.new;}) do |h, file|
|
224
|
+
basename = File.basename(file)
|
225
|
+
h[basename] << file
|
226
|
+
h
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def vertical_distribution
|
231
|
+
@files.inject(Hash.new{|hash, key| hash[key] = Array.new;}) do |h, file|
|
232
|
+
basename = File.basename(file)
|
233
|
+
h['__all__'] << file
|
234
|
+
h
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# simplyfied version of active_support's camelize version
|
239
|
+
def camelize(lower_case_and_underscored_word)
|
240
|
+
lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
data/test/test_helper.rb
ADDED
@@ -1,10 +1,9 @@
|
|
1
|
-
require '
|
2
|
-
require File.expand_path(File.dirname(__FILE__) + '/../../lib/sprite_batch_generator')
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
3
2
|
|
4
|
-
class
|
3
|
+
class BatchTest < Test::Unit::TestCase
|
5
4
|
|
6
5
|
def setup
|
7
|
-
@config = 'test
|
6
|
+
@config = File.join(Sprites::Config.root, 'test', 'config', 'batch.yml')
|
8
7
|
end
|
9
8
|
|
10
9
|
def teardown
|
@@ -12,14 +11,14 @@ class SpriteBatchGeneratorTest < Test::Unit::TestCase
|
|
12
11
|
Dir.glob('test/output/*').each{|f| File.delete f }
|
13
12
|
end
|
14
13
|
|
15
|
-
|
16
|
-
@batch =
|
14
|
+
should "read config file" do
|
15
|
+
@batch = Sprites::Batch.new(@config)
|
17
16
|
assert_not_nil @batch
|
18
17
|
assert_equal 2, @batch.batches.size
|
19
18
|
end
|
20
19
|
|
21
|
-
|
22
|
-
@batch =
|
20
|
+
should "create files from config" do
|
21
|
+
@batch = Sprites::Batch.new(@config)
|
23
22
|
css = @batch.generate
|
24
23
|
assert_not_nil css
|
25
24
|
output_files = Dir.glob('test/output/*')
|
@@ -1,24 +1,20 @@
|
|
1
|
-
require '
|
2
|
-
|
3
|
-
|
4
|
-
require File.expand_path(File.dirname(__FILE__) + '/../../lib/sprite_generator')
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
2
|
+
|
3
|
+
class GeneratorTest < Test::Unit::TestCase
|
5
4
|
|
6
|
-
class SpriteGeneratorTest < Test::Unit::TestCase
|
7
|
-
|
8
5
|
def setup
|
9
6
|
@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
|
11
|
-
@output = 'test
|
12
|
-
@page_path = 'test
|
7
|
+
@all_images_path = File.join(Sprites::Config.root, 'test', 'images', '*.png')
|
8
|
+
@output = File.join(Sprites::Config.root, 'test', 'output', 'sprite_by_create.png')
|
9
|
+
@page_path = File.join(Sprites::Config.root, 'test', 'output', 'test.html')
|
13
10
|
end
|
14
11
|
|
15
|
-
|
16
12
|
def teardown
|
17
13
|
# delete test output
|
18
|
-
Dir.glob('test
|
14
|
+
Dir.glob(File.join(Sprites::Config.root, 'test', 'output', '*')).each{|f| File.delete f }
|
19
15
|
end
|
20
16
|
|
21
|
-
|
17
|
+
should "create correct sprite for tile with vertical distribution" do
|
22
18
|
options = {
|
23
19
|
:distribution => 'vertical',
|
24
20
|
:tile => '40x300',
|
@@ -26,7 +22,7 @@ class SpriteGeneratorTest < Test::Unit::TestCase
|
|
26
22
|
:template => "a.{{file_basename}} { padding-left:2em; background: transparent url(#{this_method}.png) -{{left}}px -{{top}}px no-repeat; }"
|
27
23
|
}
|
28
24
|
|
29
|
-
generator =
|
25
|
+
generator = Sprites::Generator.new(@all_images_path, "test/output/#{this_method}.png", nil, options)
|
30
26
|
css = generator.create
|
31
27
|
assert css.include?('-4500px')
|
32
28
|
|
@@ -36,28 +32,7 @@ class SpriteGeneratorTest < Test::Unit::TestCase
|
|
36
32
|
assert File.exists?(output_file)
|
37
33
|
end
|
38
34
|
|
39
|
-
|
40
|
-
def test_should_create_correct_sprite_for_tile_with_vertical_distribution
|
41
|
-
options = {
|
42
|
-
:distribution => 'vertical',
|
43
|
-
:tile => '40x300',
|
44
|
-
:alignment => 'north_west',
|
45
|
-
:template => "a.{{file_basename}} { padding-left:2em; background: transparent url(#{this_method}.png) -{{left}}px -{{top}}px no-repeat; }"
|
46
|
-
}
|
47
|
-
|
48
|
-
generator = SpriteGenerator.new(@all_images_path, "test/output/#{this_method}.png", nil, options)
|
49
|
-
css = generator.create
|
50
|
-
assert css.include?('-4500px')
|
51
|
-
|
52
|
-
page = Liquid::Template.parse(File.open('test/templates/link.html').read).render('css' => css, 'image' => "#{this_method}.png")
|
53
|
-
output_file = File.join('test', 'output', "#{this_method}.html")
|
54
|
-
File.open(output_file, 'w+'){ |f| f.puts page }
|
55
|
-
assert File.exists?(output_file)
|
56
|
-
end
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
def test_should_create_correct_sprite_for_tile_with_horizontal_distribution
|
35
|
+
should "create correct sprite for tile with horizontal distribution" do
|
61
36
|
options = {
|
62
37
|
:distribution => 'horizontal',
|
63
38
|
:tile => '400x50',
|
@@ -65,7 +40,7 @@ class SpriteGeneratorTest < Test::Unit::TestCase
|
|
65
40
|
:template => "a.{{file_basename}} { padding-left:2em; background: transparent url(#{this_method}.png) -{{left}}px -{{top}}px no-repeat; }"
|
66
41
|
}
|
67
42
|
|
68
|
-
generator =
|
43
|
+
generator = Sprites::Generator.new(@all_images_path, "test/output/#{this_method}.png", nil, options)
|
69
44
|
css = generator.create
|
70
45
|
assert css.include?('-6000px')
|
71
46
|
|
@@ -76,42 +51,42 @@ class SpriteGeneratorTest < Test::Unit::TestCase
|
|
76
51
|
end
|
77
52
|
|
78
53
|
|
79
|
-
|
54
|
+
should "use horizontal distribution" do
|
80
55
|
template = %q{ {{left}} }
|
81
|
-
generator =
|
56
|
+
generator = Sprites::Generator.new(@all_images_path, @output, nil, { :template => template, :distribution => 'horizontal' })
|
82
57
|
css = generator.create
|
83
58
|
assert css.include?('240')
|
84
59
|
end
|
85
60
|
|
86
|
-
|
61
|
+
should "use vertical distribution" do
|
87
62
|
template = %q{ {{top}} }
|
88
|
-
generator =
|
63
|
+
generator = Sprites::Generator.new(@all_images_path, @output, nil, { :template => template, :distribution => 'vertical' })
|
89
64
|
css = generator.create
|
90
65
|
assert css.include?('240')
|
91
66
|
end
|
92
67
|
|
93
|
-
|
68
|
+
should "set correct context filebasename for images without variations" do
|
94
69
|
template = %q{ {{file_basename}} }
|
95
|
-
generator =
|
70
|
+
generator = Sprites::Generator.new(@all_images_path, @output, nil, { :template => template, :delimiter => '_' })
|
96
71
|
css = generator.create
|
97
72
|
assert css.include?('emoticon-evilgrin')
|
98
73
|
end
|
99
74
|
|
100
|
-
|
75
|
+
should "set correct context width for images without variations" do
|
101
76
|
template = %q{ {{width}} }
|
102
|
-
generator =
|
77
|
+
generator = Sprites::Generator.new(@all_images_path, @output, nil, { :template => template, :delimiter => '_' })
|
103
78
|
css = generator.create
|
104
79
|
assert css.include?('16')
|
105
80
|
end
|
106
81
|
|
107
|
-
|
82
|
+
should "set correct context top for images without variations" do
|
108
83
|
template = %q{ {{top}} }
|
109
|
-
generator =
|
84
|
+
generator = Sprites::Generator.new(@all_images_path, @output, nil, { :template => template, :delimiter => '_' })
|
110
85
|
css = generator.create
|
111
86
|
assert !css.include?('-16')
|
112
87
|
end
|
113
88
|
|
114
|
-
|
89
|
+
should "create correct context" do
|
115
90
|
template = %q{
|
116
91
|
basename: {{basename}}
|
117
92
|
variation: {{variation}}
|
@@ -127,27 +102,27 @@ class SpriteGeneratorTest < Test::Unit::TestCase
|
|
127
102
|
variation_number: {{variation_number}}
|
128
103
|
variation_name: {{variation_name}}
|
129
104
|
}
|
130
|
-
generator =
|
105
|
+
generator = Sprites::Generator.new(@all_images_path, @output, nil, {:template => template})
|
131
106
|
css = generator.create
|
132
107
|
assert css.include?('basename: emoticon-evilgrin')
|
133
108
|
assert css.include?('variation_name: evilgrin')
|
134
109
|
assert css.include?('file_basename: emoticon-evilgrin')
|
135
110
|
end
|
136
111
|
|
137
|
-
|
112
|
+
should "use alignment option" do
|
138
113
|
assert_nothing_raised do
|
139
|
-
|
114
|
+
Sprites::Generator.new(@all_images_path, @output, nil, {:template => @template, :tile => '100x100', :background => '#FFFFFF', :alignment => 'west'})
|
140
115
|
end
|
141
116
|
end
|
142
117
|
|
143
|
-
|
118
|
+
should "complain about unknown alignment option" do
|
144
119
|
assert_raise NameError do
|
145
|
-
|
120
|
+
Sprites::Generator.new(@all_images_path, @output, nil, {:template => @template, :tile => '100x100', :background => '#FFFFFF', :alignment => 'somewhere'})
|
146
121
|
end
|
147
122
|
end
|
148
123
|
|
149
|
-
|
150
|
-
@generator =
|
124
|
+
should "center images on tiles" do
|
125
|
+
@generator = Sprites::Generator.new(@all_images_path, @output, nil, {:template => @template, :tile => '100x100', :background => '#FFFFFF00'})
|
151
126
|
css = @generator.create
|
152
127
|
page = Liquid::Template.parse(File.open('test/templates/test.html').read).render('css' => css)
|
153
128
|
assert page.include?("{ background:")
|
@@ -160,8 +135,8 @@ class SpriteGeneratorTest < Test::Unit::TestCase
|
|
160
135
|
end
|
161
136
|
|
162
137
|
|
163
|
-
|
164
|
-
@generator =
|
138
|
+
should "create correct css" do
|
139
|
+
@generator = Sprites::Generator.new(@all_images_path, @output, nil, :template => @template)
|
165
140
|
css = @generator.create()
|
166
141
|
page = Liquid::Template.parse(File.open('test/templates/test.html').read).render('css' => css)
|
167
142
|
assert page.include?("{ background:")
|
@@ -174,44 +149,44 @@ class SpriteGeneratorTest < Test::Unit::TestCase
|
|
174
149
|
end
|
175
150
|
|
176
151
|
|
177
|
-
|
178
|
-
@generator =
|
152
|
+
should "generate sprite file" do
|
153
|
+
@generator = Sprites::Generator.new(@all_images_path, @output, nil, :template => @template)
|
179
154
|
css = @generator.create
|
180
155
|
assert !(css.nil? || css.empty?)
|
181
156
|
assert File.exists?(@output)
|
182
157
|
end
|
183
158
|
|
184
159
|
# bad, testing internal state
|
185
|
-
|
160
|
+
should "find versions of emoticons" do
|
186
161
|
files = Dir.glob(@all_images_path)
|
187
|
-
@generator =
|
162
|
+
@generator = Sprites::Generator.new(files, @output, nil, {})
|
188
163
|
analyzed = @generator.instance_variable_get(:@analyzed)
|
189
164
|
assert_equal 9, analyzed['emoticon'].size
|
190
165
|
end
|
191
166
|
|
192
167
|
# bad, using internal state
|
193
|
-
|
194
|
-
@generator =
|
168
|
+
should "find files for glob path" do
|
169
|
+
@generator = Sprites::Generator.new(@all_images_path, @output, nil, {})
|
195
170
|
files = @generator.instance_variable_get(:@files)
|
196
171
|
assert_equal 16, files.size
|
197
172
|
end
|
198
173
|
|
199
174
|
# bad, using internal state
|
200
|
-
|
201
|
-
@generator =
|
175
|
+
should "find files" do
|
176
|
+
@generator = Sprites::Generator.new(['test/images/emoticon-evilgrin.png', 'test/images/emoticon-grin.png'], @output, nil, {})
|
202
177
|
files = @generator.instance_variable_get(:@files)
|
203
178
|
assert_equal 2, files.size
|
204
179
|
end
|
205
180
|
|
206
181
|
# bad, using internal state
|
207
|
-
|
208
|
-
@generator =
|
182
|
+
should "not find anything" do
|
183
|
+
@generator = Sprites::Generator.new('test/blalala/*.hurz', @output, nil, {})
|
209
184
|
files = @generator.instance_variable_get(:@files)
|
210
185
|
assert_equal 0, files.size
|
211
186
|
end
|
212
187
|
|
213
188
|
protected
|
214
|
-
|
189
|
+
# output filename based on test name for inspection
|
215
190
|
def this_method
|
216
191
|
caller[0] =~ /`([^']*)'/ and $1
|
217
192
|
end
|
metadata
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sprite_generator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 3
|
8
|
+
- 0
|
9
|
+
version: 0.3.0
|
5
10
|
platform: ruby
|
6
11
|
authors:
|
7
12
|
- Marcel Scherf
|
@@ -9,30 +14,75 @@ autorequire:
|
|
9
14
|
bindir: bin
|
10
15
|
cert_chain: []
|
11
16
|
|
12
|
-
date:
|
17
|
+
date: 2011-01-18 00:00:00 +01:00
|
13
18
|
default_executable:
|
14
19
|
dependencies:
|
15
20
|
- !ruby/object:Gem::Dependency
|
16
21
|
name: liquid
|
17
|
-
|
18
|
-
|
19
|
-
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
none: false
|
20
24
|
requirements:
|
21
25
|
- - ">="
|
22
26
|
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
23
29
|
version: "0"
|
24
|
-
|
30
|
+
type: :runtime
|
31
|
+
prerelease: false
|
32
|
+
version_requirements: *id001
|
25
33
|
- !ruby/object:Gem::Dependency
|
26
34
|
name: rmagick
|
35
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
36
|
+
none: false
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
segments:
|
41
|
+
- 0
|
42
|
+
version: "0"
|
27
43
|
type: :runtime
|
28
|
-
|
29
|
-
version_requirements:
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: *id002
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: jeweler
|
48
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
30
50
|
requirements:
|
31
51
|
- - ">="
|
32
52
|
- !ruby/object:Gem::Version
|
53
|
+
segments:
|
54
|
+
- 0
|
33
55
|
version: "0"
|
34
|
-
|
35
|
-
|
56
|
+
type: :runtime
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: *id003
|
59
|
+
- !ruby/object:Gem::Dependency
|
60
|
+
name: liquid
|
61
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
segments:
|
67
|
+
- 0
|
68
|
+
version: "0"
|
69
|
+
type: :runtime
|
70
|
+
prerelease: false
|
71
|
+
version_requirements: *id004
|
72
|
+
- !ruby/object:Gem::Dependency
|
73
|
+
name: rmagick
|
74
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
segments:
|
80
|
+
- 0
|
81
|
+
version: "0"
|
82
|
+
type: :runtime
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: *id005
|
85
|
+
description: Automatically generate Sprite Images and the corresponding CSS.
|
36
86
|
email: marcel.scherf@gmail.com
|
37
87
|
executables: []
|
38
88
|
|
@@ -41,37 +91,46 @@ extensions: []
|
|
41
91
|
extra_rdoc_files:
|
42
92
|
- README.txt
|
43
93
|
files:
|
44
|
-
- lib/
|
45
|
-
- lib/
|
94
|
+
- lib/sprites.rb
|
95
|
+
- lib/sprites/batch.rb
|
96
|
+
- lib/sprites/generator.rb
|
46
97
|
- README.txt
|
98
|
+
- test/test_helper.rb
|
99
|
+
- test/units/batch_test.rb
|
100
|
+
- test/units/generator_test.rb
|
47
101
|
has_rdoc: true
|
48
102
|
homepage: http://github.com/the-architect/spritegenerator
|
49
103
|
licenses: []
|
50
104
|
|
51
105
|
post_install_message:
|
52
|
-
rdoc_options:
|
53
|
-
|
106
|
+
rdoc_options: []
|
107
|
+
|
54
108
|
require_paths:
|
55
109
|
- lib
|
56
110
|
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
none: false
|
57
112
|
requirements:
|
58
113
|
- - ">="
|
59
114
|
- !ruby/object:Gem::Version
|
115
|
+
segments:
|
116
|
+
- 0
|
60
117
|
version: "0"
|
61
|
-
version:
|
62
118
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
119
|
+
none: false
|
63
120
|
requirements:
|
64
121
|
- - ">="
|
65
122
|
- !ruby/object:Gem::Version
|
123
|
+
segments:
|
124
|
+
- 0
|
66
125
|
version: "0"
|
67
|
-
version:
|
68
126
|
requirements: []
|
69
127
|
|
70
128
|
rubyforge_project:
|
71
|
-
rubygems_version: 1.3.
|
129
|
+
rubygems_version: 1.3.7
|
72
130
|
signing_key:
|
73
131
|
specification_version: 3
|
74
132
|
summary: Automatically generate Sprite Images and the corresponding CSS using RMagick and Liquid.
|
75
133
|
test_files:
|
76
|
-
- test/
|
77
|
-
- test/units/
|
134
|
+
- test/test_helper.rb
|
135
|
+
- test/units/batch_test.rb
|
136
|
+
- test/units/generator_test.rb
|
@@ -1,37 +0,0 @@
|
|
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
|
data/lib/sprite_generator.rb
DELETED
@@ -1,264 +0,0 @@
|
|
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
|
-
@distribution = (options[:distribution] || :smart).to_sym
|
46
|
-
@analyzed = find_files_for_mode
|
47
|
-
@template = Liquid::Template.parse(options[:template] || '')
|
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
|
-
build_tile
|
58
|
-
destination = @root.nil? || @root.empty? ? @output : File.join(@root, @output)
|
59
|
-
image, css = build_sprite_and_css
|
60
|
-
background = @background
|
61
|
-
image.write(destination){ self.background_color = background }
|
62
|
-
css
|
63
|
-
end
|
64
|
-
|
65
|
-
protected
|
66
|
-
|
67
|
-
def build_sprite_and_css
|
68
|
-
background = @background
|
69
|
-
@images = ImageList.new{ self.background_color = background }
|
70
|
-
context = { 'sprite_location' => @sprite_location }
|
71
|
-
@css = []
|
72
|
-
|
73
|
-
@analyzed.keys.sort.each do |key|
|
74
|
-
value = @analyzed[key]
|
75
|
-
|
76
|
-
context['top'] = 0
|
77
|
-
context['basename'] = key
|
78
|
-
context['overall'] = @images.length
|
79
|
-
if value.size > 1
|
80
|
-
context = build_image_list(context, value.sort)
|
81
|
-
else
|
82
|
-
context = build_single_image(context, value.flatten.first)
|
83
|
-
end
|
84
|
-
@css << build_css(context)
|
85
|
-
end
|
86
|
-
[@images.append(false), @css.join("\n")]
|
87
|
-
end
|
88
|
-
|
89
|
-
|
90
|
-
def build_single_image(context, filename)
|
91
|
-
background = @background
|
92
|
-
image = Image.read(filename){ self.background_color = background }.flatten.first
|
93
|
-
|
94
|
-
context.merge!(build_context_for_single_image(image, filename, context['basename']))
|
95
|
-
if @tile
|
96
|
-
@images.from_blob(@tile.composite(image, @alignment, Magick::OverCompositeOp).to_blob){ self.background_color = background }
|
97
|
-
else
|
98
|
-
@images.from_blob(image.to_blob){ self.background_color = background }
|
99
|
-
end
|
100
|
-
context
|
101
|
-
end
|
102
|
-
|
103
|
-
|
104
|
-
def build_context_for_single_image(image, filename, basename)
|
105
|
-
file_basename = File.basename(filename, '.*')
|
106
|
-
{
|
107
|
-
'variations' => 0,
|
108
|
-
'variation_number' => 0,
|
109
|
-
'full_filename' => filename,
|
110
|
-
'file_basename' => file_basename,
|
111
|
-
'variation_name' => file_basename.gsub("#{basename}#{@delimiter}", ''),
|
112
|
-
'width' => @tile ? @tile.columns : image.columns,
|
113
|
-
'height' => @tile ? @tile.rows : image.rows,
|
114
|
-
'type' => :image,
|
115
|
-
'left' => left_value_for_context
|
116
|
-
}
|
117
|
-
end
|
118
|
-
|
119
|
-
|
120
|
-
def left_value_for_context
|
121
|
-
@tile ? (@images.length * @tile.columns) : (@images.any? ? @images.append(false).columns : 0)
|
122
|
-
end
|
123
|
-
|
124
|
-
|
125
|
-
def build_image_list(context, image_filenames)
|
126
|
-
background = @background
|
127
|
-
image_list = ImageList.new(*image_filenames){ self.background_color = background }
|
128
|
-
context.merge!(build_context_for_image_list(image_list, image_filenames))
|
129
|
-
if @tile
|
130
|
-
tiles = ImageList.new{ self.background_color = background }
|
131
|
-
image_list.each do |image|
|
132
|
-
tiles << @tile.composite(image, @alignment, Magick::OverCompositeOp)
|
133
|
-
end
|
134
|
-
append_to_sprite(tiles)
|
135
|
-
else
|
136
|
-
append_to_sprite(image_list)
|
137
|
-
end
|
138
|
-
context
|
139
|
-
end
|
140
|
-
|
141
|
-
|
142
|
-
def append_to_sprite(images)
|
143
|
-
background = @background
|
144
|
-
@images.from_blob(images.append(true).to_blob){ self.background_color = background }
|
145
|
-
end
|
146
|
-
|
147
|
-
|
148
|
-
def build_context_for_image_list(image_list, image_filenames)
|
149
|
-
{
|
150
|
-
'variations' => image_list.length,
|
151
|
-
'type' => :list,
|
152
|
-
'images' => image_list,
|
153
|
-
'filenames' => image_filenames,
|
154
|
-
'left' => left_value_for_context
|
155
|
-
}
|
156
|
-
end
|
157
|
-
|
158
|
-
|
159
|
-
def build_tile
|
160
|
-
return if @tile_size.nil?
|
161
|
-
background = @background
|
162
|
-
size_x, size_y = @tile_size.split('x').first(2).map{|dim| dim.to_i}
|
163
|
-
@tile = Magick::Image.new(size_x, size_y){ self.background_color = background }
|
164
|
-
@tile.format = "PNG"
|
165
|
-
end
|
166
|
-
|
167
|
-
|
168
|
-
def build_css(context = {})
|
169
|
-
type = context['type']
|
170
|
-
case type
|
171
|
-
when :list
|
172
|
-
css = build_css_for_list(context)
|
173
|
-
when :image
|
174
|
-
# render template if there is only one image
|
175
|
-
css = @template.render(context)
|
176
|
-
end
|
177
|
-
css
|
178
|
-
end
|
179
|
-
|
180
|
-
|
181
|
-
def build_css_for_list(context)
|
182
|
-
new_context = context.dup
|
183
|
-
image_list = context.delete('images')
|
184
|
-
new_context['type'] = :image
|
185
|
-
css = image_list.inject([]) do |css, image|
|
186
|
-
new_context['width'] = @tile ? @tile.columns : image.columns
|
187
|
-
new_context['height'] = @tile ? @tile.rows : image.rows
|
188
|
-
new_context['variation_number'] = css.size
|
189
|
-
new_context['full_filename'] = context['filenames'].shift
|
190
|
-
new_context['filename'] = File.basename(new_context['full_filename'])
|
191
|
-
new_context['file_basename'] = File.basename(new_context['full_filename'], '.*')
|
192
|
-
new_context['variation_name'] = new_context['file_basename'].gsub(/^#{new_context['basename']}#{@delimiter}/, '')
|
193
|
-
css << build_css(new_context.dup)
|
194
|
-
|
195
|
-
new_context['top'] += @tile ? @tile.rows : new_context['height']
|
196
|
-
css
|
197
|
-
end.join("\n")
|
198
|
-
end
|
199
|
-
|
200
|
-
|
201
|
-
# gather files that will be used to create the sprite
|
202
|
-
def find_files(*args)
|
203
|
-
args.inject([]) do |files, arg|
|
204
|
-
found_files = Dir.glob(arg)
|
205
|
-
if found_files.empty?
|
206
|
-
files << arg if File.exists?(arg) rescue raise arg.inspect
|
207
|
-
else
|
208
|
-
files << found_files.flatten
|
209
|
-
end
|
210
|
-
files.flatten.compact.uniq
|
211
|
-
end
|
212
|
-
end
|
213
|
-
|
214
|
-
|
215
|
-
# put filenames in format for each mode
|
216
|
-
def find_files_for_mode
|
217
|
-
case @distribution
|
218
|
-
when :smart
|
219
|
-
smart_distribution
|
220
|
-
when :horizontal
|
221
|
-
horizontal_distribution
|
222
|
-
when :vertical
|
223
|
-
vertical_distribution
|
224
|
-
end
|
225
|
-
end
|
226
|
-
|
227
|
-
|
228
|
-
# gather information about the selected files
|
229
|
-
# check for variations by using a delimiter as an indicator
|
230
|
-
def smart_distribution
|
231
|
-
@files.inject(Hash.new{|hash, key| hash[key] = Array.new;}) do |h, file|
|
232
|
-
basename = File.basename(file, '.*')
|
233
|
-
without_variation = basename.split(@delimiter)[0..-2].join(@delimiter)
|
234
|
-
basename = without_variation.nil? || without_variation == '' ? basename : without_variation
|
235
|
-
h[basename] << file
|
236
|
-
h
|
237
|
-
end
|
238
|
-
end
|
239
|
-
|
240
|
-
|
241
|
-
def horizontal_distribution
|
242
|
-
@files.inject(Hash.new{|hash, key| hash[key] = Array.new;}) do |h, file|
|
243
|
-
basename = File.basename(file)
|
244
|
-
h[basename] << file
|
245
|
-
h
|
246
|
-
end
|
247
|
-
end
|
248
|
-
|
249
|
-
|
250
|
-
def vertical_distribution
|
251
|
-
@files.inject(Hash.new{|hash, key| hash[key] = Array.new;}) do |h, file|
|
252
|
-
basename = File.basename(file)
|
253
|
-
h['__all__'] << file
|
254
|
-
h
|
255
|
-
end
|
256
|
-
end
|
257
|
-
|
258
|
-
|
259
|
-
# simplyfied version of active_support's camelize version
|
260
|
-
def camelize(lower_case_and_underscored_word)
|
261
|
-
lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
262
|
-
end
|
263
|
-
|
264
|
-
end
|