sprite-factory-custom 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/Gemfile +12 -0
  4. data/LICENSE +20 -0
  5. data/README.md +351 -0
  6. data/RELEASE_NOTES.md +60 -0
  7. data/Rakefile +73 -0
  8. data/bin/sf +57 -0
  9. data/lib/sprite_factory/layout/horizontal.rb +44 -0
  10. data/lib/sprite_factory/layout/packed.rb +118 -0
  11. data/lib/sprite_factory/layout/vertical.rb +44 -0
  12. data/lib/sprite_factory/library/chunky_png.rb +31 -0
  13. data/lib/sprite_factory/library/image_magick.rb +74 -0
  14. data/lib/sprite_factory/library/rmagick.rb +32 -0
  15. data/lib/sprite_factory/runner.rb +287 -0
  16. data/lib/sprite_factory/style.rb +74 -0
  17. data/lib/sprite_factory.rb +85 -0
  18. data/sprite_factory.gemspec +27 -0
  19. data/test/images/custom/custom.css +4 -0
  20. data/test/images/custom/running.png +0 -0
  21. data/test/images/custom/stopped.png +0 -0
  22. data/test/images/empty/readme.txt +1 -0
  23. data/test/images/formats/alice.gif +0 -0
  24. data/test/images/formats/codeincomplete.ico +0 -0
  25. data/test/images/formats/github.ico +0 -0
  26. data/test/images/formats/monkey.gif +0 -0
  27. data/test/images/formats/spies.jpg +0 -0
  28. data/test/images/formats/stackoverflow.ico +0 -0
  29. data/test/images/formats/thief.png +0 -0
  30. data/test/images/hover/div.bar__img.icon--active.png +0 -0
  31. data/test/images/hover/div.bar__img.icon--focus.png +0 -0
  32. data/test/images/hover/div.bar__img.icon--hover.png +0 -0
  33. data/test/images/hover/div.bar__img.icon--link.png +0 -0
  34. data/test/images/hover/div.bar__img.icon--visited.png +0 -0
  35. data/test/images/hover/div.bar__img.icon.png +0 -0
  36. data/test/images/hover/div.foo__img.icon--active.png +0 -0
  37. data/test/images/hover/div.foo__img.icon--focus.png +0 -0
  38. data/test/images/hover/div.foo__img.icon--hover.png +0 -0
  39. data/test/images/hover/div.foo__img.icon--link.png +0 -0
  40. data/test/images/hover/div.foo__img.icon--visited.png +0 -0
  41. data/test/images/hover/div.foo__img.icon.png +0 -0
  42. data/test/images/irregular/irregular1.png +0 -0
  43. data/test/images/irregular/irregular2.png +0 -0
  44. data/test/images/irregular/irregular3.png +0 -0
  45. data/test/images/irregular/irregular4.png +0 -0
  46. data/test/images/irregular/irregular5.png +0 -0
  47. data/test/images/irregular/readme.txt +2 -0
  48. data/test/images/reference/custom.css +22 -0
  49. data/test/images/reference/custom.png +0 -0
  50. data/test/images/reference/formats.css +28 -0
  51. data/test/images/reference/formats.png +0 -0
  52. data/test/images/reference/hover.css +38 -0
  53. data/test/images/reference/hover.png +0 -0
  54. data/test/images/reference/index.html +182 -0
  55. data/test/images/reference/irregular.css +24 -0
  56. data/test/images/reference/irregular.fixed.css +24 -0
  57. data/test/images/reference/irregular.fixed.png +0 -0
  58. data/test/images/reference/irregular.horizontal.css +24 -0
  59. data/test/images/reference/irregular.horizontal.png +0 -0
  60. data/test/images/reference/irregular.margin.css +24 -0
  61. data/test/images/reference/irregular.margin.png +0 -0
  62. data/test/images/reference/irregular.packed.css +24 -0
  63. data/test/images/reference/irregular.packed.png +0 -0
  64. data/test/images/reference/irregular.padded.css +24 -0
  65. data/test/images/reference/irregular.padded.png +0 -0
  66. data/test/images/reference/irregular.png +0 -0
  67. data/test/images/reference/irregular.sassy.css +38 -0
  68. data/test/images/reference/irregular.sassy.png +0 -0
  69. data/test/images/reference/irregular.sassy.sass +40 -0
  70. data/test/images/reference/irregular.vertical.css +24 -0
  71. data/test/images/reference/irregular.vertical.png +0 -0
  72. data/test/images/reference/regular.css +24 -0
  73. data/test/images/reference/regular.custom.css +24 -0
  74. data/test/images/reference/regular.custom.png +0 -0
  75. data/test/images/reference/regular.fixed.css +24 -0
  76. data/test/images/reference/regular.fixed.png +0 -0
  77. data/test/images/reference/regular.horizontal.css +24 -0
  78. data/test/images/reference/regular.horizontal.png +0 -0
  79. data/test/images/reference/regular.margin.css +24 -0
  80. data/test/images/reference/regular.margin.png +0 -0
  81. data/test/images/reference/regular.nocomments.css +5 -0
  82. data/test/images/reference/regular.nocomments.png +0 -0
  83. data/test/images/reference/regular.packed.css +24 -0
  84. data/test/images/reference/regular.packed.png +0 -0
  85. data/test/images/reference/regular.padded.css +24 -0
  86. data/test/images/reference/regular.padded.png +0 -0
  87. data/test/images/reference/regular.png +0 -0
  88. data/test/images/reference/regular.sassy.css +38 -0
  89. data/test/images/reference/regular.sassy.png +0 -0
  90. data/test/images/reference/regular.sassy.sass +40 -0
  91. data/test/images/reference/regular.vertical.css +24 -0
  92. data/test/images/reference/regular.vertical.png +0 -0
  93. data/test/images/reference/s.gif +0 -0
  94. data/test/images/reference/subfolders.css +24 -0
  95. data/test/images/reference/subfolders.png +0 -0
  96. data/test/images/regular/regular1.PNG +0 -0
  97. data/test/images/regular/regular2.PNG +0 -0
  98. data/test/images/regular/regular3.PNG +0 -0
  99. data/test/images/regular/regular4.PNG +0 -0
  100. data/test/images/regular/regular5.PNG +0 -0
  101. data/test/images/subfolders/england/amy.png +0 -0
  102. data/test/images/subfolders/england/bob.png +0 -0
  103. data/test/images/subfolders/france/bob.png +0 -0
  104. data/test/images/subfolders/usa/amy.png +0 -0
  105. data/test/images/subfolders/usa/bob.png +0 -0
  106. data/test/integration_test.rb +167 -0
  107. data/test/layout/horizontal_test.rb +156 -0
  108. data/test/layout/packed_test.rb +283 -0
  109. data/test/layout/test_case.rb +56 -0
  110. data/test/layout/vertical_test.rb +156 -0
  111. data/test/library_test.rb +58 -0
  112. data/test/runner_test.rb +229 -0
  113. data/test/style_test.rb +72 -0
  114. data/test/test_case.rb +138 -0
  115. metadata +286 -0
