chunky_png 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,2 +1,4 @@
1
1
  .DS_Store
2
2
  spec/resources/testing.png
3
+ /pkg
4
+ /tmp
@@ -3,8 +3,8 @@ Gem::Specification.new do |s|
3
3
 
4
4
  # Do not change the version and date fields by hand. This will be done
5
5
  # automatically by the gem release script.
6
- s.version = "0.0.1"
7
- s.date = "2010-01-10"
6
+ s.version = "0.0.2"
7
+ s.date = "2010-01-12"
8
8
 
9
9
  s.summary = "Pure ruby library for read/write, chunk-level access to PNG files"
10
10
  s.description = "Pure ruby library for read/write, chunk-level access to PNG files"
@@ -20,6 +20,6 @@ Gem::Specification.new do |s|
20
20
 
21
21
  # Do not change the files and test_files fields by hand. This will be done
22
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)
23
+ s.files = %w(spec/spec_helper.rb spec/resources/gray_10x10_grayscale.png spec/resources/gray_10x10.png .gitignore spec/resources/gray_10x10_truecolor_alpha.png lib/chunky_png/pixel_matrix.rb LICENSE spec/resources/gray_10x10_truecolor.png lib/chunky_png/pixel_matrix/decoding.rb lib/chunky_png/chunk.rb spec/unit/encoding_spec.rb Rakefile README.rdoc spec/resources/gray_10x10_indexed.png spec/integration/image_spec.rb lib/chunky_png/pixel.rb lib/chunky_png/palette.rb lib/chunky_png/datastream.rb chunky_png.gemspec tasks/github-gem.rake spec/unit/decoding_spec.rb spec/resources/gray_10x10_grayscale_alpha.png lib/chunky_png/pixel_matrix/encoding.rb lib/chunky_png/image.rb spec/unit/pixel_spec.rb spec/unit/pixel_matrix_spec.rb lib/chunky_png.rb)
24
+ s.test_files = %w(spec/unit/encoding_spec.rb spec/integration/image_spec.rb spec/unit/decoding_spec.rb spec/unit/pixel_spec.rb spec/unit/pixel_matrix_spec.rb)
25
25
  end
@@ -4,6 +4,9 @@ require 'zlib'
4
4
  require 'chunky_png/datastream'
5
5
  require 'chunky_png/chunk'
6
6
  require 'chunky_png/palette'
7
+ require 'chunky_png/pixel'
8
+ require 'chunky_png/pixel_matrix/encoding'
9
+ require 'chunky_png/pixel_matrix/decoding'
7
10
  require 'chunky_png/pixel_matrix'
8
11
  require 'chunky_png/image'
9
12
 
@@ -90,6 +90,9 @@ module ChunkyPNG
90
90
 
91
91
  class Palette < Generic
92
92
  end
93
+
94
+ class Transparency < Generic
95
+ end
93
96
 
94
97
  class ImageData < Generic
95
98
  end
@@ -97,7 +100,7 @@ module ChunkyPNG
97
100
  # Maps chunk types to classes.
98
101
  # If a chunk type is not given in this hash, a generic chunk type will be used.
99
102
  CHUNK_TYPES = {
100
- 'IHDR' => Header, 'IEND' => End, 'IDAT' => ImageData, 'PLTE' => Palette
103
+ 'IHDR' => Header, 'IEND' => End, 'IDAT' => ImageData, 'PLTE' => Palette, 'tRNS' => Transparency
101
104
  }
102
105
 
103
106
  end
@@ -7,6 +7,7 @@ module ChunkyPNG
7
7
  attr_accessor :header_chunk
8
8
  attr_accessor :other_chunks
9
9
  attr_accessor :palette_chunk
10
+ attr_accessor :transparency_chunk
10
11
  attr_accessor :data_chunks
11
12
  attr_accessor :end_chunk
12
13
 
@@ -17,10 +18,11 @@ module ChunkyPNG
17
18
  until io.eof?
18
19
  chunk = ChunkyPNG::Chunk.read(io)
19
20
  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
21
+ when ChunkyPNG::Chunk::Header then ds.header_chunk = chunk
22
+ when ChunkyPNG::Chunk::Palette then ds.palette_chunk = chunk
23
+ when ChunkyPNG::Chunk::Transparency then ds.transparency_chunk = chunk
24
+ when ChunkyPNG::Chunk::ImageData then ds.data_chunks << chunk
25
+ when ChunkyPNG::Chunk::End then ds.end_chunk = chunk
24
26
  else ds.other_chunks << chunk
