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