@@ -0,0 +1,118 @@
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 fixed :width/:height option" if options[:width] || options[:height]
10
+
11
+ return { :width => 0, :height => 0 } if images.empty?
12
+
13
+ hpadding = options[:hpadding] || 0
14
+ vpadding = options[:vpadding] || 0
15
+ hmargin = options[:hmargin] || 0
16
+ vmargin = options[:vmargin] || 0
17
+
18
+ images.each do |i|
19
+ i[:w] = i[:width] + (2*hpadding) + (2*hmargin)
20
+ i[:h] = i[:height] + (2*vpadding) + (2*vmargin)
21
+ end
22
+
23
+ images.sort! do |a,b|
24
+ diff = [b[:w], b[:h]].max <=> [a[:w], a[:h]].max
25
+ diff = [b[:w], b[:h]].min <=> [a[:w], a[:h]].min if diff.zero?
26
+ diff = b[:h] <=> a[:h] if diff.zero?
27
+ diff = b[:w] <=> a[:w] if diff.zero?
28
+ diff
29
+ end
30
+
31
+ root = { :x => 0, :y => 0, :w => images[0][:w], :h => images[0][:h] }
32
+
33
+ images.each do |i|
34
+ if (node = findNode(root, i[:w], i[:h]))
35
+ placeImage(i, node, hpadding, vpadding, hmargin, vmargin)
36
+ splitNode(node, i[:w], i[:h])
37
+ else
38
+ root = grow(root, i[:w], i[:h])
39
+ redo
40
+ end
41
+ end
42
+
43
+ { :width => root[:w], :height => root[:h] }
44
+
45
+ end
46
+
47
+ def self.placeImage(image, node, hpadding, vpadding, hmargin, vmargin)
48
+ image[:cssx] = node[:x] + hmargin
49
+ image[:cssy] = node[:y] + vmargin
50
+ image[:cssw] = image[:width] + (2*hpadding)
51
+ image[:cssh] = image[:height] + (2*vpadding)
52
+ image[:x] = image[:cssx] + hpadding
53
+ image[:y] = image[:cssy] + vpadding
54
+ end
55
+
56
+ def self.findNode(root, w, h)
57
+ if root[:used]
58
+ findNode(root[:right], w, h) || findNode(root[:down], w, h)
59
+ elsif (w <= root[:w]) && (h <= root[:h])
60
+ root
61
+ end
62
+ end
63
+
64
+ def self.splitNode(node, w, h)
65
+ node[:used] = true
66
+ node[:down] = { :x => node[:x], :y => node[:y] + h, :w => node[:w], :h => node[:h] - h }
67
+ node[:right] = { :x => node[:x] + w, :y => node[:y], :w => node[:w] - w, :h => h }
68
+ end
69
+
70
+ def self.grow(root, w, h)
71
+
72
+ canGrowDown = (w <= root[:w])
73
+ canGrowRight = (h <= root[:h])
74
+
75
+ shouldGrowRight = canGrowRight && (root[:h] >= (root[:w] + w))
76
+ shouldGrowDown = canGrowDown && (root[:w] >= (root[:h] + h))
77
+
78
+ if shouldGrowRight
79
+ growRight(root, w, h)
80
+ elsif shouldGrowDown
81
+ growDown(root, w, h)
82
+ elsif canGrowRight
83
+ growRight(root, w, h)
84
+ elsif canGrowDown
85
+ growDown(root, w, h)
86
+ else
87
+ 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"
88
+ end
89
+
90
+ end
91
+
92
+ def self.growRight(root, w, h)
93
+ return {
94
+ :used => true,
95
+ :x => 0,
96
+ :y => 0,
97
+ :w => root[:w] + w,
98
+ :h => root[:h],
99
+ :down => root,
100
+ :right => { :x => root[:w], :y => 0, :w => w, :h => root[:h] }
101
+ }
102
+ end
103
+
104
+ def self.growDown(root, w, h)
105
+ return {
106
+ :used => true,
107
+ :x => 0,
108
+ :y => 0,
109
+ :w => root[:w],
110
+ :h => root[:h] + h,
111
+ :down => { :x => 0, :y => root[:h], :w => root[:w], :h => h },
112
+ :right => root
113
+ }
114
+ end
115
+
116
+ end # module Packed
117
+ end # module Layout
118
+ end # module SpriteFactory
@@ -0,0 +1,44 @@
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
+ hmargin = options[:hmargin] || 0
11
+ vmargin = options[:vmargin] || 0
12
+ max_width = width || (2 * (hpadding + hmargin) + images.map{|i| i[:width]}.max)
13
+ y = 0
14
+ images.each do |i|
15
+
16
+ if width
17
+ i[:cssw] = width
18
+ i[:cssx] = 0
19
+ i[:x] = 0 + (width - i[:width]) / 2
20
+ else
21
+ i[:cssw] = i[:width] + (2 * hpadding) # image width plus padding
22
+ i[:cssx] = (max_width - i[:cssw]) / 2 # centered horizontally
23
+ i[:x] = i[:cssx] + hpadding # image drawn offset to account for padding
24
+ end
25
+
26
+ if height
27
+ i[:cssh] = height
28
+ i[:cssy] = y
29
+ i[:y] = y + (height - i[:height]) / 2
30
+ else
31
+ i[:cssh] = i[:height] + (2 * vpadding) # image height plus padding
32
+ i[:cssy] = y + vmargin # anchored at y
33
+ i[:y] = i[:cssy] + vpadding # image drawn offset to account for padding
34
+ end
35
+
36
+ y += i[:cssh] + 2 * vmargin
37
+
38
+ end
39
+ { :width => max_width, :height => y }
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,31 @@
1
+ require 'chunky_png'
2
+
3
+ module SpriteFactory
4
+ module Library
5
+ module ChunkyPng
6
+
7
+ VALID_EXTENSIONS = :png
8
+
9
+ def self.load(files)
10
+ files.map do |filename|
11
+ image = ChunkyPNG::Image.from_file(filename)
12
+ {
13
+ :filename => filename,
14
+ :image => image,
15
+ :width => image.width,
16
+ :height => image.height
17
+ }
18
+ end
19
+ end
20
+
21
+ def self.create(filename, images, width, height)
22
+ target = ChunkyPNG::Image.new(width, height, ChunkyPNG::Color::TRANSPARENT)
23
+ images.each do |image|
24
+ target.compose!(image[:image], image[:x], image[:y])
25
+ end
26
+ target.save(filename)
27
+ end
28
+
29
+ end # module ChunkyPng
30
+ end # module Library
31
+ end # module SpriteFactory
@@ -0,0 +1,74 @@
1
+ module SpriteFactory
2
+ module Library
3
+ module ImageMagick
4
+
5
+ # Represents an error from an underlying ImageMagick call
6
+ class Error < RuntimeError
7
+ attr_reader :command
8
+ attr_reader :args
9
+ attr_reader :output
10
+
11
+ def initialize(msg, command, args, output)
12
+ super(msg)
13
+ @command = command
14
+ @args = args
15
+ @output = output
16
+ end
17
+ end
18
+
19
+ VALID_EXTENSIONS = [:png, :jpg, :jpeg, :gif, :ico]
20
+
21
+ def self.load(files)
22
+ files.map do |filename|
23
+ path = "#{filename}[0]" # layer 0
24
+ output = run("identify", ['-format', '%wx%h', path])
25
+
26
+ width, height = output.chomp.split(/x/).map(&:to_i)
27
+ {
28
+ :filename => filename,
29
+ :path => path,
30
+ :width => width,
31
+ :height => height
32
+ }
33
+ end
34
+ end
35
+
36
+ def self.create(filename, images, width, height)
37
+ # we want to invoke:
38
+ # convert -size #{width}x#{height} xc:none
39
+ # #{input} -geometry +#{x}+#{y} -composite
40
+ # #{output}
41
+
42
+ args = ["-size", "#{width}x#{height}", "xc:none"]
43
+ images.each do |image|
44
+ args += [image[:path], "-geometry", "+#{image[:x]}+#{image[:y]}", "-composite"]
45
+ end
46
+ args << filename
47
+
48
+ run("convert", args)
49
+ true
50
+ end
51
+
52
+ protected
53
+ def self.run(command, args)
54
+ full_command = [command] + args.map(&:to_s)
55
+
56
+ r, w = IO.pipe
57
+ pid = Process.spawn(*full_command, {[:out, :err] => w})
58
+
59
+ w.close
60
+ output = r.read
61
+
62
+ Process.waitpid(pid)
63
+ success = $?.exitstatus == 0 ? true : false
64
+
65
+ if !success
66
+ raise Error.new("error running `#{command}` (check $!.args/$!.output for more information)", command, args, output)
67
+ end
68
+
69
+ output
70
+ end
71
+
72
+ end # module ImageMagick
73
+ end # module Library
74
+ end # module SpriteFactory
@@ -0,0 +1,32 @@
1
+ require 'RMagick'
2
+
3
+ module SpriteFactory
4
+ module Library
5
+ module RMagick
6
+
7
+ VALID_EXTENSIONS = [:png, :jpg, :jpeg, :gif, :ico]
8
+
9
+ def self.load(files)
10
+ files.map do |filename|
11
+ image = Magick::Image.read(filename)[0]
12
+ {
13
+ :filename => filename,
14
+ :image => image,
15
+ :width => image.columns,
16
+ :height => image.rows
17
+ }
18
+ end
19
+ end
20
+
21
+ def self.create(filename, images, width, height)
22
+ target = Magick::Image.new(width,height)
23
+ target.opacity = Magick::MaxRGB
24
+ images.each do |image|
25
+ target.composite!(image[:image], image[:x], image[:y], Magick::SrcOverCompositeOp)
26
+ end
27
+ target.write(filename)
28
+ end
29
+
30
+ end # module RMagick
31
+ end # module Library
32
+ end # module SpriteFactory
@@ -0,0 +1,287 @@
1
+ require 'pathname'
2
+ require 'fileutils'
3
+
4
+ module SpriteFactory
5
+ class Runner
6
+
7
+ #----------------------------------------------------------------------------
8
+
9
+ PSEUDO_CLASS_ORDER = [nil, ':link', ':visited', ':focus', ':hover', ':active'] # TODO: allow caller to specify this order ?
10
+
11
+ #----------------------------------------------------------------------------
12
+
13
+ attr :input
14
+ attr :config
15
+
16
+ def initialize(input, config = {})
17
+ @input = input.to_s[-1] == "/" ? input[0...-1] : input # gracefully ignore trailing slash on input directory name
18
+ @config = config
19
+ @config[:style] ||= SpriteFactory.style || :css
20
+ @config[:layout] ||= SpriteFactory.layout || :horizontal
21
+ @config[:library] ||= SpriteFactory.library || :rmagick
22
+ @config[:selector] ||= SpriteFactory.selector || 'img.'
23
+ @config[:cssurl] ||= SpriteFactory.cssurl
24
+ @config[:report] ||= SpriteFactory.report
25
+ @config[:pngcrush] ||= SpriteFactory.pngcrush
26
+ @config[:nocomments] ||= SpriteFactory.nocomments
27
+ end
28
+
29
+ #----------------------------------------------------------------------------
30
+
31
+ def run!(&block)
32
+
33
+ raise RuntimeError, "unknown layout #{layout_name}" if !Layout.respond_to?(layout_name)
34
+ raise RuntimeError, "unknown style #{style_name}" if !Style.respond_to?(style_name)
35
+ raise RuntimeError, "unknown library #{library_name}" if !Library.respond_to?(library_name)
36
+
37
+ raise RuntimeError, "input must be a single directory" if input.nil? || input.to_s.empty? || !File.directory?(input)
38
+ raise RuntimeError, "no image files found" if image_files.empty?
39
+ raise RuntimeError, "no output file specified" if output.to_s.empty?
40
+ raise RuntimeError, "no output image file specified" if output_image_file.to_s.empty?
41
+ raise RuntimeError, "no output style file specified" if output_style_file.to_s.empty?
42
+
43
+ raise RuntimeError, "set :width for fixed width, or :hpadding for horizontal padding, but not both." if width && !hpadding.zero?
44
+ raise RuntimeError, "set :height for fixed height, or :vpadding for vertical padding, but not both." if height && !vpadding.zero?
45
+ raise RuntimeError, "set :width for fixed width, or :hmargin for horizontal margin, but not both." if width && !hmargin.zero?
46
+ raise RuntimeError, "set :height for fixed height, or :vmargin for vertical margin, but not both." if height && !hmargin.zero?
47
+
48
+ raise RuntimeError, "The legacy :csspath attribute is no longer supported, please use :cssurl instead (see README)" unless @config[:csspath].nil?
49
+
50
+ images = load_images
51
+ max = layout_images(images)
52
+ header = summary(images, max)
53
+
54
+ report(header)
55
+
56
+ css = []
57
+ css << style_comment(header) unless nocomments? # header comment
58
+ css << style(selector, css_url, images, &block) # generated styles
59
+ css << IO.read(custom_style_file) if File.exists?(custom_style_file) # custom styles
60
+ css = css.join("\n")
61
+
62
+ create_sprite(images, max[:width], max[:height])
63
+
64
+ unless nocss?
65
+ css_file = File.open(output_style_file, "w+")
66
+ css_file.puts css
67
+ css_file.close
68
+ end
69
+
70
+ if config[:return] == :images
71
+ images # if caller explicitly asked for detailed images hash instead of generated CSS
72
+ else
73
+ css # otherwise, default is to return the generated CSS to caller in string form
74
+ end
75
+
76
+ end
77
+
78
+ #----------------------------------------------------------------------------
79
+
80
+ private
81
+
82
+ def selector
83
+ config[:selector]
84
+ end
85
+
86
+ def style_name
87
+ config[:style]
88
+ end
89
+
90
+ def custom_styles
91
+ config[:custom_styles]
92
+ end
93
+
94
+ def layout_name
95
+ config[:layout]
96
+ end
97
+
98
+ def library_name
99
+ config[:library]
100
+ end
101
+
102
+ def hpadding
103
+ config[:hpadding] || config[:padding] || 0
104
+ end
105
+
106
+ def vpadding
107
+ config[:vpadding] || config[:padding] || 0
108
+ end
109
+
110
+ def hmargin
111
+ config[:hmargin] || config[:margin] || 0
112
+ end
113
+
114
+ def vmargin
115
+ config[:vmargin] || config[:margin] || 0
116
+ end
117
+
118
+ def width
119
+ config[:width]
120
+ end
121
+
122
+ def height
123
+ config[:height]
124
+ end
125
+
126
+ def output
127
+ config[:output] || input
128
+ end
129
+
130
+ def output_image_file
131
+ config[:output_image] || "#{output}.png"
132
+ end
133
+
134
+ def output_style_file
135
+ config[:output_style] || "#{output}.#{style_name}"
136
+ end
137
+
138
+ def nocss?
139
+ config[:nocss] # set true if you dont want an output style file generated (e.g. you will take the #run! output and store it yourself)
140
+ end
141
+
142
+ def nocomments?
143
+ config[:nocomments] # set true if you dont want any comments in the output style file
144
+ end
145
+
146
+ def custom_style_file
147
+ File.join(input, File.basename(input) + ".#{style_name}")
148
+ end
149
+
150
+ def css_url
151
+ base = File.basename(output_image_file)
152
+ custom = config[:cssurl]
153
+ if custom
154
+ if custom.is_a?(Proc)
155
+ custom.call(base) # allow custom url using a lambda
156
+ elsif custom.include?('$IMAGE')
157
+ custom.sub('$IMAGE', base) # allow custom url with simple token replacement
158
+ else
159
+ "url(#{File.join(custom, base)})" # allow custom url with simple prepend
160
+ end
161
+ else
162
+ "url(#{base})" # otherwise, just default to basename of the output image file
163
+ end
164
+ end
165
+
166
+ def image_files
167
+ return [] if input.nil?
168
+ valid_extensions = library::VALID_EXTENSIONS
169
+ expansions = Array(valid_extensions).map{|ext| File.join(input, "**", "*.#{ext}")}
170
+ SpriteFactory.find_files(*expansions)
171
+ end
172
+
173
+ #----------------------------------------------------------------------------
174
+
175
+ def library
176
+ @library ||= Library.send(library_name)
177
+ end
178
+
179
+ def load_images
180
+ input_path = Pathname.new(input)
181
+ images = library.load(image_files)
182
+ images.each do |i|
183
+ i[:name], i[:ext] = map_image_filename(i[:filename], input_path)
184
+ raise RuntimeError, "image #{i[:name]} does not fit within a fixed width of #{width}" if width && (width < i[:width])
185
+ raise RuntimeError, "image #{i[:name]} does not fit within a fixed height of #{height}" if height && (height < i[:height])
186
+ end
187
+ images.sort_by {|i| [image_name_without_pseudo_class(i), image_pseudo_class_priority(i)] }
188
+ end
189
+
190
+ def map_image_filename(filename, input_path)
191
+ name = Pathname.new(filename).relative_path_from(input_path).to_s.gsub(File::SEPARATOR, "_")
192
+ name = name.gsub('--', ':')
193
+ name = name.gsub('__', ' ')
194
+ ext = File.extname(name)
195
+ name = name[0...-ext.length] unless ext.empty?
196
+ [name, ext]
197
+ end
198
+
199
+ def image_name_without_pseudo_class(image)
200
+ image[:name].split(':').first
201
+ end
202
+
203
+ def image_pseudo_class(image)
204
+ image[:name].slice(/:.*?\Z/)
205
+ end
206
+
207
+ def image_pseudo_class_priority(image)
208
+ PSEUDO_CLASS_ORDER.index(image_pseudo_class(image))
209
+ end
210
+
211
+ #----------------------------------------------------------------------------
212
+
213
+ def create_sprite(images, width, height)
214
+ library.create(output_image_file, images, width, height)
215
+ pngcrush(output_image_file)
216
+ end
217
+
218
+ #----------------------------------------------------------------------------
219
+
220
+ def layout_strategy
221
+ @layout_strategy ||= Layout.send(layout_name)
222
+ end
223
+
224
+ def layout_images(images)
225
+ layout_strategy.layout(images, :width => width, :height => height, :hpadding => hpadding, :vpadding => vpadding, :hmargin => hmargin, :vmargin => vmargin)
226
+ end
227
+
228
+ #----------------------------------------------------------------------------
229
+
230
+ def style(selector, url, images, &block)
231
+ defaults = Style.generate(style_name, selector, url, images, custom_styles) # must call, even if custom block is given, because it stashes generated css style into image[:style] attributes
232
+ if block_given?
233
+ yield images.inject({}) {|h,i| h[i[:name].to_sym] = i; h} # provide custom rule builder a hash by image name
234
+ else
235
+ defaults
236
+ end
237
+ end
238
+
239
+ def style_comment(comment)
240
+ Style.comment(style_name, comment)
241
+ end
242
+
243
+ #----------------------------------------------------------------------------
244
+
245
+ SUPPORTS_PNGCRUSH = !`which pngcrush`.empty? rescue false # rescue on environments without `which` (windows)
246
+
247
+ def pngcrush(image)
248
+ if SUPPORTS_PNGCRUSH && config[:pngcrush]
249
+ crushed = "#{image}.crushed"
250
+ `pngcrush -rem alla -reduce -brute #{image} #{crushed}`
251
+ FileUtils.mv(crushed, image)
252
+ end
253
+ end
254
+
255
+ #----------------------------------------------------------------------------
256
+
257
+ def summary(images, max)
258
+ return <<-EOF
259
+
260
+ Creating a sprite from following images:
261
+ \n#{images.map{|i| " #{report_path(i[:filename])} (#{i[:width]}x#{i[:height]})" }.join("\n")}
262
+
263
+ Output files:
264
+ #{report_path(output_image_file)}
265
+ #{report_path(output_style_file)}
266
+
267
+ Output size:
268
+ #{max[:width]}x#{max[:height]}
269
+
270
+ EOF
271
+ end
272
+
273
+ def report(msg)
274
+ puts msg if config[:report]
275
+ end
276
+
277
+ def report_path(path) # always report paths relative to . to avoid machine specific information in report (to avoid DIFF issues in tests and version control)
278
+ @cwd ||= Pathname.new(File.expand_path('.'))
279
+ path = Pathname.new(path)
280
+ path = path.relative_path_from(@cwd) if path.absolute?
281
+ path.to_s
282
+ end
283
+
284
+ #----------------------------------------------------------------------------
285
+
286
+ end # class Runner
287
+ end # module SpriteFactory
@@ -0,0 +1,74 @@
1
+ module SpriteFactory
2
+ module Style
3
+
4
+ #----------------------------------------------------------------------------
5
+
6
+ def self.css(selector, name, attributes)
7
+ "#{selector}#{name} { #{css_style(attributes)}; }"
8
+ end
9
+
10
+ def self.css_style(attributes)
11
+ attributes.join("; ")
12
+ end
13
+
14
+ def self.css_comment(comment)
15
+ return "/*\n#{comment}\n*/"
16
+ end
17
+
18
+ #----------------------------------------------------------------------------
19
+
20
+ def self.scss(selector, name, attributes)
21
+ css(selector, name, attributes) # scss is a superset of css, but we dont actually need any of the extra bits, so just defer to the css generator instead
22
+ end
23
+
24
+ def self.scss_style(attributes)
25
+ css_style(attributes)
26
+ end
27
+
28
+ def self.scss_comment(comment)
29
+ css_comment(comment)
30
+ end
31
+
32
+ #----------------------------------------------------------------------------
33
+
34
+ def self.sass(selector, name, attributes)
35
+ "#{selector}#{name}\n" + sass_style(attributes)
36
+ end
37
+
38
+ def self.sass_style(attributes)
39
+ attributes.map{|a| " #{a}"}.join("\n") + "\n"
40
+ end
41
+
42
+ def self.sass_comment(comment)
43
+ return "/* #{comment.rstrip} */" # SASS has peculiar indenting requirements in order to recognise closing block comment
44
+ end
45
+
46
+ #----------------------------------------------------------------------------
47
+
48
+ def self.generate(style_name, selector, url, images, custom_styles="")
49
+ styles = []
50
+ images.each do |image|
51
+ attr = [
52
+ "width: #{image[:cssw]}px",
53
+ "height: #{image[:cssh]}px",
54
+ "background: #{url} #{-image[:cssx]}px #{-image[:cssy]}px no-repeat",
55
+ custom_styles
56
+ ]
57
+ image[:selector] = selector # make selector available for (optional) custom rule generators
58
+ image[:style] = send("#{style_name}_style", attr) # make pure style available for (optional) custom rule generators (see usage of yield inside Runner#style)
59
+ styles << send(style_name, selector, image[:name], attr)
60
+ end
61
+ styles << ""
62
+ styles.join("\n")
63
+ end
64
+
65
+ #----------------------------------------------------------------------------
66
+
67
+ def self.comment(style_name, comment)
68
+ send("#{style_name}_comment", comment)
69
+ end
70
+
71
+ #----------------------------------------------------------------------------
72
+
73
+ end
74
+ end