25
27
  end
26
28
  end
@@ -35,7 +37,8 @@ module ChunkyPNG
35
37
  def chunks
36
38
  cs = [header_chunk]
37
39
  cs += other_chunks
38
- cs << palette_chunk if palette_chunk
40
+ cs << palette_chunk if palette_chunk
41
+ cs << transparency_chunk if transparency_chunk
39
42
  cs += data_chunks
40
43
  cs << end_chunk
41
44
  return cs
@@ -62,11 +65,7 @@ module ChunkyPNG
62
65
  end
63
66
 
64
67
  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
68
+ @pixel_matrix ||= ChunkyPNG::PixelMatrix.decode(self)
70
69
  end
71
70
  end
72
71
  end
@@ -3,7 +3,7 @@ module ChunkyPNG
3
3
 
4
4
  attr_reader :pixels
5
5
 
6
- def initialize(width, height, background_color = ChunkyPNG::Color::WHITE)
6
+ def initialize(width, height, background_color = ChunkyPNG::Pixel::TRANSPARENT)
7
7
  @pixels = ChunkyPNG::PixelMatrix.new(width, height, background_color)
8
8
  end
9
9
 
@@ -15,22 +15,8 @@ module ChunkyPNG
15
15
  pixels.height
16
16
  end
17
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
-
18
+ def write(io, constraints = {})
19
+ datastream = pixels.to_datastream(constraints)
34
20
  datastream.write(io)
35
21
  end
36
22
  end
@@ -4,88 +4,92 @@ module ChunkyPNG
4
4
  # PALETTE CLASS
5
5
  ###########################################
6
6
 
7
- class Palette < Set
7
+ class Palette < SortedSet
8
8
 
9
- def self.from_pixel_matrix(pixel_matrix)
10
- from_pixels(pixel_matrix.pixels)
9
+ def initialize(enum)
10
+ super(enum)
11
+ @decoding_map = enum if enum.kind_of?(Array)
11
12
  end
12
13
 
13
- def self.from_pixels(pixels)
14
- from_colors(pixels.map(&:color))
14
+ def self.from_chunks(palette_chunk, transparency_chunk = nil)
15
+ return nil if palette_chunk.nil?
16
+
17
+ decoding_map = []
18
+ index = 0
19
+
20
+ palatte_bytes = palette_chunk.content.unpack('C*')
21
+ if transparency_chunk
22
+ alpha_channel = transparency_chunk.content.unpack('C*')
23
+ else
24
+ alpha_channel = Array.new(palatte_bytes.size / 3, 255)
25
+ end
26
+
27
+ index = 0
28
+ palatte_bytes.each_slice(3) do |bytes|
29
+ bytes << alpha_channel[index]
30
+ decoding_map << ChunkyPNG::Pixel.rgba(*bytes)
31
+ index += 1
32
+ end
33
+
34
+ self.new(decoding_map)
35
+ end
36
+
37
+ def self.from_pixel_matrix(pixel_matrix)
38
+ self.new(pixel_matrix.pixels)
15
39
  end
16
40
 
17
- def self.from_colors(colors)
18
- palette = self.new
19
- colors.each { |color| palette << color }
20
- palette
41
+ def self.from_pixels(pixels)
42
+ self.new(pixels)
21
43
  end
22
44
 
23
45
  def indexable?
24
46
  size < 256
25
47
  end
26
48
 
49
+ def opaque?
50
+ all? { |pixel| pixel.opaque? }
51
+ end
52
+
53
+ def can_decode?
54
+ !@decoding_map.nil?
55
+ end
56
+
57
+ def can_encode?
58
+ !@encoding_map.nil?
59
+ end
60
+
61
+ def [](index)
62
+ @decoding_map[index]
63
+ end
64
+
27
65
  def index(color)
28
- @color_map[color]
66
+ @encoding_map[color]
67
+ end
68
+
69
+ def to_trns_chunk
70
+ ChunkyPNG::Chunk::Transparency.new('tRNS', map(&:a).pack('C*'))
29
71
  end
30
72
 
31
73
  def to_plte_chunk
32
- @color_map = {}
74
+ @encoding_map = {}
33
75
  colors = []
34
76
 
35
77
  each_with_index do |color, index|
