css-spriter 0.9.2
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.
- data/.gitignore +12 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +81 -0
- data/Rakefile +45 -0
- data/VERSION +1 -0
- data/bin/css-spriter +45 -0
- data/bin/png_info +61 -0
- data/examples/filter_util.rb +17 -0
- data/examples/sprites/.mtimes +12 -0
- data/examples/sprites/README +5 -0
- data/examples/sprites/fragment.css +0 -0
- data/examples/sprites/index.html +26 -0
- data/examples/sprites/many_sized_cats/.mtimes +3 -0
- data/examples/sprites/many_sized_cats/cat-on-keyboard.png +0 -0
- data/examples/sprites/many_sized_cats/darth_cat.png +0 -0
- data/examples/sprites/many_sized_cats/fragment.css +21 -0
- data/examples/sprites/many_sized_cats/music-keyboard-cat.png +0 -0
- data/examples/sprites/many_sized_cats/sprite.css +21 -0
- data/examples/sprites/many_sized_cats/sprite.png +0 -0
- data/examples/sprites/server.rb +10 -0
- data/examples/sprites/sprite.css +49 -0
- data/examples/sprites/words/.mtimes +4 -0
- data/examples/sprites/words/fragment.css +28 -0
- data/examples/sprites/words/latitude.png +0 -0
- data/examples/sprites/words/of.png +0 -0
- data/examples/sprites/words/set.png +0 -0
- data/examples/sprites/words/specified.png +0 -0
- data/examples/sprites/words/sprite.css +28 -0
- data/examples/sprites/words/sprite.png +0 -0
- data/init.rb +1 -0
- data/lib/css-spriter.rb +16 -0
- data/lib/css-spriter/directory_processor.rb +94 -0
- data/lib/css-spriter/image_data.rb +128 -0
- data/lib/css-spriter/mtime_tracker.rb +79 -0
- data/lib/css-spriter/png/chunk.rb +12 -0
- data/lib/css-spriter/png/file_header.rb +7 -0
- data/lib/css-spriter/png/filters.rb +80 -0
- data/lib/css-spriter/png/idat.rb +26 -0
- data/lib/css-spriter/png/iend.rb +9 -0
- data/lib/css-spriter/png/ihdr.rb +34 -0
- data/lib/css-spriter/png/image.rb +153 -0
- data/lib/css-spriter/png/parser.rb +54 -0
- data/lib/css-spriter/processor.rb +28 -0
- data/lib/css-spriter/sprite.rb +39 -0
- data/lib/css-spriter/stylesheet_builder.rb +22 -0
- data/spec/builders/image_builder.rb +23 -0
- data/spec/css_fragments/deep/style/fragment.css +1 -0
- data/spec/css_fragments/some/fragment.css +1 -0
- data/spec/expected_output/merge_right_test.png +0 -0
- data/spec/expected_output/write_test.png +0 -0
- data/spec/image_data_spec.rb +63 -0
- data/spec/images/lightening.png +0 -0
- data/spec/integration_spec.rb +151 -0
- data/spec/lib/file_header_spec.rb +10 -0
- data/spec/lib/idat_spec.rb +31 -0
- data/spec/lib/ihdr_spec.rb +43 -0
- data/spec/lib/image_spec.rb +42 -0
- data/spec/lib/parser_spec.rb +12 -0
- data/spec/lib/sprite_spec.rb +39 -0
- data/spec/mtime_tracking_spec.rb +69 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/sprite_dirs/words/latitude.png +0 -0
- data/spec/sprite_dirs/words/of.png +0 -0
- data/spec/sprite_dirs/words/set.png +0 -0
- data/spec/sprite_dirs/words/specified.png +0 -0
- data/spec/tmp/merge_right_test.png +0 -0
- data/spec/tmp/write_test.png +0 -0
- data/tasks/spriter_tasks.rake +25 -0
- metadata +148 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
.words_latitude {
|
2
|
+
background: transparent url(/words/sprite.png) 0px 0px no-repeat;
|
3
|
+
width:66;
|
4
|
+
height:21;
|
5
|
+
text-indent:-5000px;
|
6
|
+
}
|
7
|
+
|
8
|
+
.words_of {
|
9
|
+
background: transparent url(/words/sprite.png) -66px 0px no-repeat;
|
10
|
+
width:19;
|
11
|
+
height:21;
|
12
|
+
text-indent:-5000px;
|
13
|
+
}
|
14
|
+
|
15
|
+
.words_set {
|
16
|
+
background: transparent url(/words/sprite.png) -85px 0px no-repeat;
|
17
|
+
width:26;
|
18
|
+
height:21;
|
19
|
+
text-indent:-5000px;
|
20
|
+
}
|
21
|
+
|
22
|
+
.words_specified {
|
23
|
+
background: transparent url(/words/sprite.png) -111px 0px no-repeat;
|
24
|
+
width:70;
|
25
|
+
height:21;
|
26
|
+
text-indent:-5000px;
|
27
|
+
}
|
28
|
+
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,28 @@
|
|
1
|
+
.words_of {
|
2
|
+
background: transparent url(/examples/sprites/words/sprite.png) -66px 0px no-repeat;
|
3
|
+
width:19;
|
4
|
+
height:21;
|
5
|
+
text-indent:-5000px;
|
6
|
+
}
|
7
|
+
|
8
|
+
.words_set {
|
9
|
+
background: transparent url(/examples/sprites/words/sprite.png) -85px 0px no-repeat;
|
10
|
+
width:26;
|
11
|
+
height:21;
|
12
|
+
text-indent:-5000px;
|
13
|
+
}
|
14
|
+
|
15
|
+
.words_specified {
|
16
|
+
background: transparent url(/examples/sprites/words/sprite.png) -111px 0px no-repeat;
|
17
|
+
width:70;
|
18
|
+
height:21;
|
19
|
+
text-indent:-5000px;
|
20
|
+
}
|
21
|
+
|
22
|
+
.words_latitude {
|
23
|
+
background: transparent url(/examples/sprites/words/sprite.png) 0px 0px no-repeat;
|
24
|
+
width:66;
|
25
|
+
height:21;
|
26
|
+
text-indent:-5000px;
|
27
|
+
}
|
28
|
+
|
Binary file
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Include hook code here
|
data/lib/css-spriter.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
|
3
|
+
require 'css-spriter/png/file_header'
|
4
|
+
require 'css-spriter/png/parser'
|
5
|
+
require 'css-spriter/png/filters'
|
6
|
+
require 'css-spriter/png/chunk'
|
7
|
+
require 'css-spriter/png/ihdr'
|
8
|
+
require 'css-spriter/png/idat'
|
9
|
+
require 'css-spriter/png/iend'
|
10
|
+
require 'css-spriter/png/image'
|
11
|
+
require 'css-spriter/sprite'
|
12
|
+
require 'css-spriter/directory_processor'
|
13
|
+
require 'css-spriter/stylesheet_builder'
|
14
|
+
require 'css-spriter/processor'
|
15
|
+
require 'css-spriter/mtime_tracker'
|
16
|
+
require 'css-spriter/image_data'
|
@@ -0,0 +1,94 @@
|
|
1
|
+
class DirectoryProcessor
|
2
|
+
|
3
|
+
DEFAULT_TEMPLATE = <<-EOF
|
4
|
+
.<name>_<image_name> {
|
5
|
+
background: transparent url(<image_loc>) <offset>px 0px no-repeat;
|
6
|
+
width:<width>;
|
7
|
+
height:<height>;
|
8
|
+
text-indent:-5000px;
|
9
|
+
}
|
10
|
+
|
11
|
+
EOF
|
12
|
+
|
13
|
+
def initialize(dir, options = {})
|
14
|
+
@options = options
|
15
|
+
@dir = dir
|
16
|
+
@sprite = Sprite.new
|
17
|
+
@tracker = MtimeTracker.new(@dir,
|
18
|
+
:exclude => ['sprite.css', 'fragment.css', 'sprite.png'])
|
19
|
+
end
|
20
|
+
|
21
|
+
def images
|
22
|
+
Dir.glob(@dir + "/*.png").reject{|i| i.match /sprite\.png/}
|
23
|
+
end
|
24
|
+
|
25
|
+
def write
|
26
|
+
return unless @tracker.has_changes?
|
27
|
+
images.each {|f| @sprite.append(PNG::Image.image_data(f))}
|
28
|
+
@sprite.write(sprite_file)
|
29
|
+
File.open(css_file, 'w') do |f|
|
30
|
+
f.write(css)
|
31
|
+
end
|
32
|
+
@tracker.update
|
33
|
+
end
|
34
|
+
|
35
|
+
def cleanup
|
36
|
+
File.delete(sprite_file) rescue nil
|
37
|
+
File.delete(css_file) rescue nil
|
38
|
+
@tracker.cleanup
|
39
|
+
end
|
40
|
+
|
41
|
+
def sprite_file
|
42
|
+
@dir + "/sprite.png"
|
43
|
+
end
|
44
|
+
|
45
|
+
def css_file
|
46
|
+
@dir + "/fragment.css"
|
47
|
+
end
|
48
|
+
|
49
|
+
def dir_name
|
50
|
+
@dir.split('/').last
|
51
|
+
end
|
52
|
+
|
53
|
+
def image_loc
|
54
|
+
#TODO: Lame!
|
55
|
+
|
56
|
+
dir = truncate_abs_path
|
57
|
+
base = ("/" + dir + "/sprite.png").gsub(/^\/.\//, "/").gsub("//", "/")
|
58
|
+
source = @options[:source]
|
59
|
+
base = base.gsub(source, "") if source && source != "."
|
60
|
+
base = @options[:path_prefix] + base if @options[:path_prefix]
|
61
|
+
base
|
62
|
+
end
|
63
|
+
|
64
|
+
def truncate_abs_path
|
65
|
+
return @dir unless @options[:source]
|
66
|
+
path_elements = @options[:source].split('/')
|
67
|
+
path_elements.pop #we want to remove everything above the root
|
68
|
+
to_truncate = path_elements.join("/")
|
69
|
+
@dir.gsub(to_truncate, "")
|
70
|
+
end
|
71
|
+
|
72
|
+
def template_file
|
73
|
+
@dir + "/template.css"
|
74
|
+
end
|
75
|
+
|
76
|
+
def template
|
77
|
+
if File.exists?(template_file)
|
78
|
+
return File.read(template_file)
|
79
|
+
end
|
80
|
+
return DEFAULT_TEMPLATE
|
81
|
+
end
|
82
|
+
|
83
|
+
def css
|
84
|
+
@sprite.locations.inject("") do |out, image|
|
85
|
+
image_name, properties = image
|
86
|
+
out << template.gsub("<name>", dir_name).
|
87
|
+
gsub("<image_name>", image_name.to_s).
|
88
|
+
gsub("<width>", properties[:width].to_s).
|
89
|
+
gsub("<height>", properties[:height].to_s).
|
90
|
+
gsub("<offset>", properties[:x].to_s).
|
91
|
+
gsub("<image_loc>", image_loc)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'enumerator'
|
2
|
+
|
3
|
+
module CssSpriter
|
4
|
+
class ImageData
|
5
|
+
RGB_WIDTH = 3
|
6
|
+
RGBA_WIDTH = 4
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
@data = (options.delete :data) || []
|
10
|
+
@properties = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def name; @properties[:name] || "default"; end
|
14
|
+
def scanline_width; @properties[:scanline_width]; end
|
15
|
+
def width; scanline_width / pixel_width; end
|
16
|
+
def pixel_width; @properties[:pixel_width]; end
|
17
|
+
|
18
|
+
# need better checks, because currently compatible is
|
19
|
+
# similar color type, or depth.. maybe it doesn't matter...
|
20
|
+
def compatible?(image)
|
21
|
+
self.pixel_width == image.pixel_width
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
"#{name} pixel width: #{pixel_width}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def last_scanline(idx)
|
29
|
+
last_row_index = idx - 1
|
30
|
+
(last_row_index < 0 ? [] : @data[last_row_index])
|
31
|
+
end
|
32
|
+
|
33
|
+
def merge_left( other )
|
34
|
+
merged = ImageData.new(@properties.merge(:scanline_width => self.scanline_width + other.scanline_width,
|
35
|
+
:name => "#{self.name}_#{other.name}"))
|
36
|
+
other.each_with_index do |row, idx|
|
37
|
+
merged[idx] = row + self[idx]
|
38
|
+
end
|
39
|
+
merged
|
40
|
+
end
|
41
|
+
|
42
|
+
def fill_to_height( desired_height )
|
43
|
+
return self if desired_height == height
|
44
|
+
|
45
|
+
img = ImageData.new(@properties.merge(:data => @data.clone))
|
46
|
+
empty_row = [0] * ( scanline_width )
|
47
|
+
|
48
|
+
( desired_height - height ).times do
|
49
|
+
img << empty_row
|
50
|
+
end
|
51
|
+
img
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_rgba( alpha_value=255 )
|
55
|
+
# test that conversion updates pixel width, scanline width
|
56
|
+
|
57
|
+
return self if pixel_width == RGBA_WIDTH
|
58
|
+
|
59
|
+
# so ruby 1.9 has different ideas then 1.8 on how Enumerable should work
|
60
|
+
# 1.9 returns Enumerable object if no block given
|
61
|
+
# 1.8 complains if no block, unless using enum_x methods
|
62
|
+
slice_method = (RUBY_VERSION.include?( "1.9" )) ? :each_slice : :enum_slice
|
63
|
+
|
64
|
+
rgba_data = @data.map do |row|
|
65
|
+
pixels = row.send( slice_method, RGB_WIDTH )
|
66
|
+
pixels.inject([]){|result, pixel| result + pixel + [alpha_value] }
|
67
|
+
end
|
68
|
+
|
69
|
+
ImageData.new( :data => rgba_data,
|
70
|
+
:pixel_width => 4,
|
71
|
+
:scanline_width => (@properties[:scanline_width] / RGB_WIDTH) * RGBA_WIDTH,
|
72
|
+
:name => name)
|
73
|
+
end
|
74
|
+
|
75
|
+
def [](row)
|
76
|
+
@data[row]
|
77
|
+
end
|
78
|
+
|
79
|
+
def []=(idx, row_data)
|
80
|
+
@data[idx] = row_data
|
81
|
+
end
|
82
|
+
|
83
|
+
def scanline(row)
|
84
|
+
self[row]
|
85
|
+
end
|
86
|
+
|
87
|
+
def empty?
|
88
|
+
@data.empty?
|
89
|
+
end
|
90
|
+
|
91
|
+
def height
|
92
|
+
size
|
93
|
+
end
|
94
|
+
|
95
|
+
def size
|
96
|
+
@data.size
|
97
|
+
end
|
98
|
+
|
99
|
+
def last
|
100
|
+
@data.last
|
101
|
+
end
|
102
|
+
|
103
|
+
def <<(row)
|
104
|
+
@data << row
|
105
|
+
self
|
106
|
+
end
|
107
|
+
|
108
|
+
def each(&block)
|
109
|
+
@data.each &block
|
110
|
+
end
|
111
|
+
|
112
|
+
def each_with_index(&block)
|
113
|
+
@data.each_with_index(&block)
|
114
|
+
end
|
115
|
+
|
116
|
+
def flatten!
|
117
|
+
@data.flatten!
|
118
|
+
end
|
119
|
+
|
120
|
+
def pack(*args)
|
121
|
+
@data.pack(*args)
|
122
|
+
end
|
123
|
+
|
124
|
+
def == (other)
|
125
|
+
@data == other
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
class MtimeTracker
|
2
|
+
def initialize(dir, options = {})
|
3
|
+
@dir = dir
|
4
|
+
@options = options
|
5
|
+
end
|
6
|
+
|
7
|
+
def cleanup
|
8
|
+
File.delete(mtime_file) rescue nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def fresh?
|
12
|
+
not File.exists?(mtime_file)
|
13
|
+
end
|
14
|
+
|
15
|
+
def files
|
16
|
+
return @files if @files
|
17
|
+
@files = without_exclusions(Dir.glob(@dir + "/**/*"))
|
18
|
+
end
|
19
|
+
|
20
|
+
def without_exclusions(files)
|
21
|
+
return files unless @options[:exclude]
|
22
|
+
exclusions = [@options[:exclude]].flatten
|
23
|
+
files.select{|f| not exclusions.any?{|e| f.match e}}
|
24
|
+
end
|
25
|
+
|
26
|
+
def current_mtimes
|
27
|
+
@current ||= files.inject({}) do |map, f|
|
28
|
+
map[f] = File.mtime(f).to_i; map
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def file_changed?(file)
|
33
|
+
mtimes[file] != current_mtimes[file]
|
34
|
+
end
|
35
|
+
|
36
|
+
def mtimes
|
37
|
+
return @mtimes if @mtimes
|
38
|
+
return {} unless File.exists?(mtime_file)
|
39
|
+
@mtimes = read_mtimes
|
40
|
+
end
|
41
|
+
|
42
|
+
def read_mtimes
|
43
|
+
mtimes = {}
|
44
|
+
File.open(mtime_file) do |f|
|
45
|
+
f.each do |line|
|
46
|
+
name, time = line.split("\t")
|
47
|
+
mtimes[name] = time.to_i
|
48
|
+
end
|
49
|
+
end
|
50
|
+
mtimes
|
51
|
+
end
|
52
|
+
|
53
|
+
def changeset
|
54
|
+
files.select{|f| file_changed?(f)}
|
55
|
+
end
|
56
|
+
|
57
|
+
def has_changes?
|
58
|
+
not changeset.empty?
|
59
|
+
end
|
60
|
+
|
61
|
+
def mtime_file
|
62
|
+
@dir + "/.mtimes"
|
63
|
+
end
|
64
|
+
|
65
|
+
def update
|
66
|
+
File.open(mtime_file, 'w') do |f|
|
67
|
+
current = current_mtimes
|
68
|
+
flat = current.map{|k, v| "#{k}\t#{v}\n"}.join
|
69
|
+
f.write flat
|
70
|
+
end
|
71
|
+
reset
|
72
|
+
end
|
73
|
+
|
74
|
+
def reset
|
75
|
+
@mtimes = nil
|
76
|
+
@current = nil
|
77
|
+
@files = nil
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module PNG
|
2
|
+
class Chunk
|
3
|
+
def chunk_name
|
4
|
+
raise "looks like you havn't subclassed and defined a chunk_name"
|
5
|
+
end
|
6
|
+
|
7
|
+
def to_chunk
|
8
|
+
to_check = chunk_name + encode
|
9
|
+
[encode.length].pack("N") + to_check + [Zlib.crc32(to_check)].pack("N")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module PNG
|
2
|
+
class Filters
|
3
|
+
class << self
|
4
|
+
def fetch_pixel(idx, row)
|
5
|
+
idx < 0 ? 0 : row[idx] || 0
|
6
|
+
end
|
7
|
+
|
8
|
+
#filter methods are inlined here for performance
|
9
|
+
def decode( filter_type, value, index, row, last_row, record_width )
|
10
|
+
case filter_type
|
11
|
+
when 0
|
12
|
+
#no filter
|
13
|
+
value
|
14
|
+
when 1
|
15
|
+
#left
|
16
|
+
(value + fetch_pixel(index - record_width, row)) % 256
|
17
|
+
when 2
|
18
|
+
#left
|
19
|
+
(value + fetch_pixel(index, last_row)) % 256
|
20
|
+
when 3
|
21
|
+
#avg
|
22
|
+
(value + ( (fetch_pixel(index - record_width, row) + fetch_pixel(index, last_row)) / 2 ).floor) % 256
|
23
|
+
when 4
|
24
|
+
#paeth
|
25
|
+
left = fetch_pixel(index - record_width, row)
|
26
|
+
above = fetch_pixel(index, last_row)
|
27
|
+
upper_left = fetch_pixel(index - record_width, last_row)
|
28
|
+
|
29
|
+
pr = paeth_predictor( left, above, upper_left )
|
30
|
+
|
31
|
+
(value + pr) % 256
|
32
|
+
else
|
33
|
+
raise "Invalid filter type (#{filter_type})"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
def encode( filter_type, value, index, row, last_row, record_width )
|
37
|
+
case filter_type
|
38
|
+
when 0
|
39
|
+
value
|
40
|
+
when 1
|
41
|
+
#left
|
42
|
+
(value - fetch_pixel(index - record_width, row)) % 256
|
43
|
+
when 2
|
44
|
+
#up
|
45
|
+
(value - fetch_pixel(index, last_row)) % 256
|
46
|
+
when 3
|
47
|
+
#avg
|
48
|
+
(value - ( (fetch_pixel(index - record_width, row) + fetch_pixel(index, last_row)) / 2 ).floor) % 256
|
49
|
+
when 4
|
50
|
+
#paeth
|
51
|
+
left = fetch_pixel(index - record_width, row)
|
52
|
+
above = fetch_pixel(index, last_row)
|
53
|
+
upper_left = fetch_pixel(index - record_width, last_row)
|
54
|
+
|
55
|
+
pr = paeth_predictor( left, above, upper_left )
|
56
|
+
(value - pr) % 256
|
57
|
+
else
|
58
|
+
raise "Filter type (#{filter_type}) is not supported"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def paeth_predictor( left, above, upper_left )
|
65
|
+
p = left + above - upper_left
|
66
|
+
pa = (p - left).abs
|
67
|
+
pb = (p - above).abs
|
68
|
+
pc = (p - upper_left).abs
|
69
|
+
|
70
|
+
pr = upper_left
|
71
|
+
if ( pa <= pb and pa <= pc)
|
72
|
+
pr = left
|
73
|
+
elsif (pb <= pc)
|
74
|
+
pr = above
|
75
|
+
end
|
76
|
+
pr
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|