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.
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