36
- @color_map[color] = index
37
- colors += color.to_rgb_array
78
+ @encoding_map[color] = index
79
+ colors += color.to_truecolor_bytes
38
80
  end
39
81
 
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]
82
+ ChunkyPNG::Chunk::Palette.new('PLTE', colors.pack('C*'))
83
83
  end
84
84
 
85
- ### EQUALITY ###########################################
86
-
87
- def ==(other)
88
- other.kind_of?(self.class) && to_rgb_array == other.to_rgb_array
85
+ def best_colormode
86
+ if indexable?
87
+ ChunkyPNG::Chunk::Header::COLOR_INDEXED
88
+ elsif opaque?
89
+ ChunkyPNG::Chunk::Header::COLOR_TRUECOLOR
90
+ else
91
+ ChunkyPNG::Chunk::Header::COLOR_TRUECOLOR_ALPHA
92
+ end
89
93
  end
90
94
  end
91
95
  end
@@ -0,0 +1,101 @@
1
+ module ChunkyPNG
2
+
3
+ class Pixel
4
+
5
+ attr_reader :value
6
+
7
+ def initialize(value)
8
+ @value = value
9
+ end
10
+
11
+ def self.rgb(r, g, b)
12
+ rgba(r, g, b, 255)
13
+ end
14
+
15
+ def self.rgba(r, g, b, a)
16
+ new(r << 24 | g << 16 | b << 8 | a)
17
+ end
18
+
19
+ def self.grayscale(teint, a = 255)
20
+ rgba(teint, teint, teint, a)
21
+ end
22
+
23
+ def self.from_rgb_stream(stream)
24
+ self.rgb(*stream.unpack('C3'))
25
+ end
26
+
27
+ def self.from_rgba_stream(stream)
28
+ self.rgba(*stream.unpack('C4'))
29
+ end
30
+
31
+ def r
32
+ (@value & 0xff000000) >> 24
33
+ end
34
+
35
+ def g
36
+ (@value & 0x00ff0000) >> 16
37
+ end
38
+
39
+ def b
40
+ (@value & 0x0000ff00) >> 8
41
+ end
42
+
43
+ def a
44
+ @value & 0x000000ff
45
+ end
46
+
47
+ def opaque?
48
+ a == 0x000000ff
49
+ end
50
+
51
+ def inspect
52
+ '#%08x' % @value
53
+ end
54
+
55
+ def eql?(other)
56
+ other.kind_of?(self.class) && other.value == self.value
57
+ end
58
+
59
+ alias :== :eql?
60
+
61
+ def to_truecolor_alpha_bytes
62
+ [r,g,b,a]
63
+ end
64
+
65
+ def to_truecolor_bytes
66
+ [r,g,b]
67
+ end
68
+
69
+ def index(palette)
70
+ palette.index(self)
71
+ end
72
+
73
+ def to_indexed_bytes(palette)
74
+ [index(palette)]
75
+ end
76
+
77
+ def to_grayscale_bytes
78
+ [r] # Assumption: r == g == b
79
+ end
80
+
81
+ def to_grayscale_alpha_bytes
82
+ [r, a] # Assumption: r == g == b
83
+ end
84
+
85
+ BLACK = rgb( 0, 0, 0)
86
+ WHITE = rgb(255, 255, 255)
87
+
88
+ TRANSPARENT = rgba(0, 0, 0, 0)
89
+
90
+ def self.bytesize(color_mode)
91
+ case color_mode
92
+ when ChunkyPNG::Chunk::Header::COLOR_INDEXED then 1
93
+ when ChunkyPNG::Chunk::Header::COLOR_TRUECOLOR then 3
94
+ when ChunkyPNG::Chunk::Header::COLOR_TRUECOLOR_ALPHA then 4
95
+ when ChunkyPNG::Chunk::Header::COLOR_GRAYSCALE then 1
96
+ when ChunkyPNG::Chunk::Header::COLOR_GRAYSCALE_ALPHA then 2
97
+ else raise "Don't know the bytesize of pixels in this colormode: #{color_mode}!"
98
+ end
99
+ end
100
+ end
101
+ end
@@ -2,6 +2,9 @@ module ChunkyPNG
2
2
 
3
3
  class PixelMatrix
4
4
 
5
+ include Encoding
6
+ extend Decoding
7
+
5
8
  FILTER_NONE = 0
6
9
  FILTER_SUB = 1
7
10
  FILTER_UP = 2
@@ -9,11 +12,23 @@ module ChunkyPNG
9
12
  FILTER_PAETH = 4
