spittle 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/.gitignore +12 -0
  2. data/README +19 -0
  3. data/Rakefile +45 -0
  4. data/VERSION +1 -0
  5. data/bin/spittle +15 -0
  6. data/examples/sprites/README +5 -0
  7. data/examples/sprites/apple/apple.png +0 -0
  8. data/examples/sprites/apple/divider.png +0 -0
  9. data/examples/sprites/apple/downloads.png +0 -0
  10. data/examples/sprites/apple/fragment.css +63 -0
  11. data/examples/sprites/apple/iphone.png +0 -0
  12. data/examples/sprites/apple/itunes.png +0 -0
  13. data/examples/sprites/apple/mac.png +0 -0
  14. data/examples/sprites/apple/search.png +0 -0
  15. data/examples/sprites/apple/sprite.png +0 -0
  16. data/examples/sprites/apple/store.png +0 -0
  17. data/examples/sprites/apple/support.png +0 -0
  18. data/examples/sprites/fragment.css +0 -0
  19. data/examples/sprites/index.html +33 -0
  20. data/examples/sprites/server.rb +10 -0
  21. data/examples/sprites/sprite.css +91 -0
  22. data/examples/sprites/words/fragment.css +28 -0
  23. data/examples/sprites/words/latitude.png +0 -0
  24. data/examples/sprites/words/of.png +0 -0
  25. data/examples/sprites/words/set.png +0 -0
  26. data/examples/sprites/words/specified.png +0 -0
  27. data/examples/sprites/words/sprite.css +24 -0
  28. data/examples/sprites/words/sprite.png +0 -0
  29. data/lib/spittle/chunk.rb +12 -0
  30. data/lib/spittle/directory_spriter.rb +63 -0
  31. data/lib/spittle/file_header.rb +7 -0
  32. data/lib/spittle/filters.rb +64 -0
  33. data/lib/spittle/idat.rb +26 -0
  34. data/lib/spittle/iend.rb +9 -0
  35. data/lib/spittle/ihdr.rb +30 -0
  36. data/lib/spittle/image.rb +114 -0
  37. data/lib/spittle/parser.rb +54 -0
  38. data/lib/spittle/processor.rb +28 -0
  39. data/lib/spittle/sprite.rb +39 -0
  40. data/lib/spittle/stylesheet_builder.rb +22 -0
  41. data/lib/spittle.rb +16 -0
  42. data/spec/builders/image_builder.rb +22 -0
  43. data/spec/css_fragments/deep/style/fragment.css +1 -0
  44. data/spec/css_fragments/some/fragment.css +1 -0
  45. data/spec/expected_output/merge_right_test.png +0 -0
  46. data/spec/expected_output/write_test.png +0 -0
  47. data/spec/images/lightening.png +0 -0
  48. data/spec/integration_spec.rb +134 -0
  49. data/spec/lib/file_header_spec.rb +10 -0
  50. data/spec/lib/idat_spec.rb +30 -0
  51. data/spec/lib/ihdr_spec.rb +43 -0
  52. data/spec/lib/image_spec.rb +19 -0
  53. data/spec/lib/parser_spec.rb +12 -0
  54. data/spec/lib/sprite_spec.rb +36 -0
  55. data/spec/spec.opts +1 -0
  56. data/spec/spec_helper.rb +16 -0
  57. data/spec/sprite_dirs/words/latitude.png +0 -0
  58. data/spec/sprite_dirs/words/of.png +0 -0
  59. data/spec/sprite_dirs/words/set.png +0 -0
  60. data/spec/sprite_dirs/words/specified.png +0 -0
  61. data/spec/tmp/merge_right_test.png +0 -0
  62. data/spec/tmp/write_test.png +0 -0
  63. metadata +137 -0
