chunky_png 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .DS_Store
2
+ spec/resources/testing.png
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Willem van Bergen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,24 @@
1
+ = Chunky PNG
2
+
3
+ This library can read and write PNG files. It is written in pure Ruby for
4
+ maximum portability. It requires the standard Zlib library.
5
+
6
+ == Usage
7
+
8
+ require 'chunky_png'
9
+
10
+ png = ChunkyPNG.load('filename.png')
11
+ puts png.chunks.inspect
12
+ puts png.pixel_matrix.inspect
13
+
14
+ # do something with the PNG file
15
+
16
+ File.open('out.png', 'w') { |f| png.write(f) }
17
+
18
+ (Note: this is subject to change while work is in progress)
19
+
20
+ == About
21
+
22
+ The library is written by Willem van Bergen for Floorplanner.com, and released
23
+ under the MIT license (see LICENSE). Please contact me for questions or
24
+ remarks. Patches are greatly appreciated! :-)
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ Dir['tasks/*.rake'].each { |file| load(file) }
2
+
3
+ GithubGem::RakeTasks.new(:gem)
4
+ task :default => [:spec]
@@ -0,0 +1,25 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'chunky_png'
3
+
4
+ # Do not change the version and date fields by hand. This will be done
5
+ # automatically by the gem release script.
6
+ s.version = "0.0.1"
7
+ s.date = "2010-01-10"
8
+
9
+ s.summary = "Pure ruby library for read/write, chunk-level access to PNG files"
10
+ s.description = "Pure ruby library for read/write, chunk-level access to PNG files"
11
+
12
+ s.authors = ['Willem van Bergen']
13
+ s.email = ['willem@railsdoctors.com']
14
+ s.homepage = 'http://wiki.github.com/wvanbergen/chunky_png'
15
+
16
+ s.add_development_dependency('rspec', '>= 1.2.9')
17
+
18
+ s.rdoc_options << '--title' << s.name << '--main' << 'README.rdoc' << '--line-numbers' << '--inline-source'
19
+ s.extra_rdoc_files = ['README.rdoc']
20
+
21
+ # Do not change the files and test_files fields by hand. This will be done
22
+ # automatically by the gem release script.
23
+ s.files = %w(spec/spec_helper.rb .gitignore lib/chunky_png/pixel_matrix.rb LICENSE lib/chunky_png/chunk.rb Rakefile README.rdoc spec/resources/indexed_10x10.png spec/integration/image_spec.rb lib/chunky_png/palette.rb lib/chunky_png/datastream.rb chunky_png.gemspec tasks/github-gem.rake lib/chunky_png/image.rb spec/unit/pixel_matrix_spec.rb lib/chunky_png.rb)
24
+ s.test_files = %w(spec/integration/image_spec.rb spec/unit/pixel_matrix_spec.rb)
25
+ end
data/lib/chunky_png.rb ADDED
@@ -0,0 +1,34 @@
1
+ require 'set'
2
+ require 'zlib'
3
+
4
+ require 'chunky_png/datastream'
5
+ require 'chunky_png/chunk'
6
+ require 'chunky_png/palette'
7
+ require 'chunky_png/pixel_matrix'
8
+ require 'chunky_png/image'
9
+
10
+ module ChunkyPNG
11
+ extend self
12
+
13
+ def load_from_io(io)
14
+ ChunkyPNG::Datastream.read(io)
15
+ end
16
+
17
+ def load_from_file(file)
18
+ File.open(file, 'r') { |f| load_from_io(f) }
19
+ end
20
+
21
+ def load_from_memory(string)
22
+ load_from_io(StringIO.new(string))
23
+ end
24
+
25
+ def load(arg)
26
+ if arg.respond_to?(:read)
27
+ load_from_io(arg)
28
+ elsif File.exists?(arg)
29
+ load_from_file(arg)
30
+ else
31
+ load_from_memory(arg)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,104 @@
1
+ module ChunkyPNG
2
+ class Chunk
3
+
4
+ def self.read(io)
5
+
6
+ length, type = io.read(8).unpack('Na4')
7
+ content = io.read(length)
8
+ crc = io.read(4).unpack('N').first
9
+
10
+ # verify_crc!(type, content, crc)
11
+
12
+ CHUNK_TYPES.fetch(type, Generic).read(type, content)
13
+ end
14
+
15
+ class Base
16
+ attr_accessor :type
17
+
18
+ def initialize(type, attributes = {})
19
+ self.type = type
20
+ attributes.each { |k, v| send("#{k}=", v) }
21
+ end
22
+
23
+ def write_with_crc(io, content)
24
+ io << [content.length].pack('N') << type << content
25
+ io << [Zlib.crc32(content, Zlib.crc32(type))].pack('N')
26
+ end
27
+
28
+ def write(io)
29
+ write_with_crc(io, content || '')
30
+ end
31
+ end
32
+
33
+ class Generic < Base
34
+
35
+ attr_accessor :content
36
+
37
+ def initialize(type, content = '')
38
+ super(type, :content => content)
39
+ end
40
+
41
+ def self.read(type, content)
42
+ self.new(type, content)
43
+ end
44
+ end
45
+
46
+ class Header < Base
47
+
48
+ COLOR_GRAYSCALE = 0
49
+ COLOR_TRUECOLOR = 2
50
+ COLOR_INDEXED = 3
51
+ COLOR_GRAYSCALE_ALPHA = 4
52
+ COLOR_TRUECOLOR_ALPHA = 6
53
+
54
+ attr_accessor :width, :height, :depth, :color, :compression, :filtering, :interlace
55
+
56
+ def initialize(attrs = {})
57
+ super('IHDR', attrs)
58
+ @depth ||= 8
59
+ @color ||= COLOR_TRUECOLOR
60
+ @compression ||= 0
61
+ @filtering ||= 0
62
+ @interlace ||= 0
63
+ end
64
+
65
+ def self.read(type, content)
66
+ fields = content.unpack('NNC5')
67
+ self.new(:width => fields[0], :height => fields[1], :depth => fields[2], :color => fields[3],
68
+ :compression => fields[4], :filtering => fields[5], :interlace => fields[6])
69
+ end
70
+
71
+ def content
72
+ [width, height, depth, color, compression, filtering, interlace].pack('NNC5')
73
+ end
74
+ end
75
+
76
+ class End < Base
77
+ def initialize
78
+ super('IEND')
79
+ end
80
+
81
+ def self.read(type, content)
82
+ raise 'The IEND chunk should be empty!' if content != ''
83
+ self.new
84
+ end
85
+
86
+ def content
87
+ ''
88
+ end
89
+ end
90
+
91
+ class Palette < Generic
92
+ end
93
+
94
+ class ImageData < Generic
95
+ end
96
+
97
+ # Maps chunk types to classes.
98
+ # If a chunk type is not given in this hash, a generic chunk type will be used.
99
+ CHUNK_TYPES = {
100
+ 'IHDR' => Header, 'IEND' => End, 'IDAT' => ImageData, 'PLTE' => Palette
101
+ }
102
+
103
+ end
104
+ end
@@ -0,0 +1,72 @@
1
+ module ChunkyPNG
2
+
3
+ class Datastream
4
+
5
+ SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10].pack('C8')
6
+
7
+ attr_accessor :header_chunk
8
+ attr_accessor :other_chunks
9
+ attr_accessor :palette_chunk
10
+ attr_accessor :data_chunks
11
+ attr_accessor :end_chunk
12
+
13
+ def self.read(io)
14
+ verify_signature!(io)
15
+
16
+ ds = self.new
17
+ until io.eof?
18
+ chunk = ChunkyPNG::Chunk.read(io)
19
+ case chunk
20
+ when ChunkyPNG::Chunk::Header then ds.header_chunk = chunk
21
+ when ChunkyPNG::Chunk::Palette then ds.palette_chunk = chunk
22
+ when ChunkyPNG::Chunk::ImageData then ds.data_chunks << chunk
23
+ when ChunkyPNG::Chunk::End then ds.end_chunk = chunk
24
+ else ds.other_chunks << chunk
25
+ end
26
+ end
27
+ return ds
28
+ end
29
+
30
+ def self.verify_signature!(io)
31
+ signature = io.read(ChunkyPNG::Datastream::SIGNATURE.length)
32
+ raise "PNG signature not found!" unless signature == ChunkyPNG::Datastream::SIGNATURE
33
+ end
34
+
35
+ def chunks
36
+ cs = [header_chunk]
37
+ cs += other_chunks
38
+ cs << palette_chunk if palette_chunk
39
+ cs += data_chunks
40
+ cs << end_chunk
41
+ return cs
42
+ end
43
+
44
+ def initialize
45
+ @other_chunks = []
46
+ @data_chunks = []
47
+ end
48
+
49
+ def write(io)
50
+ io << SIGNATURE
51
+ chunks.each { |c| c.write(io) }
52
+ end
53
+
54
+ def idat_chunks(data)
55
+ streamdata = Zlib::Deflate.deflate(data)
56
+ # TODO: Split long streamdata over multiple chunks
57
+ return [ ChunkyPNG::Chunk::ImageData.new('IDAT', streamdata) ]
58
+ end
59
+
60
+ def pixel_matrix=(pixel_matrix)
61
+ @pixel_matrix = pixel_matrix
62
+ end
63
+
64
+ def pixel_matrix
65
+ @pixel_matrix ||= begin
66
+ data = data_chunks.map(&:content).join('')
67
+ streamdata = Zlib::Inflate.inflate(data)
68
+ matrix = ChunkyPNG::PixelMatrix.load(header, streamdata)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,37 @@
1
+ module ChunkyPNG
2
+ class Image
3
+
4
+ attr_reader :pixels
5
+
6
+ def initialize(width, height, background_color = ChunkyPNG::Color::WHITE)
7
+ @pixels = ChunkyPNG::PixelMatrix.new(width, height, background_color)
8
+ end
9
+
10
+ def width
11
+ pixels.width
12
+ end
13
+
14
+ def height
15
+ pixels.height
16
+ end
17
+
18
+ def write(io)
19
+ datastream = ChunkyPNG::Datastream.new
20
+
21
+ palette = pixels.palette
22
+ if palette.indexable?
23
+ datastream.header_chunk = ChunkyPNG::Chunk::Header.new(:width => width, :height => height, :color => ChunkyPNG::Chunk::Header::COLOR_INDEXED)
24
+ datastream.palette_chunk = palette.to_plte_chunk
25
+ datastream.data_chunks = datastream.idat_chunks(pixels.to_indexed_pixelstream(palette))
26
+ datastream.end_chunk = ChunkyPNG::Chunk::End.new
27
+ else
28
+ raise 'd'
29
+ datastream.header_chunk = ChunkyPNG::Chunk::Header.new(:width => width, :height => height)
30
+ datastream.data_chunks = datastream.idat_chunks(pixels.to_rgb_pixelstream)
31
+ datastream.end_chunk = ChunkyPNG::Chunk::End.new
32
+ end
33
+
34
+ datastream.write(io)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,91 @@
1
+ module ChunkyPNG
2
+
3
+ ###########################################
4
+ # PALETTE CLASS
5
+ ###########################################
6
+
7
+ class Palette < Set
8
+
9
+ def self.from_pixel_matrix(pixel_matrix)
10
+ from_pixels(pixel_matrix.pixels)
11
+ end
12
+
13
+ def self.from_pixels(pixels)
14
+ from_colors(pixels.map(&:color))
15
+ end
16
+
17
+ def self.from_colors(colors)
18
+ palette = self.new
19
+ colors.each { |color| palette << color }
20
+ palette
21
+ end
22
+
23
+ def indexable?
24
+ size < 256
25
+ end
26
+
27
+ def index(color)
28
+ @color_map[color]
29
+ end
30
+
31
+ def to_plte_chunk
32
+ @color_map = {}
33
+ colors = []
34
+
35
+ each_with_index do |color, index|
36
+ @color_map[color] = index
37
+ colors += color.to_rgb_array
38
+ end
39
+
40
+ ChunkyPNG::Chunk::Generic.new('PLTE', colors.pack('C*'))
41
+ end
42
+ end
43
+
44
+ ###########################################
45
+ # COLOR CLASS
46
+ ###########################################
47
+
48
+ class Color
49
+
50
+ attr_accessor :r, :g, :b
51
+
52
+ def initialize(r, g, b)
53
+ @r, @g, @b = r, g, b
54
+ end
55
+
56
+ ### CONSTRUCTORS ###########################################
57
+
58
+ def self.rgb(r, g, b)
59
+ new(r, g, b)
60
+ end
61
+
62
+ ### COLOR CONSTANTS ###########################################
63
+
64
+ BLACK = rgb( 0, 0, 0)
65
+ WHITE = rgb(255, 255, 255)
66
+
67
+ ### CONVERSION ###########################################
68
+
69
+ def index(palette)
70
+ palette.index(self)
71
+ end
72
+
73
+ def to_rgb_array
74
+ [r, g, b]
75
+ end
76
+
77
+ def to_rgb
78
+ to_rgb_array.pack('CCC')
79
+ end
80
+
81
+ def inspect
82
+ '#%02x%02x%02x' % [r, g, b]
83
+ end
84
+
85
+ ### EQUALITY ###########################################
86
+
87
+ def ==(other)
88
+ other.kind_of?(self.class) && to_rgb_array == other.to_rgb_array
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,157 @@
1
+ module ChunkyPNG
2
+
3
+ class PixelMatrix
4
+
5
+ FILTER_NONE = 0
6
+ FILTER_SUB = 1
7
+ FILTER_UP = 2
8
+ FILTER_AVERAGE = 3
9
+ FILTER_PAETH = 4
10
+
11
+ attr_reader :width, :height, :pixels
12
+
13
+ def self.load(header, content)
14
+ matrix = self.new(header.width, header.height)
15
+ matrix.decode_pixelstream(content, header)
16
+ return matrix
17
+ end
18
+
19
+ def [](x, y)
20
+ @pixels[y * width + x]
21
+ end
22
+
23
+ def each_scanline(&block)
24
+ height.times do |i|
25
+ scanline = @pixels[width * i, width]
26
+ yield(scanline)
27
+ end
28
+ end
29
+
30
+ def []=(x, y, color)
31
+ @pixels[y * width + x] = Pixel.new(color)
32
+ end
33
+
34
+ def initialize(width, height, background_color = ChunkyPNG::Color::WHITE)
35
+ @width, @height = width, height
36
+ @pixels = Array.new(width * height, Pixel.new(background_color))
37
+ end
38
+
39
+ def decode_pixelstream(stream, header = nil)
40
+ verify_length!(stream.length)
41
+ @pixels = []
42
+
43
+ decoded_bytes = Array.new(header.width * 3, 0)
44
+ height.times do |line_no|
45
+ position = line_no * (width * 3 + 1)
46
+ line_length = header.width * 3
47
+ bytes = stream.unpack("@#{position}CC#{line_length}")
48
+ filter = bytes.shift
49
+ decoded_bytes = decode_scanline(filter, bytes, decoded_bytes, header)
50
+ decoded_colors = decode_colors(decoded_bytes, header)
51
+ @pixels += decoded_colors.map { |c| Pixel.new(c) }
52
+ end
53
+
54
+ raise "Invalid amount of pixels" if @pixels.size != width * height
55
+ end
56
+
57
+ def decode_colors(bytes, header)
58
+ (0...width).map { |i| ChunkyPNG::Color.rgb(bytes[i*3+0], bytes[i*3+1], bytes[i*3+2]) }
59
+ end
60
+
61
+ def decode_scanline(filter, bytes, previous_bytes, header = nil)
62
+ case filter
63
+ when FILTER_NONE then decode_scanline_none( bytes, previous_bytes, header)
64
+ when FILTER_SUB then decode_scanline_sub( bytes, previous_bytes, header)
65
+ when FILTER_UP then decode_scanline_up( bytes, previous_bytes, header)
66
+ when FILTER_AVERAGE then raise "Average filter are not yet supported!"
67
+ when FILTER_PAETH then raise "Paeth filter are not yet supported!"
68
+ else raise "Unknown filter type"
69
+ end
70
+ end
71
+
72
+ def decode_scanline_none(bytes, previous_bytes, header = nil)
73
+ bytes
74
+ end
75
+
76
+ def decode_scanline_sub(bytes, previous_bytes, header = nil)
77
+ bytes.each_with_index { |b, i| bytes[i] = (b + (i >= 3 ? bytes[i-3] : 0)) % 256 }
78
+ bytes
79
+ end
80
+
81
+ def decode_scanline_up(bytes, previous_bytes, header = nil)
82
+ bytes.each_with_index { |b, i| bytes[i] = (b + previous_bytes[i]) % 256 }
83
+ bytes
84
+ end
85
+
86
+ def verify_length!(bytes_count)
87
+ raise "Invalid stream length!" unless bytes_count == width * height * 3 + height
88
+ end
89
+
90
+ def encode_scanline(filter, bytes, previous_bytes, header = nil)
91
+ case filter
92
+ when FILTER_NONE then encode_scanline_none( bytes, previous_bytes, header)
93
+ when FILTER_SUB then encode_scanline_sub( bytes, previous_bytes, header)
94
+ when FILTER_UP then encode_scanline_up( bytes, previous_bytes, header)
95
+ when FILTER_AVERAGE then raise "Average filter are not yet supported!"
96
+ when FILTER_PAETH then raise "Paeth filter are not yet supported!"
97
+ else raise "Unknown filter type"
98
+ end
99
+ end
100
+
101
+ def encode_scanline_none(bytes, previous_bytes, header = nil)
102
+ [FILTER_NONE] + bytes
103
+ end
104
+
105
+ def encode_scanline_sub(bytes, previous_bytes, header = nil)
106
+ encoded = (3...bytes.length).map { |n| (bytes[n-3] - bytes[n]) % 256 }
107
+ [FILTER_SUB] + bytes[0...3] + encoded
108
+ end
109
+
110
+ def encode_scanline_up(bytes, previous_bytes, header = nil)
111
+ encoded = (0...bytes.length).map { |n| previous_bytes[n] - bytes[n] % 256 }
112
+ [FILTER_UP] + encoded
113
+ end
114
+
115
+ def palette
116
+ ChunkyPNG::Palette.from_pixels(@pixels)
117
+ end
118
+
119
+ def indexable?
120
+ palette.indexable?
121
+ end
122
+
123
+ def to_indexed_pixelstream(palette)
124
+ stream = ""
125
+ each_scanline do |line|
126
+ bytes = line.map { |p| p.color.index(palette) }
127
+ stream << encode_scanline(FILTER_NONE, bytes, nil, nil).pack('C*')
128
+ end
129
+ return stream
130
+ end
131
+
132
+ def to_rgb_pixelstream
133
+ stream = ""
134
+ each_scanline do |line|
135
+ bytes = line.map { |p| p.color.to_rgb_array }.flatten
136
+ stream << encode_scanline(FILTER_NONE, bytes, nil, nil).pack('C*')
137
+ end
138
+ return stream
139
+ end
140
+ end
141
+
142
+ class Pixel
143
+ attr_accessor :color, :alpha
144
+
145
+ def initialize(color, alpha = 255)
146
+ @color, @alpha = color, alpha
147
+ end
148
+
149
+ def opaque?
150
+ alpha == 255
151
+ end
152
+
153
+ def make_opaque!
154
+ alpha = 255
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,22 @@
1
+ require File.expand_path('../spec_helper.rb', File.dirname(__FILE__))
2
+
3
+ describe ChunkyPNG::Image do
4
+
5
+ it "should write a valid PNG image using an indexed palette" do
6
+ image = ChunkyPNG::Image.new(10, 20, ChunkyPNG::Color.rgb(10, 100, 255))
7
+ filename = resource_file('testing.png')
8
+ File.open(filename, 'w') { |f| image.write(f) }
9
+
10
+ png = ChunkyPNG.load(filename)
11
+ png.header_chunk.width.should == 10
12
+ png.header_chunk.height.should == 20
13
+ png.header_chunk.color.should == ChunkyPNG::Chunk::Header::COLOR_INDEXED
14
+
15
+ png.palette_chunk.should_not be_nil
16
+ png.data_chunks.should_not be_empty
17
+ # `open #{filename}`
18
+
19
+ File.unlink(filename)
20
+ end
21
+ end
22
+
Binary file
@@ -0,0 +1,17 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+
3
+ require 'rubygems'
4
+ require 'spec'
5
+
6
+ require 'chunky_png'
7
+
8
+ module ResourceFileHelper
9
+ def resource_file(name)
10
+ File.expand_path("./resources/#{name}", File.dirname(__FILE__))
11
+ end
12
+ end
13
+
14
+
15
+ Spec::Runner.configure do |config|
16
+ config.include ResourceFileHelper
17
+ end
@@ -0,0 +1,71 @@
1
+ require File.expand_path('../spec_helper.rb', File.dirname(__FILE__))
2
+
3
+ describe ChunkyPNG::PixelMatrix do
4
+
5
+ describe '#decode_scanline' do
6
+ before(:each) do
7
+ @matrix = ChunkyPNG::PixelMatrix.new(3, 3)
8
+ end
9
+
10
+ it "should decode a line without filtering as is" do
11
+ bytes = Array.new(@matrix.width * 3, ChunkyPNG::Color.rgb(10, 20, 30))
12
+ @matrix.decode_scanline(ChunkyPNG::PixelMatrix::FILTER_NONE, bytes, nil).should == bytes
13
+ end
14
+
15
+ it "should decode a line with sub filtering correctly" do
16
+ # all white pixels
17
+ bytes = [255, 255, 255, 0, 0, 0, 0, 0, 0]
18
+ decoded_bytes = @matrix.decode_scanline(ChunkyPNG::PixelMatrix::FILTER_SUB, bytes, nil)
19
+ decoded_bytes.should == [255, 255, 255, 255, 255, 255, 255, 255, 255]
20
+
21
+ # all black pixels
22
+ bytes = [0, 0, 0, 0, 0, 0, 0, 0, 0]
23
+ decoded_bytes = @matrix.decode_scanline(ChunkyPNG::PixelMatrix::FILTER_SUB, bytes, nil)
24
+ decoded_bytes.should == [0, 0, 0, 0, 0, 0, 0, 0, 0]
25
+
26
+ # various colors
27
+ bytes = [255, 0, 45, 0, 255, 0, 112, 200, 178]
28
+ decoded_bytes = @matrix.decode_scanline(ChunkyPNG::PixelMatrix::FILTER_SUB, bytes, nil)
29
+ decoded_bytes.should == [255, 0, 45, 255, 255, 45, 111, 199, 223]
30
+ end
31
+
32
+ it "should decode a line with up filtering correctly" do
33
+ # previous line is all black
34
+ previous_bytes = [0, 0, 0, 0, 0, 0, 0, 0, 0]
35
+ bytes = [255, 255, 255, 127, 127, 127, 0, 0, 0]
36
+ decoded_bytes = @matrix.decode_scanline(ChunkyPNG::PixelMatrix::FILTER_UP, bytes, previous_bytes)
37
+ decoded_bytes.should == [255, 255, 255, 127, 127, 127, 0, 0, 0]
38
+
39
+ # previous line has various pixels
40
+ previous_bytes = [255, 255, 255, 127, 127, 127, 0, 0, 0]
41
+ bytes = [0, 127, 255, 0, 127, 255, 0, 127, 255]
42
+ decoded_bytes = @matrix.decode_scanline(ChunkyPNG::PixelMatrix::FILTER_UP, bytes, previous_bytes)
43
+ decoded_bytes.should == [255, 126, 254, 127, 254, 126, 0, 127, 255]
44
+ end
45
+ end
46
+
47
+ describe '#encode_scanline' do
48
+ before(:each) do
49
+ @matrix = ChunkyPNG::PixelMatrix.new(3, 3)
50
+ end
51
+
52
+ it "should encode a scanline without filtering correctly" do
53
+ bytes = [0, 0, 0, 1, 1, 1, 2, 2, 2]
54
+ encoded_bytes = @matrix.encode_scanline(ChunkyPNG::PixelMatrix::FILTER_NONE, bytes, nil, nil)
55
+ encoded_bytes.should == [0, 0, 0, 0, 1, 1, 1, 2, 2, 2]
56
+ end
57
+
58
+ it "should encode a scanline with sub filtering correctly" do
59
+ bytes = [255, 255, 255, 255, 255, 255, 255, 255, 255]
60
+ encoded_bytes = @matrix.encode_scanline(ChunkyPNG::PixelMatrix::FILTER_SUB, bytes, nil, nil)
61
+ encoded_bytes.should == [1, 255, 255, 255, 0, 0, 0, 0, 0, 0]
62
+ end
63
+
64
+ it "should encode a scanline with up filtering correctly" do
65
+ bytes = [255, 255, 255, 255, 255, 255, 255, 255, 255]
66
+ previous_bytes = [255, 255, 255, 255, 255, 255, 255, 255, 255]
67
+ encoded_bytes = @matrix.encode_scanline(ChunkyPNG::PixelMatrix::FILTER_UP, bytes, previous_bytes, nil)
68
+ encoded_bytes.should == [2, 0, 0, 0, 0, 0, 0, 0, 0, 0]
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,323 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/tasklib'
4
+ require 'date'
5
+ require 'git'
6
+
7
+ module GithubGem
8
+
9
+ # Detects the gemspc file of this project using heuristics.
10
+ def self.detect_gemspec_file
11
+ FileList['*.gemspec'].first
12
+ end
13
+
14
+ # Detects the main include file of this project using heuristics
15
+ def self.detect_main_include
16
+ if detect_gemspec_file =~ /^(\.*)\.gemspec$/ && File.exist?("lib/#{$1}.rb")
17
+ "lib/#{$1}.rb"
18
+ elsif FileList['lib/*.rb'].length == 1
19
+ FileList['lib/*.rb'].first
20
+ else
21
+ nil
22
+ end
23
+ end
24
+
25
+ class RakeTasks
26
+
27
+ attr_reader :gemspec, :modified_files, :git
28
+ attr_accessor :gemspec_file, :task_namespace, :main_include, :root_dir, :spec_pattern, :test_pattern, :remote, :remote_branch, :local_branch
29
+
30
+ # Initializes the settings, yields itself for configuration
31
+ # and defines the rake tasks based on the gemspec file.
32
+ def initialize(task_namespace = :gem)
33
+ @gemspec_file = GithubGem.detect_gemspec_file
34
+ @task_namespace = task_namespace
35
+ @main_include = GithubGem.detect_main_include
36
+ @modified_files = []
37
+ @root_dir = Dir.pwd
38
+ @test_pattern = 'test/**/*_test.rb'
39
+ @spec_pattern = 'spec/**/*_spec.rb'
40
+ @local_branch = 'master'
41
+ @remote = 'origin'
42
+ @remote_branch = 'master'
43
+
44
+ yield(self) if block_given?
45
+
46
+ @git = Git.open(@root_dir)
47
+ load_gemspec!
48
+ define_tasks!
49
+ end
50
+
51
+ protected
52
+
53
+ # Define Unit test tasks
54
+ def define_test_tasks!
55
+ require 'rake/testtask'
56
+
57
+ namespace(:test) do
58
+ Rake::TestTask.new(:basic) do |t|
59
+ t.pattern = test_pattern
60
+ t.verbose = true
61
+ t.libs << 'test'
62
+ end
63
+ end
64
+
65
+ desc "Run all unit tests for #{gemspec.name}"
66
+ task(:test => ['test:basic'])
67
+ end
68
+
69
+ # Defines RSpec tasks
70
+ def define_rspec_tasks!
71
+ require 'spec/rake/spectask'
72
+
73
+ namespace(:spec) do
74
+ desc "Verify all RSpec examples for #{gemspec.name}"
75
+ Spec::Rake::SpecTask.new(:basic) do |t|
76
+ t.spec_files = FileList[spec_pattern]
77
+ end
78
+
79
+ desc "Verify all RSpec examples for #{gemspec.name} and output specdoc"
80
+ Spec::Rake::SpecTask.new(:specdoc) do |t|
81
+ t.spec_files = FileList[spec_pattern]
82
+ t.spec_opts << '--format' << 'specdoc' << '--color'
83
+ end
84
+
85
+ desc "Run RCov on specs for #{gemspec.name}"
86
+ Spec::Rake::SpecTask.new(:rcov) do |t|
87
+ t.spec_files = FileList[spec_pattern]
88
+ t.rcov = true
89
+ t.rcov_opts = ['--exclude', '"spec/*,gems/*"', '--rails']
90
+ end
91
+ end
92
+
93
+ desc "Verify all RSpec examples for #{gemspec.name} and output specdoc"
94
+ task(:spec => ['spec:specdoc'])
95
+ end
96
+
97
+ # Defines the rake tasks
98
+ def define_tasks!
99
+
100
+ define_test_tasks! if has_tests?
101
+ define_rspec_tasks! if has_specs?
102
+
103
+ namespace(@task_namespace) do
104
+ desc "Updates the filelist in the gemspec file"
105
+ task(:manifest) { manifest_task }
106
+
107
+ desc "Builds the .gem package"
108
+ task(:build => :manifest) { build_task }
109
+
110
+ desc "Sets the version of the gem in the gemspec"
111
+ task(:set_version => [:check_version, :check_current_branch]) { version_task }
112
+ task(:check_version => :fetch_origin) { check_version_task }
113
+
114
+ task(:fetch_origin) { fetch_origin_task }
115
+ task(:check_current_branch) { check_current_branch_task }
116
+ task(:check_clean_status) { check_clean_status_task }
117
+ task(:check_not_diverged => :fetch_origin) { check_not_diverged_task }
118
+
119
+ checks = [:check_current_branch, :check_clean_status, :check_not_diverged, :check_version]
120
+ checks.unshift('spec:basic') if has_specs?
121
+ checks.unshift('test:basic') if has_tests?
122
+ checks.push << [:check_rubyforge] if gemspec.rubyforge_project
123
+
124
+ desc "Perform all checks that would occur before a release"
125
+ task(:release_checks => checks)
126
+
127
+ release_tasks = [:release_checks, :set_version, :build, :github_release]
128
+ release_tasks << [:rubyforge_release] if gemspec.rubyforge_project
129
+
130
+ desc "Release a new verison of the gem"
131
+ task(:release => release_tasks) { release_task }
132
+
133
+ task(:check_rubyforge) { check_rubyforge_task }
134
+ task(:rubyforge_release) { rubyforge_release_task }
135
+ task(:github_release => [:commit_modified_files, :tag_version]) { github_release_task }
136
+ task(:tag_version) { tag_version_task }
137
+ task(:commit_modified_files) { commit_modified_files_task }
138
+
139
+ desc "Updates the gem release tasks with the latest version on Github"
140
+ task(:update_tasks) { update_tasks_task }
141
+ end
142
+ end
143
+
144
+ # Updates the files list and test_files list in the gemspec file using the list of files
145
+ # in the repository and the spec/test file pattern.
146
+ def manifest_task
147
+ # Load all the gem's files using "git ls-files"
148
+ repository_files = git.ls_files.keys
149
+ test_files = Dir[test_pattern] + Dir[spec_pattern]
150
+
151
+ update_gemspec(:files, repository_files)
152
+ update_gemspec(:test_files, repository_files & test_files)
153
+ end
154
+
155
+ # Builds the gem
156
+ def build_task
157
+ sh "gem build -q #{gemspec_file}"
158
+ Dir.mkdir('pkg') unless File.exist?('pkg')
159
+ sh "mv #{gemspec.name}-#{gemspec.version}.gem pkg/#{gemspec.name}-#{gemspec.version}.gem"
160
+ end
161
+
162
+ # Updates the version number in the gemspec file, the VERSION constant in the main
163
+ # include file and the contents of the VERSION file.
164
+ def version_task
165
+ update_gemspec(:version, ENV['VERSION']) if ENV['VERSION']
166
+ update_gemspec(:date, Date.today)
167
+
168
+ update_version_file(gemspec.version)
169
+ update_version_constant(gemspec.version)
170
+ end
171
+
172
+ def check_version_task
173
+ raise "#{ENV['VERSION']} is not a valid version number!" if ENV['VERSION'] && !Gem::Version.correct?(ENV['VERSION'])
174
+ proposed_version = Gem::Version.new(ENV['VERSION'] || gemspec.version)
175
+ # Loads the latest version number using the created tags
176
+ newest_version = git.tags.map { |tag| tag.name.split('-').last }.compact.map { |v| Gem::Version.new(v) }.max
177
+ raise "This version (#{proposed_version}) is not higher than the highest tagged version (#{newest_version})" if newest_version && newest_version >= proposed_version
178
+ end
179
+
180
+ # Checks whether the current branch is not diverged from the remote branch
181
+ def check_not_diverged_task
182
+ raise "The current branch is diverged from the remote branch!" if git.log.between('HEAD', git.remote(remote).branch(remote_branch).gcommit).any?
183
+ end
184
+
185
+ # Checks whether the repository status ic clean
186
+ def check_clean_status_task
187
+ raise "The current working copy contains modifications" if git.status.changed.any?
188
+ end
189
+
190
+ # Checks whether the current branch is correct
191
+ def check_current_branch_task
192
+ raise "Currently not on #{local_branch} branch!" unless git.branch.name == local_branch.to_s
193
+ end
194
+
195
+ # Fetches the latest updates from Github
196
+ def fetch_origin_task
197
+ git.fetch('origin')
198
+ end
199
+
200
+ # Commits every file that has been changed by the release task.
201
+ def commit_modified_files_task
202
+ if modified_files.any?
203
+ modified_files.each { |file| git.add(file) }
204
+ git.commit("Released #{gemspec.name} gem version #{gemspec.version}")
205
+ end
206
+ end
207
+
208
+ # Adds a tag for the released version
209
+ def tag_version_task
210
+ git.add_tag("#{gemspec.name}-#{gemspec.version}")
211
+ end
212
+
213
+ # Pushes the changes and tag to github
214
+ def github_release_task
215
+ git.push(remote, remote_branch, true)
216
+ end
217
+
218
+ # Checks whether Rubyforge is configured properly
219
+ def check_rubyforge_task
220
+ # Login no longer necessary when using rubyforge 2.0.0 gem
221
+ # raise "Could not login on rubyforge!" unless `rubyforge login 2>&1`.strip.empty?
222
+ output = `rubyforge names`.split("\n")
223
+ raise "Rubyforge group not found!" unless output.any? { |line| %r[^groups\s*\:.*\b#{Regexp.quote(gemspec.rubyforge_project)}\b.*] =~ line }
224
+ raise "Rubyforge package not found!" unless output.any? { |line| %r[^packages\s*\:.*\b#{Regexp.quote(gemspec.name)}\b.*] =~ line }
225
+ end
226
+
227
+ # Task to release the .gem file toRubyforge.
228
+ def rubyforge_release_task
229
+ sh 'rubyforge', 'add_release', gemspec.rubyforge_project, gemspec.name, gemspec.version.to_s, "pkg/#{gemspec.name}-#{gemspec.version}.gem"
230
+ end
231
+
232
+ # Gem release task.
233
+ # All work is done by the task's dependencies, so just display a release completed message.
234
+ def release_task
235
+ puts
236
+ puts '------------------------------------------------------------'
237
+ puts "Released #{gemspec.name} version #{gemspec.version}"
238
+ end
239
+
240
+ private
241
+
242
+ # Checks whether this project has any RSpec files
243
+ def has_specs?
244
+ FileList[spec_pattern].any?
245
+ end
246
+
247
+ # Checks whether this project has any unit test files
248
+ def has_tests?
249
+ FileList[test_pattern].any?
250
+ end
251
+
252
+ # Loads the gemspec file
253
+ def load_gemspec!
254
+ @gemspec = eval(File.read(@gemspec_file))
255
+ end
256
+
257
+ # Updates the VERSION file with the new version
258
+ def update_version_file(version)
259
+ if File.exists?('VERSION')
260
+ File.open('VERSION', 'w') { |f| f << version.to_s }
261
+ modified_files << 'VERSION'
262
+ end
263
+ end
264
+
265
+ # Updates the VERSION constant in the main include file if it exists
266
+ def update_version_constant(version)
267
+ if main_include && File.exist?(main_include)
268
+ file_contents = File.read(main_include)
269
+ if file_contents.sub!(/^(\s+VERSION\s*=\s*)[^\s].*$/) { $1 + version.to_s.inspect }
270
+ File.open(main_include, 'w') { |f| f << file_contents }
271
+ modified_files << main_include
272
+ end
273
+ end
274
+ end
275
+
276
+ # Updates an attribute of the gemspec file.
277
+ # This function will open the file, and search/replace the attribute using a regular expression.
278
+ def update_gemspec(attribute, new_value, literal = false)
279
+
280
+ unless literal
281
+ new_value = case new_value
282
+ when Array then "%w(#{new_value.join(' ')})"
283
+ when Hash, String then new_value.inspect
284
+ when Date then new_value.strftime('%Y-%m-%d').inspect
285
+ else raise "Cannot write value #{new_value.inspect} to gemspec file!"
286
+ end
287
+ end
288
+
289
+ spec = File.read(gemspec_file)
290
+ regexp = Regexp.new('^(\s+\w+\.' + Regexp.quote(attribute.to_s) + '\s*=\s*)[^\s].*$')
291
+ if spec.sub!(regexp) { $1 + new_value }
292
+ File.open(gemspec_file, 'w') { |f| f << spec }
293
+ modified_files << gemspec_file
294
+
295
+ # Reload the gemspec so the changes are incorporated
296
+ load_gemspec!
297
+ end
298
+ end
299
+
300
+ # Updates the tasks file using the latest file found on Github
301
+ def update_tasks_task
302
+ require 'net/http'
303
+
304
+ server = 'github.com'
305
+ path = '/wvanbergen/github-gem/raw/master/tasks/github-gem.rake'
306
+
307
+ Net::HTTP.start(server) do |http|
308
+ response = http.get(path)
309
+ open(__FILE__, "w") { |file| file.write(response.body) }
310
+ end
311
+
312
+ relative_file = File.expand_path(__FILE__).sub(%r[^#{git.dir.path}/], '')
313
+ if git.status[relative_file] && git.status[relative_file].type == 'M'
314
+ git.add(relative_file)
315
+ git.commit("Updated to latest gem release management tasks.")
316
+ puts "Updated to latest version of gem release management tasks."
317
+ else
318
+ puts "Release managament tasks already are at the latest version."
319
+ end
320
+ end
321
+
322
+ end
323
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chunky_png
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Willem van Bergen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-01-10 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rspec
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.2.9
24
+ version:
25
+ description: Pure ruby library for read/write, chunk-level access to PNG files
26
+ email:
27
+ - willem@railsdoctors.com
28
+ executables: []
29
+
30
+ extensions: []
31
+
32
+ extra_rdoc_files:
33
+ - README.rdoc
34
+ files:
35
+ - spec/spec_helper.rb
36
+ - .gitignore
37
+ - lib/chunky_png/pixel_matrix.rb
38
+ - LICENSE
39
+ - lib/chunky_png/chunk.rb
40
+ - Rakefile
41
+ - README.rdoc
42
+ - spec/resources/indexed_10x10.png
43
+ - spec/integration/image_spec.rb
44
+ - lib/chunky_png/palette.rb
45
+ - lib/chunky_png/datastream.rb
46
+ - chunky_png.gemspec
47
+ - tasks/github-gem.rake
48
+ - lib/chunky_png/image.rb
49
+ - spec/unit/pixel_matrix_spec.rb
50
+ - lib/chunky_png.rb
51
+ has_rdoc: true
52
+ homepage: http://wiki.github.com/wvanbergen/chunky_png
53
+ licenses: []
54
+
55
+ post_install_message:
56
+ rdoc_options:
57
+ - --title
58
+ - chunky_png
59
+ - --main
60
+ - README.rdoc
61
+ - --line-numbers
62
+ - --inline-source
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ version:
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: "0"
76
+ version:
77
+ requirements: []
78
+
79
+ rubyforge_project:
80
+ rubygems_version: 1.3.5
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: Pure ruby library for read/write, chunk-level access to PNG files
84
+ test_files:
85
+ - spec/integration/image_spec.rb
86
+ - spec/unit/pixel_matrix_spec.rb