chunky_png 0.5.1 → 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- data/chunky_png.gemspec +1 -1
- data/lib/chunky_png.rb +1 -1
- data/lib/chunky_png/canvas/adam7_interlacing.rb +2 -2
- data/lib/chunky_png/canvas/png_decoding.rb +44 -35
- data/lib/chunky_png/canvas/png_encoding.rb +38 -34
- data/lib/chunky_png/color.rb +5 -5
- data/spec/chunky_png/canvas/png_decoding_spec.rb +1 -1
- data/spec/chunky_png/canvas/png_encoding_spec.rb +1 -1
- data/spec/chunky_png/canvas_spec.rb +14 -6
- data/spec/chunky_png/rmagick_spec.rb +2 -4
- metadata +1 -1
data/chunky_png.gemspec
CHANGED
@@ -3,7 +3,7 @@ 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.5.
|
6
|
+
s.version = "0.5.2"
|
7
7
|
s.date = "2010-01-16"
|
8
8
|
|
9
9
|
s.summary = "Pure ruby library for read/write, chunk-level access to PNG files"
|
data/lib/chunky_png.rb
CHANGED
@@ -22,7 +22,7 @@ module ChunkyPNG
|
|
22
22
|
|
23
23
|
# The current version of ChunkyPNG. This value will be updated automatically
|
24
24
|
# by them gem:release rake task.
|
25
|
-
VERSION = "0.5.
|
25
|
+
VERSION = "0.5.2"
|
26
26
|
|
27
27
|
###################################################
|
28
28
|
# PNG international standard defined constants
|
@@ -26,8 +26,8 @@ module ChunkyPNG
|
|
26
26
|
|
27
27
|
def adam7_merge_pass(pass, canvas, subcanvas)
|
28
28
|
m_o = adam7_multiplier_offset(pass)
|
29
|
-
0
|
30
|
-
0
|
29
|
+
for y in 0...subcanvas.height do
|
30
|
+
for x in 0...subcanvas.width do
|
31
31
|
new_x = x * m_o[:x_multiplier] + m_o[:x_offset]
|
32
32
|
new_y = y * m_o[:y_multiplier] + m_o[:y_offset]
|
33
33
|
canvas[new_x, new_y] = subcanvas[x, y]
|
@@ -5,6 +5,8 @@ module ChunkyPNG
|
|
5
5
|
# The datastream can be provided as filename, string or IO object.
|
6
6
|
module PNGDecoding
|
7
7
|
|
8
|
+
attr_accessor :decoding_palette
|
9
|
+
|
8
10
|
def from_blob(str)
|
9
11
|
from_datastream(ChunkyPNG::Datastream.from_blob(str))
|
10
12
|
end
|
@@ -26,37 +28,59 @@ module ChunkyPNG
|
|
26
28
|
height = ds.header_chunk.height
|
27
29
|
color_mode = ds.header_chunk.color
|
28
30
|
interlace = ds.header_chunk.interlace
|
29
|
-
|
30
|
-
|
31
|
-
|
31
|
+
|
32
|
+
self.decoding_palette = ChunkyPNG::Palette.from_chunks(ds.palette_chunk, ds.transparency_chunk)
|
33
|
+
pixelstream = ChunkyPNG::Chunk::ImageData.combine_chunks(ds.data_chunks)
|
34
|
+
|
35
|
+
decode_png_pixelstream(pixelstream, width, height, color_mode, interlace)
|
32
36
|
end
|
33
37
|
|
34
|
-
def decode_png_pixelstream(stream, width, height, color_mode = ChunkyPNG::COLOR_TRUECOLOR,
|
35
|
-
raise "This palette is not suitable for decoding!" if
|
36
|
-
|
37
|
-
pixel_size = Color.bytesize(color_mode)
|
38
|
-
pixel_decoder = case color_mode
|
39
|
-
when ChunkyPNG::COLOR_TRUECOLOR then lambda { |bytes| ChunkyPNG::Color.rgb(*bytes) }
|
40
|
-
when ChunkyPNG::COLOR_TRUECOLOR_ALPHA then lambda { |bytes| ChunkyPNG::Color.rgba(*bytes) }
|
41
|
-
when ChunkyPNG::COLOR_INDEXED then lambda { |bytes| palette[bytes.first] }
|
42
|
-
when ChunkyPNG::COLOR_GRAYSCALE then lambda { |bytes| ChunkyPNG::Color.grayscale(*bytes) }
|
43
|
-
when ChunkyPNG::COLOR_GRAYSCALE_ALPHA then lambda { |bytes| ChunkyPNG::Color.grayscale(*bytes) }
|
44
|
-
else raise "No suitable pixel decoder found for color mode #{color_mode}!"
|
45
|
-
end
|
38
|
+
def decode_png_pixelstream(stream, width, height, color_mode = ChunkyPNG::COLOR_TRUECOLOR, interlace = ChunkyPNG::INTERLACING_NONE)
|
39
|
+
raise "This palette is not suitable for decoding!" if decoding_palette && !decoding_palette.can_decode?
|
46
40
|
|
47
41
|
return case interlace
|
48
|
-
when ChunkyPNG::INTERLACING_NONE then decode_png_without_interlacing(stream, width, height,
|
49
|
-
when ChunkyPNG::INTERLACING_ADAM7 then decode_png_with_adam7_interlacing(stream, width, height,
|
42
|
+
when ChunkyPNG::INTERLACING_NONE then decode_png_without_interlacing(stream, width, height, color_mode)
|
43
|
+
when ChunkyPNG::INTERLACING_ADAM7 then decode_png_with_adam7_interlacing(stream, width, height, color_mode)
|
50
44
|
else raise "Don't know how the handle interlacing method #{interlace}!"
|
51
45
|
end
|
52
46
|
end
|
53
47
|
|
54
48
|
protected
|
55
49
|
|
56
|
-
def
|
57
|
-
|
50
|
+
def decode_png_without_interlacing(stream, width, height, color_mode)
|
51
|
+
decode_png_image_pass(stream, width, height, color_mode)
|
52
|
+
end
|
58
53
|
|
54
|
+
def decode_png_with_adam7_interlacing(stream, width, height, color_mode)
|
55
|
+
canvas = ChunkyPNG::Canvas.new(width, height)
|
56
|
+
pixel_size = Color.bytesize(color_mode)
|
57
|
+
start_pos = 0
|
58
|
+
for pass in 0...7 do
|
59
|
+
sm_width, sm_height = adam7_pass_size(pass, width, height)
|
60
|
+
sm = decode_png_image_pass(stream, sm_width, sm_height, color_mode, start_pos)
|
61
|
+
adam7_merge_pass(pass, canvas, sm)
|
62
|
+
start_pos += (sm_width * sm_height * pixel_size) + sm_height
|
63
|
+
end
|
64
|
+
canvas
|
65
|
+
end
|
66
|
+
|
67
|
+
def decode_png_image_pass(stream, width, height, color_mode, start_pos = 0)
|
68
|
+
|
69
|
+
pixel_size = Color.bytesize(color_mode)
|
70
|
+
pixel_decoder = case color_mode
|
71
|
+
when ChunkyPNG::COLOR_TRUECOLOR then lambda { |bytes| ChunkyPNG::Color.rgb(*bytes) }
|
72
|
+
when ChunkyPNG::COLOR_TRUECOLOR_ALPHA then lambda { |bytes| ChunkyPNG::Color.rgba(*bytes) }
|
73
|
+
when ChunkyPNG::COLOR_INDEXED then lambda { |bytes| decoding_palette[bytes.first] }
|
74
|
+
when ChunkyPNG::COLOR_GRAYSCALE then lambda { |bytes| ChunkyPNG::Color.grayscale(*bytes) }
|
75
|
+
when ChunkyPNG::COLOR_GRAYSCALE_ALPHA then lambda { |bytes| ChunkyPNG::Color.grayscale_alpha(*bytes) }
|
76
|
+
else raise "No suitable pixel decoder found for color mode #{color_mode}!"
|
77
|
+
end
|
78
|
+
|
79
|
+
pixels = []
|
59
80
|
if width > 0
|
81
|
+
|
82
|
+
raise "Invalid stream length!" unless stream.length - start_pos >= width * height * pixel_size + height
|
83
|
+
|
60
84
|
decoded_bytes = Array.new(width * pixel_size, 0)
|
61
85
|
for line_no in 0...height do
|
62
86
|
|
@@ -75,22 +99,7 @@ module ChunkyPNG
|
|
75
99
|
new(width, height, pixels)
|
76
100
|
end
|
77
101
|
|
78
|
-
def decode_png_without_interlacing(stream, width, height, pixel_size, pixel_decoder)
|
79
|
-
raise "Invalid stream length!" unless stream.length == width * height * pixel_size + height
|
80
|
-
decode_png_image_pass(stream, width, height, pixel_size, pixel_decoder)
|
81
|
-
end
|
82
102
|
|
83
|
-
def decode_png_with_adam7_interlacing(stream, width, height, pixel_size, pixel_decoder)
|
84
|
-
start_pos = 0
|
85
|
-
canvas = ChunkyPNG::Canvas.new(width, height)
|
86
|
-
0.upto(6) do |pass|
|
87
|
-
sm_width, sm_height = adam7_pass_size(pass, width, height)
|
88
|
-
sm = decode_png_image_pass(stream, sm_width, sm_height, pixel_size, pixel_decoder, start_pos)
|
89
|
-
adam7_merge_pass(pass, canvas, sm)
|
90
|
-
start_pos += (sm_width * sm_height * pixel_size) + sm_height
|
91
|
-
end
|
92
|
-
canvas
|
93
|
-
end
|
94
103
|
|
95
104
|
def decode_png_scanline(filter, bytes, previous_bytes, pixelsize = 3)
|
96
105
|
case filter
|
@@ -121,7 +130,7 @@ module ChunkyPNG
|
|
121
130
|
bytes.each_with_index do |byte, i|
|
122
131
|
a = (i >= pixelsize) ? bytes[i - pixelsize] : 0
|
123
132
|
b = previous_bytes[i]
|
124
|
-
bytes[i] = (byte + (a + b
|
133
|
+
bytes[i] = (byte + ((a + b) >> 1)) % 256
|
125
134
|
end
|
126
135
|
bytes
|
127
136
|
end
|
@@ -5,11 +5,8 @@ module ChunkyPNG
|
|
5
5
|
#
|
6
6
|
module PNGEncoding
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
GRAYSCALE_ENCODER = lambda { |color| Color.to_grayscale_bytes(color) }
|
11
|
-
GRAYSCALE_ALPHA_ENCODER = lambda { |color| Color.to_grayscale_alpha_bytes(color) }
|
12
|
-
|
8
|
+
attr_accessor :encoding_palette
|
9
|
+
|
13
10
|
def write(io, constraints = {})
|
14
11
|
to_datastream(constraints).write(io)
|
15
12
|
end
|
@@ -46,11 +43,11 @@ module ChunkyPNG
|
|
46
43
|
result[:header] = { :width => width, :height => height, :color => encoding[:color_mode], :interlace => encoding[:interlace] }
|
47
44
|
|
48
45
|
if encoding[:color_mode] == ChunkyPNG::COLOR_INDEXED
|
49
|
-
result[:palette_chunk] =
|
50
|
-
result[:transparency_chunk] =
|
46
|
+
result[:palette_chunk] = encoding_palette.to_plte_chunk
|
47
|
+
result[:transparency_chunk] = encoding_palette.to_trns_chunk unless encoding_palette.opaque?
|
51
48
|
end
|
52
49
|
|
53
|
-
result[:pixelstream] = encode_png_pixelstream(encoding[:color_mode], encoding[:
|
50
|
+
result[:pixelstream] = encode_png_pixelstream(encoding[:color_mode], encoding[:interlace])
|
54
51
|
return result
|
55
52
|
end
|
56
53
|
|
@@ -66,10 +63,12 @@ module ChunkyPNG
|
|
66
63
|
|
67
64
|
# Do not create a pallete when the encoding is given and does not require a palette.
|
68
65
|
if encoding[:color_mode]
|
69
|
-
|
66
|
+
if encoding[:color_mode] == ChunkyPNG::COLOR_INDEXED
|
67
|
+
self.encoding_palette = self.palette
|
68
|
+
end
|
70
69
|
else
|
71
|
-
|
72
|
-
encoding[:color_mode] ||=
|
70
|
+
self.encoding_palette = self.palette
|
71
|
+
encoding[:color_mode] ||= encoding_palette.best_colormode
|
73
72
|
end
|
74
73
|
|
75
74
|
encoding[:interlace] = case encoding[:interlace]
|
@@ -81,52 +80,57 @@ module ChunkyPNG
|
|
81
80
|
return encoding
|
82
81
|
end
|
83
82
|
|
84
|
-
def encode_png_pixelstream(color_mode = ChunkyPNG::COLOR_TRUECOLOR,
|
83
|
+
def encode_png_pixelstream(color_mode = ChunkyPNG::COLOR_TRUECOLOR, interlace = ChunkyPNG::INTERLACING_NONE)
|
85
84
|
|
86
|
-
if color_mode == ChunkyPNG::COLOR_INDEXED && !
|
85
|
+
if color_mode == ChunkyPNG::COLOR_INDEXED && (encoding_palette.nil? || !encoding_palette.can_encode?)
|
87
86
|
raise "This palette is not suitable for encoding!"
|
88
87
|
end
|
89
88
|
|
90
|
-
pixel_size = Color.bytesize(color_mode)
|
91
|
-
pixel_encoder = case color_mode
|
92
|
-
when ChunkyPNG::COLOR_TRUECOLOR then TRUECOLOR_ENCODER
|
93
|
-
when ChunkyPNG::COLOR_TRUECOLOR_ALPHA then TRUECOLOR_ALPHA_ENCODER
|
94
|
-
when ChunkyPNG::COLOR_GRAYSCALE then GRAYSCALE_ENCODER
|
95
|
-
when ChunkyPNG::COLOR_GRAYSCALE_ALPHA then GRAYSCALE_ALPHA_ENCODER
|
96
|
-
when ChunkyPNG::COLOR_INDEXED then lambda { |color| [palette.index(color)] }
|
97
|
-
else raise "Cannot encode pixels for this mode: #{color_mode}!"
|
98
|
-
end
|
99
|
-
|
100
89
|
case interlace
|
101
|
-
when ChunkyPNG::INTERLACING_NONE then encode_png_image_without_interlacing(
|
102
|
-
when ChunkyPNG::INTERLACING_ADAM7 then encode_png_image_with_interlacing(
|
90
|
+
when ChunkyPNG::INTERLACING_NONE then encode_png_image_without_interlacing(color_mode)
|
91
|
+
when ChunkyPNG::INTERLACING_ADAM7 then encode_png_image_with_interlacing(color_mode)
|
103
92
|
else raise "Unknown interlacing method!"
|
104
93
|
end
|
105
94
|
end
|
106
95
|
|
107
|
-
def encode_png_image_without_interlacing(
|
96
|
+
def encode_png_image_without_interlacing(color_mode)
|
108
97
|
stream = ""
|
109
|
-
encode_png_image_pass_to_stream(stream,
|
98
|
+
encode_png_image_pass_to_stream(stream, color_mode)
|
110
99
|
stream
|
111
100
|
end
|
112
101
|
|
113
|
-
def encode_png_image_with_interlacing(
|
102
|
+
def encode_png_image_with_interlacing(color_mode)
|
114
103
|
stream = ""
|
115
104
|
0.upto(6) do |pass|
|
116
105
|
subcanvas = self.class.adam7_extract_pass(pass, self)
|
117
|
-
subcanvas.
|
106
|
+
subcanvas.encoding_palette = encoding_palette
|
107
|
+
subcanvas.encode_png_image_pass_to_stream(stream, color_mode)
|
118
108
|
end
|
119
109
|
stream
|
120
110
|
end
|
121
111
|
|
122
|
-
def encode_png_image_pass_to_stream(stream,
|
123
|
-
|
124
|
-
|
112
|
+
def encode_png_image_pass_to_stream(stream, color_mode)
|
113
|
+
|
114
|
+
case color_mode
|
115
|
+
when ChunkyPNG::COLOR_TRUECOLOR_ALPHA
|
125
116
|
stream << pixels.pack("xN#{width}" * height)
|
126
|
-
|
117
|
+
|
118
|
+
when ChunkyPNG::COLOR_TRUECOLOR
|
127
119
|
line_packer = 'x' + ('NX' * width)
|
128
120
|
stream << pixels.pack(line_packer * height)
|
121
|
+
|
129
122
|
else
|
123
|
+
|
124
|
+
pixel_size = Color.bytesize(color_mode)
|
125
|
+
pixel_encoder = case color_mode
|
126
|
+
when ChunkyPNG::COLOR_TRUECOLOR then lambda { |color| Color.to_truecolor_bytes(color) }
|
127
|
+
when ChunkyPNG::COLOR_TRUECOLOR_ALPHA then lambda { |color| Color.to_truecolor_alpha_bytes(color) }
|
128
|
+
when ChunkyPNG::COLOR_INDEXED then lambda { |color| [encoding_palette.index(color)] }
|
129
|
+
when ChunkyPNG::COLOR_GRAYSCALE then lambda { |color| Color.to_grayscale_bytes(color) }
|
130
|
+
when ChunkyPNG::COLOR_GRAYSCALE_ALPHA then lambda { |color| Color.to_grayscale_alpha_bytes(color) }
|
131
|
+
else raise "Cannot encode pixels for this mode: #{color_mode}!"
|
132
|
+
end
|
133
|
+
|
130
134
|
previous_bytes = Array.new(pixel_size * width, 0)
|
131
135
|
each_scanline do |line|
|
132
136
|
unencoded_bytes = line.map(&pixel_encoder).flatten
|
@@ -183,7 +187,7 @@ module ChunkyPNG
|
|
183
187
|
for index in 0...original_bytes.length do
|
184
188
|
a = (index >= pixelsize) ? original_bytes[index - pixelsize] : 0
|
185
189
|
b = previous_bytes[index]
|
186
|
-
encoded_bytes[index] = (original_bytes[index] - (a + b
|
190
|
+
encoded_bytes[index] = (original_bytes[index] - ((a + b) >> 1)) % 256
|
187
191
|
end
|
188
192
|
[ChunkyPNG::FILTER_AVERAGE] + encoded_bytes
|
189
193
|
end
|
data/lib/chunky_png/color.rb
CHANGED
@@ -31,20 +31,20 @@ module ChunkyPNG
|
|
31
31
|
|
32
32
|
# Creates a new color using an r, g, b triple.
|
33
33
|
# @return [Fixnum] The newly constructed color value.
|
34
|
-
def rgb(r, g, b
|
35
|
-
|
34
|
+
def rgb(r, g, b)
|
35
|
+
r << 24 | g << 16 | b << 8 | 0xff
|
36
36
|
end
|
37
37
|
|
38
38
|
# Creates a new color using a grayscale teint.
|
39
39
|
# @return [ChunkyPNG::Color] The newly constructed color value.
|
40
|
-
def grayscale(teint
|
41
|
-
|
40
|
+
def grayscale(teint)
|
41
|
+
teint << 24 | teint << 16 | teint << 8 | 0xff
|
42
42
|
end
|
43
43
|
|
44
44
|
# Creates a new color using a grayscale teint and alpha value.
|
45
45
|
# @return [Fixnum] The newly constructed color value.
|
46
46
|
def grayscale_alpha(teint, a)
|
47
|
-
|
47
|
+
teint << 24 | teint << 16 | teint << 8 | a
|
48
48
|
end
|
49
49
|
|
50
50
|
####################################################################
|
@@ -43,7 +43,7 @@ describe ChunkyPNG::Canvas::PNGDecoding do
|
|
43
43
|
|
44
44
|
it "should decode a line with average filtering correctly" do
|
45
45
|
previous = [10, 20, 30, 40, 50, 60, 70, 80, 80, 100, 110, 120]
|
46
|
-
current = [ 0, 0, 10,
|
46
|
+
current = [ 0, 0, 10, 23, 15, 13, 23, 63, 38, 60, 253, 53]
|
47
47
|
decoded = decode_png_scanline(ChunkyPNG::FILTER_AVERAGE, current, previous)
|
48
48
|
decoded.should == [5, 10, 25, 45, 45, 55, 80, 125, 105, 150, 114, 165]
|
49
49
|
end
|
@@ -57,7 +57,7 @@ describe ChunkyPNG::Canvas::PNGEncoding do
|
|
57
57
|
previous = [10, 20, 30, 40, 50, 60, 70, 80, 80, 100, 110, 120]
|
58
58
|
current = [ 5, 10, 25, 45, 45, 55, 80, 125, 105, 150, 114, 165]
|
59
59
|
encoded = encode_png_scanline(ChunkyPNG::FILTER_AVERAGE, current, previous)
|
60
|
-
encoded.should == [3, 0,
|
60
|
+
encoded.should == [3, 0, 0, 10, 23, 15, 13, 23, 63, 38, 60, 253, 53]
|
61
61
|
end
|
62
62
|
|
63
63
|
it "should encode a scanline with paeth filtering correctly" do
|
@@ -3,7 +3,7 @@ require 'spec_helper'
|
|
3
3
|
describe ChunkyPNG::Canvas do
|
4
4
|
|
5
5
|
describe '.from_rgb_stream' do
|
6
|
-
it "should load an image correctly from a
|
6
|
+
it "should load an image correctly from a datastream" do
|
7
7
|
File.open(resource_file('pixelstream.rgb')) do |stream|
|
8
8
|
matrix = ChunkyPNG::Canvas.from_rgb_stream(240, 180, stream)
|
9
9
|
matrix.should == reference_canvas('pixelstream_reference')
|
@@ -12,7 +12,7 @@ describe ChunkyPNG::Canvas do
|
|
12
12
|
end
|
13
13
|
|
14
14
|
describe '.from_rgba_stream' do
|
15
|
-
it "should load an image correctly from a
|
15
|
+
it "should load an image correctly from a datastream" do
|
16
16
|
File.open(resource_file('pixelstream.rgba')) do |stream|
|
17
17
|
matrix = ChunkyPNG::Canvas.from_rgba_stream(240, 180, stream)
|
18
18
|
matrix.should == reference_canvas('pixelstream_reference')
|
@@ -21,16 +21,24 @@ describe ChunkyPNG::Canvas do
|
|
21
21
|
end
|
22
22
|
|
23
23
|
describe '#to_rgba_stream' do
|
24
|
-
|
24
|
+
before (:each) do
|
25
|
+
File.open(resource_file('pixelstream.rgba'), 'rb') { |f| @reference_data = f.read }
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should load an image correctly from a datastream" do
|
25
29
|
canvas = reference_canvas('pixelstream_reference')
|
26
|
-
canvas.to_rgba_stream.should ==
|
30
|
+
canvas.to_rgba_stream.should == @reference_data
|
27
31
|
end
|
28
32
|
end
|
29
33
|
|
30
34
|
describe '#to_rgb_stream' do
|
31
|
-
|
35
|
+
before (:each) do
|
36
|
+
File.open(resource_file('pixelstream.rgb'), 'rb') { |f| @reference_data = f.read }
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should load an image correctly from a datastream" do
|
32
40
|
canvas = reference_canvas('pixelstream_reference')
|
33
|
-
canvas.to_rgb_stream.should ==
|
41
|
+
canvas.to_rgb_stream.should == @reference_data
|
34
42
|
end
|
35
43
|
end
|
36
44
|
|
@@ -13,9 +13,7 @@ describe ChunkyPNG::RMagick do
|
|
13
13
|
it "should export an image to RMagick correctly" do
|
14
14
|
canvas = reference_canvas('16x16_non_interlaced')
|
15
15
|
image = ChunkyPNG::RMagick.export(canvas)
|
16
|
-
|
17
|
-
|
18
|
-
image.write("png:#{filename}") { |info| info.depth = 8 }
|
19
|
-
canvas.should == ChunkyPNG::Canvas.from_file(filename)
|
16
|
+
image.format = 'PNG32'
|
17
|
+
canvas.should == ChunkyPNG::Canvas.from_blob(image.to_blob)
|
20
18
|
end
|
21
19
|
end
|