sprite_generator 0.2.5 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|