10
13
 
11
14
  attr_reader :width, :height, :pixels
15
+
16
+
17
+ def initialize(width, height, initial = ChunkyPNG::Pixel::TRANSPARENT)
18
+
19
+ @width, @height = width, height
20
+
21
+ if initial.kind_of?(ChunkyPNG::Pixel)
22
+ @pixels = Array.new(width * height, initial)
23
+ elsif initial.kind_of?(Array) && initial.size == width * height
24
+ @pixels = initial
25
+ else
26
+ raise "Cannot use this value as initial pixel matrix: #{initial.inspect}!"
27
+ end
28
+ end
12
29
 
13
- def self.load(header, content)
14
- matrix = self.new(header.width, header.height)
15
- matrix.decode_pixelstream(content, header)
16
- return matrix
30
+ def []=(x, y, pixel)
31
+ @pixels[y * width + x] = pixel
17
32
  end
18
33
 
19
34
  def [](x, y)
@@ -27,131 +42,34 @@ module ChunkyPNG
27
42
  end
28
43
  end
29
44
 
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
45
  def palette
116
46
  ChunkyPNG::Palette.from_pixels(@pixels)
117
47
  end
118
48
 
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
49
+ def opaque?
50
+ pixels.all? { |pixel| pixel.opaque? }
130
51
  end
131
52
 
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
53
+ def indexable?
54
+ palette.indexable?
139
55
  end
140
- end
141
-
142
- class Pixel
143
- attr_accessor :color, :alpha
144
56
 
145
- def initialize(color, alpha = 255)
146
- @color, @alpha = color, alpha
57
+ def to_datastream(constraints = {})
58
+ data = encode(constraints)
59
+ ds = Datastream.new
60
+ ds.header_chunk = Chunk::Header.new(data[:header])
61
+ ds.palette_chunk = data[:palette_chunk] if data[:palette_chunk]
62
+ ds.transparency_chunk = data[:transparency_chunk] if data[:transparency_chunk]
63
+ ds.data_chunks = ds.idat_chunks(data[:pixelstream])
64
+ ds.end_chunk = Chunk::End.new
65
+ return ds
147
66
  end
148
67
 
149
- def opaque?
150
- alpha == 255
68
+ def eql?(other)
69
+ other.kind_of?(self.class) && other.pixels == self.pixels &&
70
+ other.width == self.width && other.height == self.height
151
71
  end
152
72
 
153
- def make_opaque!
154
- alpha = 255
155
- end
73
+ alias :== :eql?
156
74
  end
157
75
  end
