sprite-factory 1.0.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ *.swp
6
+ .sass-cache
7
+ test/images/*.png
8
+ test/images/*.css
9
+ test/images/*.sass
10
+ test/images/*.scss
data/README.md CHANGED
@@ -12,6 +12,7 @@ The library provides:
12
12
  * many customizable options
13
13
  * support for any stylesheet syntax, including [CSS](http://www.w3.org/Style/CSS/) and [Sass](http://sass-lang.com/).
14
14
  * support for any image library, including [RMagick](http://rmagick.rubyforge.org/) and [ChunkyPNG](https://github.com/wvanbergen/chunky_png).
15
+ * support for pngcrush'n the generated image file
15
16
 
16
17
 
17
18
  Installation
@@ -20,11 +21,18 @@ Installation
20
21
  $ gem install sprite-factory
21
22
 
22
23
  An image library is also required. SpriteFactory comes with built in support for
23
- [RMagick](http://rmagick.rubyforge.org/) or
24
- [ChunkyPng](https://github.com/wvanbergen/chunky_png) and is easily extensible to
25
- use any image library of your choice.
24
+ [RMagick](http://rmagick.rubyforge.org/) or [ChunkyPng](https://github.com/wvanbergen/chunky_png).
26
25
 
27
- _(see below for instructions to install an image library if you don't already have one.)_
26
+ RMagick is the most common image libary to use, installation instructions for ubuntu:
27
+
28
+ $ sudo aptitude install imageMagick libMagickWand-dev
29
+ $ sudo gem install rmagick
30
+
31
+ ChunkyPng is lighter weight but only supports .png format:
32
+
33
+ $ gem install chunky_png
34
+
35
+ SpriteFactory can also be easily extended to use the image library of your choice.
28
36
 
29
37
  Usage
30
38
  =====
@@ -62,7 +70,7 @@ Customization
62
70
  Much of the behavior can be customized by overriding the following options:
63
71
 
64
72
  - `:output` - specify output location for generated files
65
- - `:layout` - specify layout algorithm (horizontal or vertical)
73
+ - `:layout` - specify layout algorithm (horizontal, vertical or packed)
66
74
  - `:style` - specify output style (css or sass)
67
75
  - `:library` - specify image library to use (rmagick or chunkypng)
68
76
  - `:selector` - specify custom css selector (see below)
@@ -70,18 +78,32 @@ Much of the behavior can be customized by overriding the following options:
70
78
  - `:padding` - add padding to each sprite
71
79
  - `:width` - fix width of each sprite to a specific size
72
80
  - `:height` - fix height of each sprite to a specific size
81
+ - `:pngcrush` - pngcrush the generated output image (if pngcrush is available)
73
82
 
74
83
  Options can be passed as command line arguments to the `sf` script:
75
84
 
76
- $ sf images/icons --style sass --layout vertical
85
+ $ sf images/icons --style sass --layout packed
77
86
 
78
87
  Options can also be passed as the 2nd argument to the `#run!` method:
79
88
 
80
- SpriteFactory.run!('images/icons', :style => :sass, :layout => :vertical)
89
+ SpriteFactory.run!('images/icons', :style => :sass, :layout => :packed)
81
90
 
82
91
  You can see the results of many of these options by viewing the sample page that
83
92
  comes with the gem in `test/images/reference/index.html`.
84
93
 
94
+ Layout
95
+ ======
96
+
97
+ The generated image can be laid out in a horizontal or a vertical strip by
98
+ providing a `:layout` option (defaults to horizontal). A **new option in v1.2.0** is
99
+ to use a **:packed** layout which will attempt to generate an optimized packed
100
+ square-ish layout.
101
+
102
+ For more details on the bin-packing algorithm used:
103
+
104
+ * You can find a [description here](http://codeincomplete.com/posts/2011/5/7/bin_packing/)
105
+ * You can find a [demo here](http://codeincomplete.com/posts/2011/5/7/bin_packing/example/)
106
+
85
107
  Customizing the CSS Selector
86
108
  ============================
87
109
 
@@ -113,7 +135,7 @@ building a Ruby on Rails application you might need to generate URL's using the
113
135
  helper method to ensure it gets the appopriate cache-busting query parameter.
114
136
 
115
137
  By default, the SpriteFactory generates simple url's that contain only the basename of the
116
- unified sprite image, but you can control the generation of these url's using the :csspath
138
+ unified sprite image, but you can control the generation of these url's using the `:csspath`
117
139
  option:
118
140
 
119
141
  For most CDN's, you can prepend a simple string to the image name:
@@ -171,37 +193,23 @@ The sprite factory library can also be extended in a number of other ways.
171
193
 
172
194
  _(see existing code for examples of each)._
173
195
 
174
- Installing an Image Library
175
- ===========================
176
-
177
- SpriteFactory comes with built in support for
178
- [RMagick](http://rmagick.rubyforge.org/) or
179
- [ChunkyPng](https://github.com/wvanbergen/chunky_png)
180
-
181
- RMagick is the most flexible image libary to use, but requires ImageMagick
182
- binaries, installation instructions for ubuntu:
183
-
184
- $ sudo aptitude install imageMagick libMagickWand-dev
185
- $ sudo gem install rmagick
186
-
187
- ChunkyPng is lighter weight and has no binary requirements, but only supports
188
- .png format. Installation is a simple gem install:
189
-
190
- $ gem install chunky_png
196
+ License
197
+ =======
191
198
 
192
- SpriteFactory can also be easily extended to use the image library of your choice.
199
+ See [LICENSE](https://github.com/jakesgordon/sprite-factory/blob/master/LICENSE) file.
193
200
 
194
- License
201
+ Credits
195
202
  =======
196
203
 
197
- See LICENSE file.
204
+ Thanks to my employer, [LiquidPlanner](http://liquidplanner.com) for allowing me to take this idea from our
205
+ online project management web application and release it into the wild.
198
206
 
199
207
  Contact
200
208
  =======
201
209
 
202
- You can reach me at [jake@codeincomplete.com](mailto:jake@codeincomplete.com), or via
203
- my website: [Code inComplete](http://codeincomplete.com).
204
-
210
+ If you have any ideas, feedback, requests or bug reports, you can reach me at
211
+ [jake@codeincomplete.com](mailto:jake@codeincomplete.com), or via
212
+ my website: [Code inComplete](http://codeincomplete.com/posts/2011/4/29/sprite_factory/).
205
213
 
206
214
 
207
215
 
data/Rakefile CHANGED
@@ -31,6 +31,7 @@ task :reference do
31
31
  regenerate.call('test/images/regular')
32
32
  regenerate.call('test/images/regular', :output => 'test/images/regular.horizontal', :selector => 'img.horizontal_', :layout => :horizontal)
33
33
  regenerate.call('test/images/regular', :output => 'test/images/regular.vertical', :selector => 'img.vertical_', :layout => :vertical)
34
+ regenerate.call('test/images/regular', :output => 'test/images/regular.packed', :selector => 'img.packed_', :layout => :packed)
34
35
  regenerate.call('test/images/regular', :output => 'test/images/regular.padded', :selector => 'img.padded_', :padding => 10)
35
36
  regenerate.call('test/images/regular', :output => 'test/images/regular.fixed', :selector => 'img.fixed_', :width => 100, :height => 100)
36
37
  regenerate.call('test/images/regular', :output => 'test/images/regular.sassy', :selector => 'img.sassy_', :style => :sass)
@@ -38,6 +39,7 @@ task :reference do
38
39
  regenerate.call('test/images/irregular')
39
40
  regenerate.call('test/images/irregular', :output => 'test/images/irregular.horizontal', :selector => 'img.horizontal_', :layout => :horizontal)
40
41
  regenerate.call('test/images/irregular', :output => 'test/images/irregular.vertical', :selector => 'img.vertical_', :layout => :vertical)
42
+ regenerate.call('test/images/irregular', :output => 'test/images/irregular.packed', :selector => 'img.packed_', :layout => :packed)
41
43
  regenerate.call('test/images/irregular', :output => 'test/images/irregular.padded', :selector => 'img.padded_', :padding => 10)
42
44
  regenerate.call('test/images/irregular', :output => 'test/images/irregular.fixed', :selector => 'img.fixed_', :width => 100, :height => 100)
43
45
  regenerate.call('test/images/irregular', :output => 'test/images/irregular.sassy', :selector => 'img.sassy_', :style => :sass)
data/bin/sf CHANGED
@@ -20,11 +20,12 @@ op.on("-v", "--version") do
20
20
  end
21
21
 
22
22
  output_help = "specify output location, without any extension"
23
- layout_help = "specify layout orientation ( horizontal, vertical )"
23
+ layout_help = "specify layout orientation ( horizontal, vertical, packed )"
24
24
  style_help = "specify output style format ( css, sass )"
25
25
  library_help = "specify image library to use ( rmagic, chunkypng )"
26
26
  selector_help = "specify custom selector to use for each css rule ( default: 'img.' )"
27
27
  csspath_help = "specify custom path to use for css image urls ( default: output file's basename )"
28
+ pngcrush_help = "use pngcrush to optimize generated image"
28
29
 
29
30
  op.on("--output [PATH]", output_help) {|value| options[:output] = value }
30
31
  op.on("--layout [ORIENTATION]", layout_help) {|value| options[:layout] = value }
@@ -32,6 +33,7 @@ op.on("--style [STYLE]", style_help) {|value| options[:style] = v
32
33
  op.on("--library [LIBRARY]", library_help) {|value| options[:library] = value }
33
34
  op.on("--selector [SELECTOR]", selector_help) {|value| options[:selector] = value }
34
35
  op.on("--csspath [CSSPATH]", csspath_help) {|value| options[:csspath] = value }
36
+ op.on("--pngcrush", pngcrush_help) {|value| options[:pngcrush] = value }
35
37
 
36
38
  begin
37
39
  op.parse!(ARGV)
@@ -39,7 +41,6 @@ begin
39
41
  SpriteFactory.run!(ARGV[0], options)
40
42
  rescue Exception => ex
41
43
  puts ex.message
42
- puts op.to_s
43
44
  exit
44
45
  end
45
46
 
@@ -2,14 +2,13 @@ module SpriteFactory
2
2
 
3
3
  #----------------------------------------------------------------------------
4
4
 
5
- VERSION = "1.0.0"
5
+ VERSION = "1.2.0"
6
6
  SUMMARY = "Automatic CSS sprite generator"
7
7
  DESCRIPTION = "Combines individual images from a directory into a single sprite image file and creates an appropriate CSS stylesheet"
8
8
  LIB = File.dirname(__FILE__)
9
9
 
10
10
  autoload :Runner, File.join(LIB, 'sprite_factory/runner') # controller that glues everything together
11
- autoload :Layout, File.join(LIB, 'sprite_factory/layout') # layout calculations
12
- autoload :Style, File.join(LIB, 'sprite_factory/style') # style generators
11
+ autoload :Style, File.join(LIB, 'sprite_factory/style') # style generators all live in a single module (for now)
13
12
 
14
13
  def self.run!(input, config = {}, &block)
15
14
  Runner.new(input, config).run!(&block)
@@ -26,6 +25,29 @@ module SpriteFactory
26
25
  attr_accessor :library
27
26
  attr_accessor :selector
28
27
  attr_accessor :csspath
28
+ attr_accessor :pngcrush
29
+ end
30
+
31
+ #----------------------------------------------------------------------------
32
+
33
+ module Layout # abstract module for various layout strategies
34
+
35
+ autoload :Horizontal, File.join(LIB, 'sprite_factory/layout/horizontal') # concrete module for layout in a single horizontal strip
36
+ autoload :Vertical, File.join(LIB, 'sprite_factory/layout/vertical') # concrete module for layout in a single vertical strip
37
+ autoload :Packed, File.join(LIB, 'sprite_factory/layout/packed') # concrete module for layout in a bin-packed square
38
+
39
+ def self.horizontal
40
+ Horizontal
41
+ end
42
+
43
+ def self.vertical
44
+ Vertical
45
+ end
46
+
47
+ def self.packed
48
+ Packed
49
+ end
50
+
29
51
  end
30
52
 
31
53
  #----------------------------------------------------------------------------
@@ -0,0 +1,42 @@
1
+ module SpriteFactory
2
+ module Layout
3
+ module Horizontal
4
+
5
+ def self.layout(images, options = {})
6
+ width = options[:width]
7
+ height = options[:height]
8
+ hpadding = options[:hpadding] || 0
9
+ vpadding = options[:vpadding] || 0
10
+ max_height = height || ((2 * vpadding) + images.map{|i| i[:height]}.max)
11
+ x = 0
12
+ images.each do |i|
13
+
14
+ if width
15
+ i[:cssw] = width
16
+ i[:cssx] = x
17
+ i[:x] = x + (width - i[:width]) / 2
18
+ else
19
+ i[:cssw] = i[:width] + (2 * hpadding) # image width plus padding
20
+ i[:cssx] = x # anchored at x
21
+ i[:x] = i[:cssx] + hpadding # image drawn offset to account for padding
22
+ end
23
+
24
+ if height
25
+ i[:cssh] = height
26
+ i[:cssy] = 0
27
+ i[:y] = 0 + (height - i[:height]) / 2
28
+ else
29
+ i[:cssh] = i[:height] + (2 * vpadding) # image height plus padding
30
+ i[:cssy] = (max_height - i[:cssh]) / 2 # centered vertically
31
+ i[:y] = i[:cssy] + vpadding # image drawn offset to account for padding
32
+ end
33
+
34
+ x = x + i[:cssw]
35
+
36
+ end
37
+ { :width => x, :height => max_height }
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,107 @@
1
+ module SpriteFactory
2
+ module Layout
3
+ module Packed
4
+
5
+ #------------------------------------------------------------------------
6
+
7
+ def self.layout(images, options = {})
8
+
9
+ raise NotImplementedError, ":packed layout does not support the :padding option" if (options[:padding].to_i > 0) || (options[:hpadding].to_i > 0) || (options[:vpadding].to_i > 0)
10
+ raise NotImplementedError, ":packed layout does not support fixed :width/:height option" if options[:width] || options[:height]
11
+
12
+ return { :width => 0, :height => 0 } if images.empty?
13
+
14
+ images.sort! do |a,b|
15
+ diff = [b[:width], b[:height]].max <=> [a[:width], a[:height]].max
16
+ diff = [b[:width], b[:height]].min <=> [a[:width], a[:height]].min if diff.zero?
17
+ diff = b[:height] <=> a[:height] if diff.zero?
18
+ diff = b[:width] <=> a[:width] if diff.zero?
19
+ diff
20
+ end
21
+
22
+ root = { :x => 0, :y => 0, :w => images[0][:width], :h => images[0][:height] }
23
+
24
+ images.each do |i|
25
+ if (node = findNode(root, i[:width], i[:height]))
26
+ placeImage(i, node)
27
+ splitNode(node, i[:width], i[:height])
28
+ else
29
+ root = grow(root, i[:width], i[:height])
30
+ redo
31
+ end
32
+ end
33
+
34
+ { :width => root[:w], :height => root[:h] }
35
+
36
+ end
37
+
38
+ def self.placeImage(image, node)
39
+ image[:cssx] = image[:x] = node[:x] # TODO
40
+ image[:cssy] = image[:y] = node[:y] # * support for :padding option
41
+ image[:cssw] = image[:width] # * support for fixed :width/:height option
42
+ image[:cssh] = image[:height]
43
+ end
44
+
45
+ def self.findNode(root, w, h)
46
+ if root[:used]
47
+ findNode(root[:right], w, h) || findNode(root[:down], w, h)
48
+ elsif (w <= root[:w]) && (h <= root[:h])
49
+ root
50
+ end
51
+ end
52
+
53
+ def self.splitNode(node, w, h)
54
+ node[:used] = true
55
+ node[:down] = { :x => node[:x], :y => node[:y] + h, :w => node[:w], :h => node[:h] - h }
56
+ node[:right] = { :x => node[:x] + w, :y => node[:y], :w => node[:w] - w, :h => h }
57
+ end
58
+
59
+ def self.grow(root, w, h)
60
+
61
+ canGrowDown = (w <= root[:w])
62
+ canGrowRight = (h <= root[:h])
63
+
64
+ shouldGrowRight = canGrowRight && (root[:h] >= (root[:w] + w))
65
+ shouldGrowDown = canGrowDown && (root[:w] >= (root[:h] + h))
66
+
67
+ if shouldGrowRight
68
+ growRight(root, w, h)
69
+ elsif shouldGrowDown
70
+ growDown(root, w, h)
71
+ elsif canGrowRight
72
+ growRight(root, w, h)
73
+ elsif canGrowDown
74
+ growDown(root, w, h)
75
+ else
76
+ raise RuntimeError, "can't fit #{w}x#{h} block into root #{root[:w]}x#{root[:h]} - this should not happen if images are pre-sorted correctly"
77
+ end
78
+
79
+ end
80
+
81
+ def self.growRight(root, w, h)
82
+ return {
83
+ :used => true,
84
+ :x => 0,
85
+ :y => 0,
86
+ :w => root[:w] + w,
87
+ :h => root[:h],
88
+ :down => root,
89
+ :right => { :x => root[:w], :y => 0, :w => w, :h => root[:h] }
90
+ }
91
+ end
92
+
93
+ def self.growDown(root, w, h)
94
+ return {
95
+ :used => true,
96
+ :x => 0,
97
+ :y => 0,
98
+ :w => root[:w],
99
+ :h => root[:h] + h,
100
+ :down => { :x => 0, :y => root[:h], :w => root[:w], :h => h },
101
+ :right => root
102
+ }
103
+ end
104
+
105
+ end # module Packed
106
+ end # module Layout
107
+ end # module SpriteFactory
@@ -0,0 +1,42 @@
1
+ module SpriteFactory
2
+ module Layout
3
+ module Vertical
4
+
5
+ def self.layout(images, options = {})
6
+ width = options[:width]
7
+ height = options[:height]
8
+ hpadding = options[:hpadding] || 0
9
+ vpadding = options[:vpadding] || 0
10
+ max_width = width || ((2 * hpadding) + images.map{|i| i[:width]}.max)
11
+ y = 0
12
+ images.each do |i|
13
+
14
+ if width
15
+ i[:cssw] = width
16
+ i[:cssx] = 0
17
+ i[:x] = 0 + (width - i[:width]) / 2
18
+ else
19
+ i[:cssw] = i[:width] + (2 * hpadding) # image width plus padding
20
+ i[:cssx] = (max_width - i[:cssw]) / 2 # centered horizontally
21
+ i[:x] = i[:cssx] + hpadding # image drawn offset to account for padding
22
+ end
23
+
24
+ if height
25
+ i[:cssh] = height
26
+ i[:cssy] = y
27
+ i[:y] = y + (height - i[:height]) / 2
28
+ else
29
+ i[:cssh] = i[:height] + (2 * vpadding) # image height plus padding
30
+ i[:cssy] = y # anchored at y
31
+ i[:y] = i[:cssy] + vpadding # image drawn offset to account for padding
32
+ end
33
+
34
+ y = y + i[:cssh]
35
+
36
+ end
37
+ { :width => max_width, :height => y }
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -1,4 +1,5 @@
1
1
  require 'pathname'
2
+ require 'fileutils'
2
3
 
3
4
  module SpriteFactory
4
5
  class Runner
@@ -17,6 +18,7 @@ module SpriteFactory
17
18
  @config[:selector] ||= SpriteFactory.selector || 'img.'
18
19
  @config[:csspath] ||= SpriteFactory.csspath
19
20
  @config[:report] ||= SpriteFactory.report
21
+ @config[:pngcrush] ||= SpriteFactory.pngcrush
20
22
  end
21
23
 
22
24
  #----------------------------------------------------------------------------
@@ -146,12 +148,17 @@ module SpriteFactory
146
148
 
147
149
  def create_sprite(images, width, height)
148
150
  library.create(output_image_file, images, width, height)
151
+ pngcrush(output_image_file)
149
152
  end
150
153
 
151
154
  #----------------------------------------------------------------------------
152
155
 
156
+ def layout_strategy
157
+ @layout_strategy ||= Layout.send(layout_name)
158
+ end
159
+
153
160
  def layout_images(images)
154
- Layout.send(layout_name, images, :width => width, :height => height, :hpadding => hpadding, :vpadding => vpadding)
161
+ layout_strategy.layout(images, :width => width, :height => height, :hpadding => hpadding, :vpadding => vpadding)
155
162
  end
156
163
 
157
164
  #----------------------------------------------------------------------------
@@ -171,6 +178,18 @@ module SpriteFactory
171
178
 
172
179
  #----------------------------------------------------------------------------
173
180
 
181
+ SUPPORTS_PNGCRUSH = !`which pngcrush`.empty?
182
+
183
+ def pngcrush(image)
184
+ if SUPPORTS_PNGCRUSH && config[:pngcrush]
185
+ crushed = "#{image}.crushed"
186
+ `pngcrush -rem alla -reduce -brute #{image} #{crushed}`
187
+ FileUtils.mv(crushed, image)
188
+ end
189
+ end
190
+
191
+ #----------------------------------------------------------------------------
192
+
174
193
  def summary(images, max)
175
194
  return <<-EOF
176
195