spittle 0.9.0

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