@@ -0,0 +1,75 @@
1
+ module ChunkyPNG
2
+ class PixelMatrix
3
+ module Decoding
4
+
5
+ def decode(ds)
6
+ stream = Zlib::Inflate.inflate(ds.data_chunks.map(&:content).join(''))
7
+ width = ds.header_chunk.width
8
+ height = ds.header_chunk.height
9
+ color_mode = ds.header_chunk.color
10
+ palette = ChunkyPNG::Palette.from_chunks(ds.palette_chunk, ds.transparency_chunk)
11
+ decode_pixelstream(stream, width, height, color_mode, palette)
12
+ end
13
+
14
+ def decode_pixelstream(stream, width, height, color_mode = ChunkyPNG::Chunk::Header::COLOR_TRUECOLOR, palette = nil)
15
+
16
+ pixel_size = Pixel.bytesize(color_mode)
17
+ raise "Invalid stream length!" unless stream.length == width * height * pixel_size + height
18
+ raise "This palette is not suitable for decoding!" if palette && !palette.can_decode?
19
+
20
+ pixel_decoder = case color_mode
21
+ when ChunkyPNG::Chunk::Header::COLOR_TRUECOLOR then lambda { |bytes| ChunkyPNG::Pixel.rgb(*bytes) }
22
+ when ChunkyPNG::Chunk::Header::COLOR_TRUECOLOR_ALPHA then lambda { |bytes| ChunkyPNG::Pixel.rgba(*bytes) }
23
+ when ChunkyPNG::Chunk::Header::COLOR_INDEXED then lambda { |bytes| palette[bytes.first] }
24
+ when ChunkyPNG::Chunk::Header::COLOR_GRAYSCALE then lambda { |bytes| ChunkyPNG::Pixel.grayscale(*bytes) }
25
+ when ChunkyPNG::Chunk::Header::COLOR_GRAYSCALE_ALPHA then lambda { |bytes| ChunkyPNG::Pixel.grayscale(*bytes) }
26
+ else raise "No suitable pixel decoder found for color mode #{color_mode}!"
27
+ end
28
+
29
+ pixels = []
30
+ decoded_bytes = Array.new(width * pixel_size, 0)
31
+ height.times do |line_no|
32
+
33
+ # get bytes of scanline
34
+ position = line_no * (width * pixel_size + 1)
35
+ line_length = width * pixel_size
36
+ bytes = stream.unpack("@#{position}CC#{line_length}")
37
+ filter = bytes.shift
38
+ decoded_bytes = decode_scanline(filter, bytes, decoded_bytes, pixel_size)
39
+
40
+ # decode bytes into colors
41
+ decoded_bytes.each_slice(pixel_size) { |bytes| pixels << pixel_decoder.call(bytes) }
42
+ end
43
+
44
+ return ChunkyPNG::PixelMatrix.new(width, height, pixels)
45
+ end
46
+
47
+ protected
48
+
49
+ def decode_scanline(filter, bytes, previous_bytes, pixelsize = 3)
50
+ case filter
51
+ when FILTER_NONE then decode_scanline_none( bytes, previous_bytes, pixelsize)
52
+ when FILTER_SUB then decode_scanline_sub( bytes, previous_bytes, pixelsize)
53
+ when FILTER_UP then decode_scanline_up( bytes, previous_bytes, pixelsize)
54
+ when FILTER_AVERAGE then raise "Average filter are not yet supported!"
55
+ when FILTER_PAETH then raise "Paeth filter are not yet supported!"
56
+ else raise "Unknown filter type"
57
+ end
58
+ end
59
+
60
+ def decode_scanline_none(bytes, previous_bytes, pixelsize = 3)
61
+ bytes
62
+ end
63
+
64
+ def decode_scanline_sub(bytes, previous_bytes, pixelsize = 3)
65
+ bytes.each_with_index { |b, i| bytes[i] = (b + (i >= pixelsize ? bytes[i-pixelsize] : 0)) % 256 }
66
+ bytes
67
+ end
68
+
69
+ def decode_scanline_up(bytes, previous_bytes, pixelsize = 3)
70
+ bytes.each_with_index { |b, i| bytes[i] = (b + previous_bytes[i]) % 256 }
71
+ bytes
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,83 @@
1
+ module ChunkyPNG
2
+ class PixelMatrix
3
+ module Encoding
4
+
5
+ def encode(constraints = {})
6
+ encoding = determine_encoding(constraints)
7
+ result = {}
8
+ result[:header] = { :width => width, :height => height, :color => encoding[:color_mode] }
9
+
10
+ if encoding[:color_mode] == ChunkyPNG::Chunk::Header::COLOR_INDEXED
11
+ result[:palette_chunk] = encoding[:palette].to_plte_chunk
12
+ result[:transparency_chunk] = encoding[:palette].to_trns_chunk unless encoding[:palette].opaque?
13
+ end
14
+
15
+ result[:pixelstream] = encode_pixelstream(encoding[:color_mode], encoding[:palette])
16
+ return result
17
+ end
18
+
19
+ protected
20
+
21
+ def determine_encoding(constraints = {})
22
+ encoding = constraints
23
+ encoding[:palette] ||= palette
24
+ encoding[:color_mode] ||= encoding[:palette].best_colormode
25
+ return encoding
26
+ end
27
+
28
+ def encode_pixelstream(color_mode = ChunkyPNG::Chunk::Header::COLOR_TRUECOLOR, palette = nil)
29
+
30
+ pixel_encoder = case color_mode
31
+ when ChunkyPNG::Chunk::Header::COLOR_TRUECOLOR then lambda { |pixel| pixel.to_truecolor_bytes }
32
+ when ChunkyPNG::Chunk::Header::COLOR_TRUECOLOR_ALPHA then lambda { |pixel| pixel.to_truecolor_alpha_bytes }
33
+ when ChunkyPNG::Chunk::Header::COLOR_INDEXED then lambda { |pixel| pixel.to_indexed_bytes(palette) }
34
+ when ChunkyPNG::Chunk::Header::COLOR_GRAYSCALE then lambda { |pixel| pixel.to_grayscale_bytes }
35
+ when ChunkyPNG::Chunk::Header::COLOR_GRAYSCALE_ALPHA then lambda { |pixel| pixel.to_grayscale_alpha_bytes }
36
+ else raise "Cannot encode pixels for this mode: #{color_mode}!"
37
+ end
38
+
39
+ raise "This palette is not suitable for encoding!" if palette && !palette.can_encode?
40
+
41
+ pixelsize = Pixel.bytesize(color_mode)
42
+
43
+ stream = ""
44
+ previous = nil
45
+ each_scanline do |line|
46
+ bytes = line.map(&pixel_encoder).flatten
47
+ if previous
48
+ stream << encode_scanline_up(bytes, previous, pixelsize).pack('C*')
49
+ else
50
+ stream << encode_scanline_sub(bytes, previous, pixelsize).pack('C*')
51
+ end
52
+ previous = bytes
53
+ end
54
+ return stream
55
+ end
56
+
57
+ def encode_scanline(filter, bytes, previous_bytes = nil, pixelsize = 3)
58
+ case filter
59
+ when FILTER_NONE then encode_scanline_none( bytes, previous_bytes, pixelsize)
60
+ when FILTER_SUB then encode_scanline_sub( bytes, previous_bytes, pixelsize)
61
+ when FILTER_UP then encode_scanline_up( bytes, previous_bytes, pixelsize)
62
+ when FILTER_AVERAGE then raise "Average filter are not yet supported!"
63
+ when FILTER_PAETH then raise "Paeth filter are not yet supported!"
64
+ else raise "Unknown filter type"
65
+ end
66
+ end
67
+
68
+ def encode_scanline_none(bytes, previous_bytes = nil, pixelsize = 3)
69
+ [FILTER_NONE] + bytes
70
+ end
71
+
72
+ def encode_scanline_sub(bytes, previous_bytes = nil, pixelsize = 3)
73
+ encoded = (pixelsize...bytes.length).map { |n| (bytes[n-pixelsize] - bytes[n]) % 256 }
74
+ [FILTER_SUB] + bytes[0...pixelsize] + encoded
75
+ end
76
+
77
+ def encode_scanline_up(bytes, previous_bytes, pixelsize = 3)
78
+ encoded = (0...bytes.length).map { |n| previous_bytes[n] - bytes[n] % 256 }
79
+ [FILTER_UP] + encoded
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,22 +1,15 @@
1
1
  require File.expand_path('../spec_helper.rb', File.dirname(__FILE__))
