chunky_png 0.0.1 → 0.0.2

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