chunky_png 0.0.1

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