@@ -0,0 +1,114 @@
1
+ module PNG
2
+ class Image
3
+
4
+ def self.open( file_name )
5
+ name = File.basename( file_name, ".png" )
6
+
7
+ File.open(file_name, "r") do |f|
8
+ ihdr, idat = Parser.go!( f )
9
+ Image.new( ihdr, idat, name )
10
+ end
11
+
12
+ end
13
+
14
+ def initialize( ihdr, idat, name )
15
+ @ihdr = ihdr
16
+ @idat = idat
17
+ @name = name
18
+ end
19
+
20
+ attr_reader :name
21
+ def width; @ihdr.width end
22
+ def height; @ihdr.height end
23
+ def depth; @ihdr.depth end
24
+ def color_type; @ihdr.color_type end
25
+
26
+ def compatible?(image)
27
+ self.height == image.height
28
+ end
29
+
30
+ def write(file_name)
31
+ File.open(file_name, 'w') do |f|
32
+ f.write(generate_png)
33
+ end
34
+ end
35
+
36
+ def merge_left( other )
37
+ l = other.rows
38
+ r = self.rows
39
+
40
+ data = l.zip r
41
+
42
+ #prepend the filter byte 0 = no filter
43
+ data.each { |row| row.unshift(0) }
44
+ data.flatten!
45
+
46
+ ihdr = IHDR.new( width + other.width, height, depth, color_type)
47
+ idat = IDAT.new( data )
48
+ img_name = "#{name}_#{other.name}"
49
+
50
+ Image.new( ihdr, idat, img_name )
51
+ end
52
+
53
+ #color types
54
+ RGB = 2
55
+ RGBA = 3
56
+
57
+ # check for RGB or RGBA
58
+ def pixel_width
59
+ ( color_type == RGB ? 3 : 4)
60
+ end
61
+
62
+ def scanline_width
63
+ # + 1 adds filter byte
64
+ (width * pixel_width) + 1
65
+ end
66
+
67
+ def rows
68
+ out = []
69
+ offset = 0
70
+
71
+ height.times do |scanline|
72
+ end_row = scanline_width + offset
73
+ row = @idat.uncompressed.slice(offset, scanline_width)
74
+ out << decode(scanline, row, out, pixel_width)
75
+ offset = end_row
76
+ end
77
+ out
78
+ end
79
+
80
+ def inspect
81
+ "color type: #{color_type}, depth: #{depth}, width: #{width}, height: #{height}"
82
+ end
83
+ private
84
+
85
+ def last_scanline(current, data)
86
+ (current - 1 < 0 ? [] : data[current - 1])
87
+ end
88
+
89
+ def decode(current, row, data, pixel_width)
90
+ filter_type = row.shift
91
+
92
+ process_row(row, last_scanline(current, data), filter_type, pixel_width)
93
+ end
94
+
95
+ def process_row(row, last_scanline, filter_type, pixel_width)
96
+ o = []
97
+ row.each_with_index do |e, i|
98
+ o[i] = Filters.call(filter_type, e, i, o, last_scanline, pixel_width)
99
+ end
100
+ o
101
+ end
102
+
103
+ def generate_png
104
+ file_header = PNG::FileHeader.new.encode
105
+ raw_data = @idat.uncompressed
106
+
107
+ ihdr = PNG::IHDR.new( width, height, depth, color_type ).to_chunk
108
+ idat = PNG::IDAT.new( raw_data ).to_chunk
109
+ iend = PNG::IEND.new.to_chunk
110
+
111
+ file_header + ihdr + idat + iend
112
+ end
113
+ end
114
+ 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 Spittle
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)}
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
+ module PNG
2
+ class ImageFormatException < Exception; end
3
+ class Sprite
4
+ attr_reader :images
5
+
6
+ def initialize
7
+ @images = []
8
+ @locations = {}
9
+ end
10
+
11
+ def append( image )
12
+ @images.each do |i|
13
+ unless i.compatible? image
14
+ raise ImageFormatException.new("Incompatible image #{i}")
15
+ end
16
+ end
17
+ @images << image
18
+ end
19
+
20
+ def locations
21
+ @images.inject(0) do |x, image|
22
+ @locations[image.name.to_sym] = { :x => -(x),
23
+ :width => image.width,
24
+ :height => image.height}
25
+ image.width + x
26
+ end
27
+ @locations
28
+ end
29
+
30
+ def write( output_filename )
31
+ return if @images.empty?
32
+
33
+ # head is the last image, then we merge left
34
+ head, *tail = @images.reverse
35
+ result = tail.inject( head ){ |head, image| head.merge_left( image ) }
36
+ result.write( output_filename )
37
+ end
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 {}
21
+ end
22
+ end
data/lib/spittle.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'zlib'
2
+
3
+ $:.unshift( File.dirname( __FILE__ ))
4
+
5
+ require 'spittle/file_header'
6
+ require 'spittle/parser'
7
+ require 'spittle/filters'
8
+ require 'spittle/chunk'
9
+ require 'spittle/ihdr'
10
+ require 'spittle/idat'
11
+ require 'spittle/iend'
12
+ require 'spittle/image'
13
+ require 'spittle/sprite'
14
+ require 'spittle/directory_spriter'
15
+ require 'spittle/stylesheet_builder'
16
+ require 'spittle/processor'
@@ -0,0 +1,22 @@
1
+ class ImageBuilder
2
+ def initialize( general_options={} )
3
+ default_options = {
4
+ :width => 100,
5
+ :height => 100,
6
+ :data => [],
7
+ :name => "test"
8
+ }
9
+
10
+ @general_options = default_options.merge general_options
11
+ end
12
+
13
+
14
+ def build( specific_options={} )
15
+ args = @general_options.merge specific_options
16
+
17
+ ihdr = PNG::IHDR.new( args[:width], args[:height] )
18
+ idat = PNG::IDAT.new( args[:data] )
19
+
20
+ PNG::Image.new( ihdr, idat, args[:name] )
21
+ end
22
+ end
@@ -0,0 +1 @@
1
+ .deep {}
@@ -0,0 +1 @@
1
+ .some_style {}
Binary file
Binary file
@@ -0,0 +1,134 @@
1
+ require 'benchmark'
2
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
3
+
4
+ describe 'PNG' do
5
+ before do
6
+ @img_dir = File.dirname(__FILE__) + '/images'
7
+ @expected_dir = File.dirname(__FILE__) + '/expected_output'
8
+ @tmp_dir = File.dirname(__FILE__) + '/tmp'
9
+ end
10
+
11
+ it 'can read and write a PNG' do
12
+ img = PNG::Image.open("#{@img_dir}/lightening.png")
13
+ img.write("#{@tmp_dir}/write_test.png")
14
+ read("#{@expected_dir}/write_test.png").should == read("#{@tmp_dir}/write_test.png")
15
+ end
16
+
17
+ it 'can merge one PNG on the left of another' do
18
+ one = PNG::Image.open("#{@img_dir}/lightening.png")
19
+ two = PNG::Image.open("#{@img_dir}/lightening.png")
20
+ merged = one.merge_left two
21
+ merged.write("#{@tmp_dir}/merge_right_test.png")
22
+ read("#{@expected_dir}/merge_right_test.png").should == read("#{@tmp_dir}/merge_right_test.png")
23
+ end
24
+
25
+ def read(file_name)
26
+ File.read(file_name)
27
+ end
28
+ end
29
+
30
+ describe "Dir sprite" do
31
+ before :all do
32
+ @dir = File.dirname(__FILE__) + "/sprite_dirs/words"
33
+ @spriter = DirectoryProcessor.new(@dir)
34
+ @sprite_file = @dir + "/sprite.png"
35
+ @css_file = @dir + "/fragment.css"
36
+ @spriter.write
37
+ end
38
+
39
+ after :all do
40
+ @spriter.cleanup
41
+ end
42
+
43
+ describe "Sprite generation" do
44
+ it "provides the correct dir name" do
45
+ @spriter.dir_name.should == 'words'
46
+ end
47
+
48
+ it "find all the pngs in a directory" do
49
+ expected = ['latitude.png', 'of.png', 'set.png', 'specified.png']
50
+ images = @spriter.images
51
+ images.map{|f| f.split('/').last}.should == expected
52
+ end
53
+
54
+ it "sprites all the images in a directory" do
55
+ File.exists?(@sprite_file).should be_true
56
+ end
57
+ end
58
+
59
+ describe "CSS fragments" do
60
+ before :all do
61
+ @css = @spriter.css
62
+ end
63
+
64
+ it "should compose class names" do
65
+ @css.should include( ".words_latitude")
66
+ @css.should include( ".words_of" )
67
+ end
68
+
69
+ it "has the correct image path" do
70
+ @css.should include( "/sprite_dirs/words/sprite.png" )
71
+ end
72
+
73
+ it "should write css fragments for a sprite" do
74
+ File.exists?(@css_file).should be_true
75
+ end
76
+ end
77
+ end
78
+
79
+ describe 'Stylesheet generator' do
80
+ before :all do
81
+ @dir = File.dirname(__FILE__) + "/css_fragments"
82
+ @out = @dir + "/complete.css"
83
+ @builder = StylesheetBuilder.new(@dir)
84
+ @builder.output_file(@out)
85
+ @css = @builder.css
86
+ end
87
+
88
+ after :all do
89
+ @builder.cleanup
90
+ end
91
+
92
+ it "takes the css fragments and concatonates them into a single stylesheet" do
93
+ @css.should include( ".some_style" )
94
+ end
95
+
96
+ it "can handle nested folder structures" do
97
+ @css.should include( ".deep" )
98
+ end
99
+
100
+ it "writes the css file to the specified location" do
101
+ @builder.write
102
+ File.exists?(@out).should be_true
103
+ end
104
+ end
105
+
106
+ describe "Complete spriting process" do
107
+ before :all do
108
+ @dir = File.dirname(__FILE__) + "/sprite_dirs"
109
+ @css_file = @dir + "/sprite.css"
110
+ @spittle = Spittle::Processor.new(:source => @dir, :css_file => @css_file)
111
+ @spittle.write
112
+ end
113
+
114
+ after :all do
115
+ @spittle.cleanup
116
+ #making sure it cleans things up - shitty place for these
117
+ File.exists?(@css_file).should be_false
118
+ File.exists?(@dir + "/words/sprite.png").should be_false
119
+ end
120
+
121
+ it "can find all the sprite directories" do
122
+ dirs = @spittle.directories.map{|d| d.split('/').last}
123
+ dirs.should include( "words" )
124
+ end
125
+
126
+ it "generates the css file at the appropriate location" do
127
+ File.exists?(@css_file).should be_true
128
+ end
129
+
130
+ it "creates sprites/css for all subfolders" do
131
+ File.exists?(@dir + "/words/sprite.png").should be_true
132
+ File.exists?(@dir + "/words/fragment.css").should be_true
133
+ end
134
+ end
@@ -0,0 +1,10 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe PNG::FileHeader do
4
+ # the pnd header provides sanity checks against most common file transfer errrors
5
+ it "outputs the PNG header" do
6
+ header = PNG::FileHeader.new.encode
7
+ header.should == [137, 80, 78, 71, 13, 10, 26, 10].pack("C*")
8
+ end
9
+ end
10
+
@@ -0,0 +1,30 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe PNG::IDAT do
4
+ before :each do
5
+ # it's just "Hello, World!" encoded
6
+ @data = "x\234\363H\315\311\311\327Q\b\317/\312IQ\004\000\037\236\004j"
7
+ end
8
+
9
+ it "accepts compressed data" do
10
+ @idat = PNG::IDAT.new
11
+
12
+ @idat << @data
13
+
14
+ @idat.encode.should == @data
15
+ end
16
+
17
+ it "can chunk its self" do
18
+ @idat = PNG::IDAT.new
19
+ @idat << @data
20
+ @chunk = chunk( "IDAT", @idat.encode )
21
+
22
+ @idat.to_chunk.should == @chunk
23
+ end
24
+
25
+ it "accepts uncompressed data for it's constructor" do
26
+ @idat = PNG::IDAT.new( "Hello, World!".unpack("C*") )
27
+
28
+ @idat.encode.should == @data
29
+ end
30
+ end
@@ -0,0 +1,43 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe PNG::IHDR do
4
+ before :each do
5
+ @width = 40
6
+ @height = 40
7
+ @bit_depth = 8
8
+ @color_type = 2
9
+
10
+ @raw = [@width, @height, @bit_depth, @color_type, 0, 0, 0].pack("N2C5")
11
+ @chunk = chunk( "IHDR", @raw )
12
+ end
13
+
14
+ it "pulls out the width from the ihdr block" do
15
+ @header = PNG::IHDR.new_from_raw( @raw )
16
+ @header.width.should == @width
17
+ end
18
+
19
+ it "pulls out the height from the ihdr block" do
20
+ @header = PNG::IHDR.new_from_raw( @raw )
21
+ @header.height.should == @height
22
+ end
23
+
24
+ it "pulls out the bit depth from the ihdr block" do
25
+ @header = PNG::IHDR.new_from_raw( @raw )
26
+ @header.depth.should == @bit_depth
27
+ end
28
+
29
+ it "pulls out the color type from the ihdr block" do
30
+ @header = PNG::IHDR.new_from_raw( @raw )
31
+ @header.color_type.should == @color_type
32
+ end
33
+
34
+ it "encodes it's self properly" do
35
+ @header = PNG::IHDR.new_from_raw( @raw )
36
+ @header.encode.should == @raw
37
+ end
38
+
39
+ it "should be able to make a header chunk" do
40
+ @header = PNG::IHDR.new_from_raw( @raw )
41
+ @header.to_chunk.should == @chunk
42
+ end
43
+ end
@@ -0,0 +1,19 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe PNG::Image do
4
+ before :each do
5
+ @sprite = PNG::Sprite.new
6
+
7
+ @builder = ImageBuilder.new
8
+
9
+ # 0 is the filter type for the row, then an RGB triplet since builder defaults to color type 2
10
+ @image1 = @builder.build( :width => 1, :height => 1, :name => "image1", :data => [0,1,2,3] )
11
+ @image2 = @builder.build( :width => 1, :height => 1, :name => "image2", :data => [0,4,5,6] )
12
+ end
13
+
14
+ it "can merge left" do
15
+ result = @image1.merge_left @image2
16
+
17
+ result.rows.should == [[4,5,6,1,2,3]]
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe PNG::Parser do
4
+ it "errors out when the file header is wrong" do
5
+ bad_header = [5, 80, 78, 71, 13, 10, 26, 10].pack("C*")
6
+ file = StringIO.new( bad_header)
7
+
8
+ lambda {
9
+ Parser.go!( file )
10
+ }.should raise_error
11
+ end
12
+ end
@@ -0,0 +1,36 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe PNG::Sprite do
4
+ before :each do
5
+ @sprite = PNG::Sprite.new
6
+ @builder = ImageBuilder.new
7
+
8
+ @image1 = @builder.build( :width => 50, :height => 50, :name => "image1")
9
+ @image2 = @builder.build( :width => 50, :height => 50, :name => "image2")
10
+ end
11
+
12
+ it "can merge an image to the right" do
13
+ @sprite.append( @image1 )
14
+ @sprite.append( @image2 )
15
+
16
+ @sprite.images.should == [@image1, @image2]
17
+ end
18
+
19
+ it "knows the location of each image in the sprite" do
20
+ @sprite.append( @image1 )
21
+ @sprite.append( @image2 )
22
+
23
+ @sprite.locations[@image1.name.to_sym].should == {:x => -( 0 ), :width=> @image1.width, :height => @image1.height }
24
+ @sprite.locations[@image2.name.to_sym].should == {:x => -( @image2.width ), :width=> @image2.width, :height => @image2.height }
25
+ end
26
+
27
+ it "raises a pretty exception when the images are incompatible" do
28
+ taller_image = @builder.build( :width => 50, :height => 60, :name => "image")
29
+
30
+ lambda do
31
+ @sprite.append taller_image
32
+ @sprite.append @image1
33
+ end.should raise_error(PNG::ImageFormatException)
34
+ end
35
+
36
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,16 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'spittle'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+
7
+ require 'builders/image_builder'
8
+
9
+ Spec::Runner.configure do |config|
10
+
11
+ end
12
+
13
+ def chunk(type, data)
14
+ to_check = type + data
15
+ [data.length].pack("N") + to_check + [Zlib.crc32(to_check)].pack("N")
16
+ end
Binary file
Binary file
Binary file
Binary file
Binary file