2
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
3
+ describe ChunkyPNG do
4
+
5
+ # it "should create reference images for all color modes" do
6
+ # image = ChunkyPNG::Image.new(10, 10, ChunkyPNG::Pixel.rgb(100, 100, 100))
7
+ # [:indexed, :grayscale, :grayscale_alpha, :truecolor, :truecolor_alpha].each do |color_mode|
8
+ #
9
+ # color_mode_id = ChunkyPNG::Chunk::Header.const_get("COLOR_#{color_mode.to_s.upcase}")
10
+ # filename = resource_file("gray_10x10_#{color_mode}.png")
11
+ # File.open(filename, 'w') { |f| image.write(f, :color_mode => color_mode_id) }
12
+ # end
13
+ # end
21
14
  end
22
15
 
@@ -0,0 +1,44 @@
1
+ require File.expand_path('../spec_helper.rb', File.dirname(__FILE__))
2
+
3
+ describe ChunkyPNG::PixelMatrix::Decoding do
4
+ include ChunkyPNG::PixelMatrix::Decoding
5
+
6
+ describe '#decode_scanline' do
7
+
8
+ it "should decode a line without filtering as is" do
9
+ bytes = [255, 255, 255, 255, 255, 255, 255, 255, 255]
10
+ decode_scanline(ChunkyPNG::PixelMatrix::FILTER_NONE, bytes, nil).should == bytes
11
+ end
12
+
13
+ it "should decode a line with sub filtering correctly" do
14
+ # all white pixels
15
+ bytes = [255, 255, 255, 0, 0, 0, 0, 0, 0]
16
+ decoded_bytes = decode_scanline(ChunkyPNG::PixelMatrix::FILTER_SUB, bytes, nil)
17
+ decoded_bytes.should == [255, 255, 255, 255, 255, 255, 255, 255, 255]
18
+
19
+ # all black pixels
20
+ bytes = [0, 0, 0, 0, 0, 0, 0, 0, 0]
21
+ decoded_bytes = decode_scanline(ChunkyPNG::PixelMatrix::FILTER_SUB, bytes, nil)
22
+ decoded_bytes.should == [0, 0, 0, 0, 0, 0, 0, 0, 0]
23
+
24
+ # various colors
25
+ bytes = [255, 0, 45, 0, 255, 0, 112, 200, 178]
26
+ decoded_bytes = decode_scanline(ChunkyPNG::PixelMatrix::FILTER_SUB, bytes, nil)
27
+ decoded_bytes.should == [255, 0, 45, 255, 255, 45, 111, 199, 223]
28
+ end
29
+
30
+ it "should decode a line with up filtering correctly" do
31
+ # previous line is all black
32
+ previous_bytes = [0, 0, 0, 0, 0, 0, 0, 0, 0]
33
+ bytes = [255, 255, 255, 127, 127, 127, 0, 0, 0]
34
+ decoded_bytes = decode_scanline(ChunkyPNG::PixelMatrix::FILTER_UP, bytes, previous_bytes)
35
+ decoded_bytes.should == [255, 255, 255, 127, 127, 127, 0, 0, 0]
36
+
37
+ # previous line has various pixels
38
+ previous_bytes = [255, 255, 255, 127, 127, 127, 0, 0, 0]
39
+ bytes = [0, 127, 255, 0, 127, 255, 0, 127, 255]
40
+ decoded_bytes = decode_scanline(ChunkyPNG::PixelMatrix::FILTER_UP, bytes, previous_bytes)
41
+ decoded_bytes.should == [255, 126, 254, 127, 254, 126, 0, 127, 255]
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,27 @@
1
+ require File.expand_path('../spec_helper.rb', File.dirname(__FILE__))
2
+
3
+ describe ChunkyPNG::PixelMatrix::Encoding do
4
+ include ChunkyPNG::PixelMatrix::Encoding
5
+
6
+ describe '#encode_scanline' do
7
+
8
+ it "should encode a scanline without filtering correctly" do
9
+ bytes = [0, 0, 0, 1, 1, 1, 2, 2, 2]
10
+ encoded_bytes = encode_scanline(ChunkyPNG::PixelMatrix::FILTER_NONE, bytes, nil)
11
+ encoded_bytes.should == [0, 0, 0, 0, 1, 1, 1, 2, 2, 2]
12
+ end
13
+
14
+ it "should encode a scanline with sub filtering correctly" do
15
+ bytes = [255, 255, 255, 255, 255, 255, 255, 255, 255]
16
+ encoded_bytes = encode_scanline(ChunkyPNG::PixelMatrix::FILTER_SUB, bytes, nil)
17
+ encoded_bytes.should == [1, 255, 255, 255, 0, 0, 0, 0, 0, 0]
18
+ end
19
+
20
+ it "should encode a scanline with up filtering correctly" do
21
+ bytes = [255, 255, 255, 255, 255, 255, 255, 255, 255]
22
+ previous_bytes = [255, 255, 255, 255, 255, 255, 255, 255, 255]
23
+ encoded_bytes = encode_scanline(ChunkyPNG::PixelMatrix::FILTER_UP, bytes, previous_bytes)
24
+ encoded_bytes.should == [2, 0, 0, 0, 0, 0, 0, 0, 0, 0]
25
+ end
26
+ end
27
+ end
@@ -2,70 +2,36 @@ require File.expand_path('../spec_helper.rb', File.dirname(__FILE__))
2
2
 
