sprite-factory 1.0.0 → 1.2.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.
@@ -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