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,26 @@
|
|
1
|
+
module PNG
|
2
|
+
class IDAT < Chunk
|
3
|
+
# I don't like that @compressed contains different values depending on how you're using it
|
4
|
+
# maybe we should introduce a builder?
|
5
|
+
def initialize( uncompressed="" )
|
6
|
+
@compressed = ""
|
7
|
+
@compressed += Zlib::Deflate.deflate( uncompressed.pack("C*") ) unless uncompressed == ""
|
8
|
+
end
|
9
|
+
|
10
|
+
def <<( data )
|
11
|
+
@compressed << data
|
12
|
+
end
|
13
|
+
|
14
|
+
def encode
|
15
|
+
@compressed
|
16
|
+
end
|
17
|
+
|
18
|
+
def uncompressed
|
19
|
+
@uncompressed ||= Zlib::Inflate.inflate( @compressed ).unpack("C*")
|
20
|
+
end
|
21
|
+
|
22
|
+
def chunk_name
|
23
|
+
"IDAT"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module PNG
|
2
|
+
class IHDR < Chunk
|
3
|
+
SUPPORTED_COLOR_TYPES = [2,3,6]
|
4
|
+
attr_accessor :width, :height, :depth, :color_type
|
5
|
+
# attr_accessor :compression_method, :filter_method, :interlace_method
|
6
|
+
|
7
|
+
def self.new_from_raw( data )
|
8
|
+
raw = data.unpack("N2C5")
|
9
|
+
|
10
|
+
new( *raw[0..6] )
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize( width, height, depth=8, color_type=2, compression=0, filter=0, interlace=0 )
|
14
|
+
raise "for now, css-spriter only supports non-interlaced images" unless interlace == 0
|
15
|
+
raise "for now, css-spriter only supports images with a bit depth of 8" unless depth == 8
|
16
|
+
unless SUPPORTED_COLOR_TYPES.include? color_type
|
17
|
+
raise "for now, css-spriter only supports color types #{SUPPORTED_COLOR_TYPES.JOIN(',')} color type was #{color_type}"
|
18
|
+
end
|
19
|
+
@width, @height, @depth, @color_type = width, height, depth, color_type
|
20
|
+
end
|
21
|
+
|
22
|
+
def encode
|
23
|
+
to_a.pack("N2C5")
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_a
|
27
|
+
[@width, @height, @depth, @color_type, 0, 0, 0]
|
28
|
+
end
|
29
|
+
|
30
|
+
def chunk_name
|
31
|
+
"IHDR"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module PNG
|
2
|
+
class Image
|
3
|
+
#color types
|
4
|
+
RGB = 2
|
5
|
+
RGBA = 6
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def image_data( file_name, options={} )
|
9
|
+
image_options = {:rgba => true}.merge( options )
|
10
|
+
|
11
|
+
png = open(file_name)
|
12
|
+
|
13
|
+
return png.to_image unless image_options[:rgba]
|
14
|
+
png.to_image.to_rgba
|
15
|
+
end
|
16
|
+
|
17
|
+
def open( file_name )
|
18
|
+
name = File.basename( file_name, ".png" )
|
19
|
+
|
20
|
+
File.open(file_name, "r") do |f|
|
21
|
+
ihdr, idat = Parser.go!( f )
|
22
|
+
Image.new( ihdr, idat, name )
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def write( file_name, data, options = {} )
|
27
|
+
ihdr = PNG::IHDR.new(data.width, data.height, 8, color_type_of(data.pixel_width))
|
28
|
+
|
29
|
+
Image.new(ihdr, nil, file_name, :rows => data).write( file_name, options)
|
30
|
+
end
|
31
|
+
|
32
|
+
def default_filter_type
|
33
|
+
4 # paeth
|
34
|
+
end
|
35
|
+
|
36
|
+
private # class methods
|
37
|
+
|
38
|
+
def color_type_of(pixel_width)
|
39
|
+
case pixel_width
|
40
|
+
when 3
|
41
|
+
RGB
|
42
|
+
when 4
|
43
|
+
RGBA
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def initialize( ihdr, idat, name, options = {} )
|
49
|
+
@ihdr = ihdr
|
50
|
+
@idat = idat
|
51
|
+
@name = name
|
52
|
+
@rows = options[:rows]
|
53
|
+
end
|
54
|
+
|
55
|
+
attr_reader :name
|
56
|
+
def width; @ihdr.width end
|
57
|
+
def height; @ihdr.height end
|
58
|
+
def depth; @ihdr.depth end
|
59
|
+
def color_type; @ihdr.color_type end
|
60
|
+
def uncompressed; @idat.uncompressed end
|
61
|
+
|
62
|
+
def write(file_name, options={})
|
63
|
+
filter_type = options[:filter_type] || Image.default_filter_type
|
64
|
+
File.open(file_name, 'w') do |f|
|
65
|
+
f.write(generate_png( filter_type ))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# check for RGB or RGBA
|
70
|
+
def pixel_width
|
71
|
+
( color_type == RGB ? 3 : 4)
|
72
|
+
end
|
73
|
+
|
74
|
+
def filter_encoded_rows(filter_type)
|
75
|
+
out = Array.new(height)
|
76
|
+
rows.each_with_index do |row, scanline|
|
77
|
+
last_row = rows.last_scanline(scanline)
|
78
|
+
out[scanline] = encode_row( row, last_row, filter_type, pixel_width)
|
79
|
+
end
|
80
|
+
out
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_image
|
84
|
+
uncompressed = @idat.uncompressed
|
85
|
+
|
86
|
+
#scanline_width - 1 because we're stripping the filter bit
|
87
|
+
n_out = CssSpriter::ImageData.new(:scanline_width => scanline_width - 1,
|
88
|
+
:pixel_width => pixel_width,
|
89
|
+
:name => self.name,
|
90
|
+
:data => Array.new(height))
|
91
|
+
offset = 0
|
92
|
+
height.times do |scanline|
|
93
|
+
end_row = scanline_width + offset
|
94
|
+
row = uncompressed.slice(offset, scanline_width)
|
95
|
+
n_out[scanline] = decode(scanline, row, n_out, pixel_width)
|
96
|
+
offset = end_row
|
97
|
+
end
|
98
|
+
n_out
|
99
|
+
end
|
100
|
+
|
101
|
+
def to_s
|
102
|
+
inspect
|
103
|
+
end
|
104
|
+
|
105
|
+
def inspect
|
106
|
+
"#{@name} (#{height} x #{width}) [color type: #{color_type}, depth: #{depth}]"
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def scanline_width
|
112
|
+
# + 1 adds filter byte
|
113
|
+
(width * pixel_width) + 1
|
114
|
+
end
|
115
|
+
|
116
|
+
def rows
|
117
|
+
@rows ||= to_image
|
118
|
+
end
|
119
|
+
|
120
|
+
def decode(current, row, data, pixel_width)
|
121
|
+
filter_type = row.shift
|
122
|
+
decode_row(row, data.last_scanline(current), filter_type, pixel_width)
|
123
|
+
end
|
124
|
+
|
125
|
+
def decode_row(row, last_scanline, filter_type, pixel_width)
|
126
|
+
o = Array.new(row.size)
|
127
|
+
row.each_with_index do |byte, i|
|
128
|
+
o[i] = Filters.decode(filter_type, byte, i, o, last_scanline, pixel_width)
|
129
|
+
end
|
130
|
+
o
|
131
|
+
end
|
132
|
+
|
133
|
+
def encode_row( row, last_scanline, filter_type, pixel_width )
|
134
|
+
o = Array.new(row.size)
|
135
|
+
row.each_with_index do |byte, scanline|
|
136
|
+
o[scanline] = Filters.encode( filter_type, byte, scanline, row, last_scanline, pixel_width)
|
137
|
+
end
|
138
|
+
o.unshift( filter_type )
|
139
|
+
end
|
140
|
+
|
141
|
+
def generate_png( filter_type )
|
142
|
+
file_header = PNG::FileHeader.new.encode
|
143
|
+
raw_data = filter_encoded_rows( filter_type )
|
144
|
+
raw_data.flatten!
|
145
|
+
|
146
|
+
ihdr = PNG::IHDR.new( width, height, depth, color_type ).to_chunk
|
147
|
+
idat = PNG::IDAT.new( raw_data ).to_chunk
|
148
|
+
iend = PNG::IEND.new.to_chunk
|
149
|
+
|
150
|
+
file_header + ihdr + idat + iend
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module PNG
|
2
|
+
class Parser
|
3
|
+
def self.go!(file)
|
4
|
+
#TODO: Wanted to remove instance go! and use initialize, didn't work.
|
5
|
+
#Weird
|
6
|
+
Parser.new.go!(file)
|
7
|
+
end
|
8
|
+
def go!( file )
|
9
|
+
check_header( file )
|
10
|
+
|
11
|
+
while(not file.eof?) do
|
12
|
+
parse_chunk(file)
|
13
|
+
end
|
14
|
+
|
15
|
+
[ @ihdr, @idat ]
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def check_header( file )
|
20
|
+
header = file.read(8)
|
21
|
+
raise "Invalid PNG file header" unless ( header == FileHeader.new.encode)
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse_chunk(f)
|
25
|
+
len = f.read(4).unpack("N")
|
26
|
+
type = f.read(4)
|
27
|
+
data = f.read(len[0])
|
28
|
+
crc = f.read(4)
|
29
|
+
|
30
|
+
raise "invalid CRC for chunk type #{type}" if crc_invalid?( type, data, crc )
|
31
|
+
|
32
|
+
handle(type, data)
|
33
|
+
end
|
34
|
+
|
35
|
+
def handle(type, data)
|
36
|
+
case(type)
|
37
|
+
when "IHDR"
|
38
|
+
@ihdr = PNG::IHDR.new_from_raw( data )
|
39
|
+
@width, @height, @depth, @color_type = @ihdr.to_a
|
40
|
+
when "IDAT"
|
41
|
+
@idat ||= PNG::IDAT.new
|
42
|
+
@idat << data
|
43
|
+
when "IEND"
|
44
|
+
# NOOP
|
45
|
+
else
|
46
|
+
#puts "Ignoring chunk type #{type}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def crc_invalid?( type, data, crc )
|
51
|
+
[Zlib.crc32( type + data )] != crc.unpack("N")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module CssSpriter
|
2
|
+
class Processor
|
3
|
+
def initialize(opts)
|
4
|
+
@options = opts
|
5
|
+
@processors = dir_processors
|
6
|
+
@css_builder = StylesheetBuilder.new(@options[:source])
|
7
|
+
@css_builder.output_file(@options[:css_file] || @options[:source] + "/sprite.css")
|
8
|
+
end
|
9
|
+
|
10
|
+
def write
|
11
|
+
@processors.each{|d| d.write}
|
12
|
+
@css_builder.write
|
13
|
+
end
|
14
|
+
|
15
|
+
def directories
|
16
|
+
Dir.glob(@options[:source] + "/**/").map{|d| d.gsub(/\/$/, "")}
|
17
|
+
end
|
18
|
+
|
19
|
+
def dir_processors
|
20
|
+
directories.map{|d| DirectoryProcessor.new(d, @options)}
|
21
|
+
end
|
22
|
+
|
23
|
+
def cleanup
|
24
|
+
@processors.each{|d| d.cleanup}
|
25
|
+
@css_builder.cleanup
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class Sprite
|
2
|
+
class ImageFormatException < Exception; end
|
3
|
+
attr_reader :images, :max_height
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@images = []
|
7
|
+
@locations = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def append( image )
|
11
|
+
@images.each do |i|
|
12
|
+
unless i.compatible? image
|
13
|
+
raise ImageFormatException.new("Image #{i} not compatible with #{image}")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
@images << image
|
18
|
+
@max_height = @images.map{ |i| i.height }.max
|
19
|
+
end
|
20
|
+
|
21
|
+
def locations
|
22
|
+
@images.inject(0) do |x, image|
|
23
|
+
@locations[image.name.to_sym] = { :x => -(x),
|
24
|
+
:width => image.width,
|
25
|
+
:height => image.height}
|
26
|
+
image.width + x
|
27
|
+
end
|
28
|
+
@locations
|
29
|
+
end
|
30
|
+
|
31
|
+
def write( output_filename )
|
32
|
+
return if @images.empty?
|
33
|
+
right_sized = @images.map{|i| i.fill_to_height(@max_height)}
|
34
|
+
# head is the last image, then we merge left
|
35
|
+
head, *tail = right_sized.reverse
|
36
|
+
result = tail.inject( head ){ |head, image| head.merge_left( image ) }
|
37
|
+
PNG::Image.write( output_filename, result )
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class StylesheetBuilder
|
2
|
+
def initialize(dir)
|
3
|
+
@dir = dir
|
4
|
+
@output_file = @dir + "/sprite.css"
|
5
|
+
end
|
6
|
+
|
7
|
+
def output_file(file)
|
8
|
+
@output_file = file
|
9
|
+
end
|
10
|
+
|
11
|
+
def css
|
12
|
+
@css ||= Dir.glob(@dir + "/**/fragment.css").inject("") {|acc, f| acc + File.read(f)}
|
13
|
+
end
|
14
|
+
|
15
|
+
def write
|
16
|
+
File.open(@output_file, 'w') {|f| f.write(css)}
|
17
|
+
end
|
18
|
+
|
19
|
+
def cleanup
|
20
|
+
File.delete(@output_file) rescue nil
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class ImageBuilder
|
2
|
+
def initialize( general_options={} )
|
3
|
+
default_options = {
|
4
|
+
:width => 100,
|
5
|
+
:height => 100,
|
6
|
+
:name => "test"
|
7
|
+
}
|
8
|
+
@general_options = default_options.merge general_options
|
9
|
+
end
|
10
|
+
|
11
|
+
def data(width, height)
|
12
|
+
Array.new((width * height * 3) + height).fill(0)
|
13
|
+
end
|
14
|
+
|
15
|
+
def build( specific_options={} )
|
16
|
+
args = @general_options.merge specific_options
|
17
|
+
|
18
|
+
ihdr = PNG::IHDR.new( args[:width], args[:height] )
|
19
|
+
idat = PNG::IDAT.new( args[:data] || data(args[:width], args[:height]) )
|
20
|
+
|
21
|
+
PNG::Image.new( ihdr, idat, args[:name] )
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
.deep {}
|
@@ -0,0 +1 @@
|
|
1
|
+
.some_style {}
|
Binary file
|
Binary file
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe CssSpriter::ImageData do
|
4
|
+
before do
|
5
|
+
data = [[1,2,3],
|
6
|
+
[4,5,6]]
|
7
|
+
@id = CssSpriter::ImageData.new(:scanline_width => 3, :pixel_width => 3, :data => data)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "can fill to a specified height" do
|
11
|
+
result = @id.fill_to_height(3)
|
12
|
+
result.should == [[1,2,3], [4,5,6], [0,0,0]]
|
13
|
+
end
|
14
|
+
it "has scanline width and pixel width attributes" do
|
15
|
+
@id.scanline_width.should == 3
|
16
|
+
@id.pixel_width.should == 3
|
17
|
+
end
|
18
|
+
|
19
|
+
it "can give you any arbitrary row in the data set" do
|
20
|
+
@id[0].should == [1,2,3]
|
21
|
+
@id.scanline(0).should == [1,2,3]
|
22
|
+
end
|
23
|
+
|
24
|
+
it "has an empty array by default" do
|
25
|
+
id = CssSpriter::ImageData.new
|
26
|
+
id.empty?.should be_true
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should return nil when asked for an index that doesn't exist" do
|
30
|
+
id = CssSpriter::ImageData.new
|
31
|
+
id[0].should be_nil
|
32
|
+
end
|
33
|
+
|
34
|
+
it "can be assigned a row" do
|
35
|
+
@id[0] = [1,2,3]
|
36
|
+
@id[0].should == [1,2,3]
|
37
|
+
end
|
38
|
+
|
39
|
+
it "behaves like an array" do
|
40
|
+
@id << [1,2,3]
|
41
|
+
@id.last.should == [1,2,3]
|
42
|
+
|
43
|
+
@id.size.should == 3
|
44
|
+
end
|
45
|
+
|
46
|
+
it "will return the last scanline given a current index" do
|
47
|
+
@id.last_scanline(1).should == [1,2,3]
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "RGBA conversion" do
|
51
|
+
it "updates pixel width" do
|
52
|
+
@id.to_rgba.pixel_width.should == 4 # RGBA pixel width
|
53
|
+
end
|
54
|
+
|
55
|
+
it "updates scanline width" do
|
56
|
+
@id.to_rgba.scanline_width.should == 4
|
57
|
+
end
|
58
|
+
|
59
|
+
it "puts in an alpha byte with a default value of 255" do
|
60
|
+
@id.to_rgba[0].should == [1,2,3,255]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|