css-spriter 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. data/.gitignore +12 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +81 -0
  4. data/Rakefile +45 -0
  5. data/VERSION +1 -0
  6. data/bin/css-spriter +45 -0
  7. data/bin/png_info +61 -0
  8. data/examples/filter_util.rb +17 -0
  9. data/examples/sprites/.mtimes +12 -0
  10. data/examples/sprites/README +5 -0
  11. data/examples/sprites/fragment.css +0 -0
  12. data/examples/sprites/index.html +26 -0
  13. data/examples/sprites/many_sized_cats/.mtimes +3 -0
  14. data/examples/sprites/many_sized_cats/cat-on-keyboard.png +0 -0
  15. data/examples/sprites/many_sized_cats/darth_cat.png +0 -0
  16. data/examples/sprites/many_sized_cats/fragment.css +21 -0
  17. data/examples/sprites/many_sized_cats/music-keyboard-cat.png +0 -0
  18. data/examples/sprites/many_sized_cats/sprite.css +21 -0
  19. data/examples/sprites/many_sized_cats/sprite.png +0 -0
  20. data/examples/sprites/server.rb +10 -0
  21. data/examples/sprites/sprite.css +49 -0
  22. data/examples/sprites/words/.mtimes +4 -0
  23. data/examples/sprites/words/fragment.css +28 -0
  24. data/examples/sprites/words/latitude.png +0 -0
  25. data/examples/sprites/words/of.png +0 -0
  26. data/examples/sprites/words/set.png +0 -0
  27. data/examples/sprites/words/specified.png +0 -0
  28. data/examples/sprites/words/sprite.css +28 -0
  29. data/examples/sprites/words/sprite.png +0 -0
  30. data/init.rb +1 -0
  31. data/lib/css-spriter.rb +16 -0
  32. data/lib/css-spriter/directory_processor.rb +94 -0
  33. data/lib/css-spriter/image_data.rb +128 -0
  34. data/lib/css-spriter/mtime_tracker.rb +79 -0
  35. data/lib/css-spriter/png/chunk.rb +12 -0
  36. data/lib/css-spriter/png/file_header.rb +7 -0
  37. data/lib/css-spriter/png/filters.rb +80 -0
  38. data/lib/css-spriter/png/idat.rb +26 -0
  39. data/lib/css-spriter/png/iend.rb +9 -0
  40. data/lib/css-spriter/png/ihdr.rb +34 -0
  41. data/lib/css-spriter/png/image.rb +153 -0
  42. data/lib/css-spriter/png/parser.rb +54 -0
  43. data/lib/css-spriter/processor.rb +28 -0
  44. data/lib/css-spriter/sprite.rb +39 -0
  45. data/lib/css-spriter/stylesheet_builder.rb +22 -0
  46. data/spec/builders/image_builder.rb +23 -0
  47. data/spec/css_fragments/deep/style/fragment.css +1 -0
  48. data/spec/css_fragments/some/fragment.css +1 -0
  49. data/spec/expected_output/merge_right_test.png +0 -0
  50. data/spec/expected_output/write_test.png +0 -0
  51. data/spec/image_data_spec.rb +63 -0
  52. data/spec/images/lightening.png +0 -0
  53. data/spec/integration_spec.rb +151 -0
  54. data/spec/lib/file_header_spec.rb +10 -0
  55. data/spec/lib/idat_spec.rb +31 -0
  56. data/spec/lib/ihdr_spec.rb +43 -0
  57. data/spec/lib/image_spec.rb +42 -0
  58. data/spec/lib/parser_spec.rb +12 -0
  59. data/spec/lib/sprite_spec.rb +39 -0
  60. data/spec/mtime_tracking_spec.rb +69 -0
  61. data/spec/spec.opts +1 -0
  62. data/spec/spec_helper.rb +17 -0
  63. data/spec/sprite_dirs/words/latitude.png +0 -0
  64. data/spec/sprite_dirs/words/of.png +0 -0
  65. data/spec/sprite_dirs/words/set.png +0 -0
  66. data/spec/sprite_dirs/words/specified.png +0 -0
  67. data/spec/tmp/merge_right_test.png +0 -0
  68. data/spec/tmp/write_test.png +0 -0
  69. data/tasks/spriter_tasks.rake +25 -0
  70. 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,9 @@
1
+ module PNG
2
+ class IEND < Chunk
3
+ def encode; "" end
4
+
5
+ def chunk_name
6
+ "IEND"
7
+ end
8
+ end
9
+ 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
@@ -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