3
3
  describe ChunkyPNG::PixelMatrix do
4
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
5
+ describe '.decode' do
14
6
 
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]
7
+ before(:each) do
8
+ @reference = ChunkyPNG::PixelMatrix.new(10, 10, ChunkyPNG::Pixel.rgb(100, 100, 100))
30
9
  end
31
10
 
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]
11
+ [:indexed, :grayscale, :grayscale_alpha, :truecolor, :truecolor_alpha].each do |color_mode|
12
+ it "should decode an image with color mode #{color_mode} correctly" do
13
+ ds = ChunkyPNG.load(resource_file("gray_10x10_#{color_mode}.png"))
14
+ ds.pixel_matrix.should == @reference
15
+ end
44
16
  end
45
17
  end
46
18
 
47
- describe '#encode_scanline' do
19
+ describe '.encode' do
48
20
  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]
21
+ @reference = ChunkyPNG::PixelMatrix.new(10, 10, ChunkyPNG::Pixel.rgb(100, 100, 100))
56
22
  end
57
23
 
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]
24
+ [:indexed, :grayscale, :grayscale_alpha, :truecolor, :truecolor_alpha].each do |color_mode|
25
+ it "should encode an image with color mode #{color_mode} correctly" do
26
+
27
+ filename = resource_file("_tmp_#{color_mode}.png")
28
+ File.open(filename, 'w') { |f| @reference.to_datastream.write(f) }
29
+
30
+ ChunkyPNG.load(filename).pixel_matrix.should == @reference
31
+
32
+ File.unlink(filename)
33
+ end
62
34
  end
