sprite-factory-custom 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/Gemfile +12 -0
- data/LICENSE +20 -0
- data/README.md +351 -0
- data/RELEASE_NOTES.md +60 -0
- data/Rakefile +73 -0
- data/bin/sf +57 -0
- data/lib/sprite_factory/layout/horizontal.rb +44 -0
- data/lib/sprite_factory/layout/packed.rb +118 -0
- data/lib/sprite_factory/layout/vertical.rb +44 -0
- data/lib/sprite_factory/library/chunky_png.rb +31 -0
- data/lib/sprite_factory/library/image_magick.rb +74 -0
- data/lib/sprite_factory/library/rmagick.rb +32 -0
- data/lib/sprite_factory/runner.rb +287 -0
- data/lib/sprite_factory/style.rb +74 -0
- data/lib/sprite_factory.rb +85 -0
- data/sprite_factory.gemspec +27 -0
- data/test/images/custom/custom.css +4 -0
- data/test/images/custom/running.png +0 -0
- data/test/images/custom/stopped.png +0 -0
- data/test/images/empty/readme.txt +1 -0
- data/test/images/formats/alice.gif +0 -0
- data/test/images/formats/codeincomplete.ico +0 -0
- data/test/images/formats/github.ico +0 -0
- data/test/images/formats/monkey.gif +0 -0
- data/test/images/formats/spies.jpg +0 -0
- data/test/images/formats/stackoverflow.ico +0 -0
- data/test/images/formats/thief.png +0 -0
- data/test/images/hover/div.bar__img.icon--active.png +0 -0
- data/test/images/hover/div.bar__img.icon--focus.png +0 -0
- data/test/images/hover/div.bar__img.icon--hover.png +0 -0
- data/test/images/hover/div.bar__img.icon--link.png +0 -0
- data/test/images/hover/div.bar__img.icon--visited.png +0 -0
- data/test/images/hover/div.bar__img.icon.png +0 -0
- data/test/images/hover/div.foo__img.icon--active.png +0 -0
- data/test/images/hover/div.foo__img.icon--focus.png +0 -0
- data/test/images/hover/div.foo__img.icon--hover.png +0 -0
- data/test/images/hover/div.foo__img.icon--link.png +0 -0
- data/test/images/hover/div.foo__img.icon--visited.png +0 -0
- data/test/images/hover/div.foo__img.icon.png +0 -0
- data/test/images/irregular/irregular1.png +0 -0
- data/test/images/irregular/irregular2.png +0 -0
- data/test/images/irregular/irregular3.png +0 -0
- data/test/images/irregular/irregular4.png +0 -0
- data/test/images/irregular/irregular5.png +0 -0
- data/test/images/irregular/readme.txt +2 -0
- data/test/images/reference/custom.css +22 -0
- data/test/images/reference/custom.png +0 -0
- data/test/images/reference/formats.css +28 -0
- data/test/images/reference/formats.png +0 -0
- data/test/images/reference/hover.css +38 -0
- data/test/images/reference/hover.png +0 -0
- data/test/images/reference/index.html +182 -0
- data/test/images/reference/irregular.css +24 -0
- data/test/images/reference/irregular.fixed.css +24 -0
- data/test/images/reference/irregular.fixed.png +0 -0
- data/test/images/reference/irregular.horizontal.css +24 -0
- data/test/images/reference/irregular.horizontal.png +0 -0
- data/test/images/reference/irregular.margin.css +24 -0
- data/test/images/reference/irregular.margin.png +0 -0
- data/test/images/reference/irregular.packed.css +24 -0
- data/test/images/reference/irregular.packed.png +0 -0
- data/test/images/reference/irregular.padded.css +24 -0
- data/test/images/reference/irregular.padded.png +0 -0
- data/test/images/reference/irregular.png +0 -0
- data/test/images/reference/irregular.sassy.css +38 -0
- data/test/images/reference/irregular.sassy.png +0 -0
- data/test/images/reference/irregular.sassy.sass +40 -0
- data/test/images/reference/irregular.vertical.css +24 -0
- data/test/images/reference/irregular.vertical.png +0 -0
- data/test/images/reference/regular.css +24 -0
- data/test/images/reference/regular.custom.css +24 -0
- data/test/images/reference/regular.custom.png +0 -0
- data/test/images/reference/regular.fixed.css +24 -0
- data/test/images/reference/regular.fixed.png +0 -0
- data/test/images/reference/regular.horizontal.css +24 -0
- data/test/images/reference/regular.horizontal.png +0 -0
- data/test/images/reference/regular.margin.css +24 -0
- data/test/images/reference/regular.margin.png +0 -0
- data/test/images/reference/regular.nocomments.css +5 -0
- data/test/images/reference/regular.nocomments.png +0 -0
- data/test/images/reference/regular.packed.css +24 -0
- data/test/images/reference/regular.packed.png +0 -0
- data/test/images/reference/regular.padded.css +24 -0
- data/test/images/reference/regular.padded.png +0 -0
- data/test/images/reference/regular.png +0 -0
- data/test/images/reference/regular.sassy.css +38 -0
- data/test/images/reference/regular.sassy.png +0 -0
- data/test/images/reference/regular.sassy.sass +40 -0
- data/test/images/reference/regular.vertical.css +24 -0
- data/test/images/reference/regular.vertical.png +0 -0
- data/test/images/reference/s.gif +0 -0
- data/test/images/reference/subfolders.css +24 -0
- data/test/images/reference/subfolders.png +0 -0
- data/test/images/regular/regular1.PNG +0 -0
- data/test/images/regular/regular2.PNG +0 -0
- data/test/images/regular/regular3.PNG +0 -0
- data/test/images/regular/regular4.PNG +0 -0
- data/test/images/regular/regular5.PNG +0 -0
- data/test/images/subfolders/england/amy.png +0 -0
- data/test/images/subfolders/england/bob.png +0 -0
- data/test/images/subfolders/france/bob.png +0 -0
- data/test/images/subfolders/usa/amy.png +0 -0
- data/test/images/subfolders/usa/bob.png +0 -0
- data/test/integration_test.rb +167 -0
- data/test/layout/horizontal_test.rb +156 -0
- data/test/layout/packed_test.rb +283 -0
- data/test/layout/test_case.rb +56 -0
- data/test/layout/vertical_test.rb +156 -0
- data/test/library_test.rb +58 -0
- data/test/runner_test.rb +229 -0
- data/test/style_test.rb +72 -0
- data/test/test_case.rb +138 -0
- 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
|