chunky_png 0.5.1 → 0.5.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/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
|