63
35
 
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
36
  end
71
37
  end
@@ -0,0 +1,32 @@
1
+ require File.expand_path('../spec_helper.rb', File.dirname(__FILE__))
2
+
3
+ describe ChunkyPNG::Pixel do
4
+
5
+ before(:each) do
6
+ @white = ChunkyPNG::Pixel.rgba(255, 255, 255, 255)
7
+ @black = ChunkyPNG::Pixel.rgba( 0, 0, 0, 255)
8
+ @opaque = ChunkyPNG::Pixel.rgba( 10, 100, 150, 255)
9
+ @non_opaque = ChunkyPNG::Pixel.rgba( 10, 100, 150, 100)
10
+ @fully_transparent = ChunkyPNG::Pixel.rgba( 10, 100, 150, 0)
11
+ end
12
+
13
+ it "should represent pixels as the correct number" do
14
+ @white.value.should == 0xffffffff
15
+ @black.value.should == 0x000000ff
16
+ @opaque.value.should == 0x0a6496ff
17
+ end
18
+
19
+ it "should correctly check for opaqueness" do
20
+ @white.should be_opaque
21
+ @black.should be_opaque
22
+ @opaque.should be_opaque
23
+ @non_opaque.should_not be_opaque
24
+ @fully_transparent.should_not be_opaque
25
+ end
26
+
27
+ it "should convert the individual color values back correctly" do
28
+ @opaque.to_truecolor_bytes.should == [10, 100, 150]
29
+ @non_opaque.to_truecolor_alpha_bytes.should == [10, 100, 150, 100]
30
+ end
31
+ end
32
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chunky_png
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Willem van Bergen
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-01-10 00:00:00 +01:00
12
+ date: 2010-01-12 00:00:00 +01:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -33,19 +33,30 @@ extra_rdoc_files:
33
33
  - README.rdoc
34
34
  files:
35
35
  - spec/spec_helper.rb
36
+ - spec/resources/gray_10x10_grayscale.png
37
+ - spec/resources/gray_10x10.png
36
38
  - .gitignore
39
+ - spec/resources/gray_10x10_truecolor_alpha.png
37
40
  - lib/chunky_png/pixel_matrix.rb
38
41
  - LICENSE
42
+ - spec/resources/gray_10x10_truecolor.png
43
+ - lib/chunky_png/pixel_matrix/decoding.rb
39
44
  - lib/chunky_png/chunk.rb
45
+ - spec/unit/encoding_spec.rb
40
46
  - Rakefile
41
47
  - README.rdoc
42
- - spec/resources/indexed_10x10.png
48
+ - spec/resources/gray_10x10_indexed.png
43
49
  - spec/integration/image_spec.rb
50
+ - lib/chunky_png/pixel.rb
44
51
  - lib/chunky_png/palette.rb
45
52
  - lib/chunky_png/datastream.rb
46
53
  - chunky_png.gemspec
47
54
  - tasks/github-gem.rake
55
+ - spec/unit/decoding_spec.rb
56
+ - spec/resources/gray_10x10_grayscale_alpha.png
57
+ - lib/chunky_png/pixel_matrix/encoding.rb
48
58
  - lib/chunky_png/image.rb
59
+ - spec/unit/pixel_spec.rb
49
60
  - spec/unit/pixel_matrix_spec.rb
50
61
  - lib/chunky_png.rb
51
62
  has_rdoc: true
@@ -82,5 +93,8 @@ signing_key:
82
93
  specification_version: 3
83
94
  summary: Pure ruby library for read/write, chunk-level access to PNG files
84
95
  test_files:
96
+ - spec/unit/encoding_spec.rb
85
97
  - spec/integration/image_spec.rb
98
+ - spec/unit/decoding_spec.rb
99
+ - spec/unit/pixel_spec.rb
86
100
  - spec/unit/pixel_matrix_spec.rb