chunky_png 0.0.5 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +20 -10
- data/chunky_png.gemspec +18 -6
- data/lib/chunky_png.rb +11 -28
- data/lib/chunky_png/canvas.rb +186 -0
- data/lib/chunky_png/canvas/adam7_interlacing.rb +53 -0
- data/lib/chunky_png/canvas/drawing.rb +8 -0
- data/lib/chunky_png/{pixel_matrix → canvas}/operations.rb +12 -12
- data/lib/chunky_png/canvas/png_decoding.rb +145 -0
- data/lib/chunky_png/canvas/png_encoding.rb +182 -0
- data/lib/chunky_png/chunk.rb +101 -23
- data/lib/chunky_png/color.rb +307 -0
- data/lib/chunky_png/datastream.rb +143 -45
- data/lib/chunky_png/image.rb +31 -30
- data/lib/chunky_png/palette.rb +49 -47
- data/lib/chunky_png/rmagick.rb +43 -0
- data/spec/chunky_png/canvas/adam7_interlacing_spec.rb +108 -0
- data/spec/chunky_png/canvas/png_decoding_spec.rb +81 -0
- data/spec/chunky_png/canvas/png_encoding_spec.rb +70 -0
- data/spec/chunky_png/canvas_spec.rb +71 -0
- data/spec/chunky_png/color_spec.rb +104 -0
- data/spec/chunky_png/datastream_spec.rb +32 -0
- data/spec/chunky_png/image_spec.rb +25 -0
- data/spec/chunky_png/rmagick_spec.rb +21 -0
- data/spec/{integration/image_spec.rb → chunky_png_spec.rb} +14 -8
- data/spec/resources/composited.png +0 -0
- data/spec/resources/cropped.png +0 -0
- data/spec/resources/damaged_chunk.png +0 -0
- data/spec/resources/damaged_signature.png +13 -0
- data/spec/resources/pixelstream.rgba +67 -0
- data/spec/resources/replaced.png +0 -0
- data/spec/resources/text_chunk.png +0 -0
- data/spec/resources/ztxt_chunk.png +0 -0
- data/spec/spec_helper.rb +8 -5
- data/tasks/github-gem.rake +1 -1
- metadata +37 -18
- data/lib/chunky_png/pixel.rb +0 -272
- data/lib/chunky_png/pixel_matrix.rb +0 -136
- data/lib/chunky_png/pixel_matrix/decoding.rb +0 -159
- data/lib/chunky_png/pixel_matrix/encoding.rb +0 -89
- data/spec/unit/decoding_spec.rb +0 -83
- data/spec/unit/encoding_spec.rb +0 -27
- data/spec/unit/pixel_matrix_spec.rb +0 -93
- data/spec/unit/pixel_spec.rb +0 -47
@@ -0,0 +1,145 @@
|
|
1
|
+
module ChunkyPNG
|
2
|
+
class Canvas
|
3
|
+
|
4
|
+
# The PNGDecoding contains methods for decoding PNG datastreams to create a Canvas object.
|
5
|
+
# The datastream can be provided as filename, string or IO object.
|
6
|
+
module PNGDecoding
|
7
|
+
|
8
|
+
def from_blob(str)
|
9
|
+
from_datastream(ChunkyPNG::Datastream.from_blob(str))
|
10
|
+
end
|
11
|
+
|
12
|
+
alias :from_string :from_blob
|
13
|
+
|
14
|
+
def from_file(filename)
|
15
|
+
from_datastream(ChunkyPNG::Datastream.from_file(filename))
|
16
|
+
end
|
17
|
+
|
18
|
+
def from_io(io)
|
19
|
+
from_datastream(ChunkyPNG::Datastream.from_io(io))
|
20
|
+
end
|
21
|
+
|
22
|
+
def from_datastream(ds)
|
23
|
+
raise "Only 8-bit color depth is currently supported by ChunkyPNG!" unless ds.header_chunk.depth == 8
|
24
|
+
|
25
|
+
width = ds.header_chunk.width
|
26
|
+
height = ds.header_chunk.height
|
27
|
+
color_mode = ds.header_chunk.color
|
28
|
+
interlace = ds.header_chunk.interlace
|
29
|
+
palette = ChunkyPNG::Palette.from_chunks(ds.palette_chunk, ds.transparency_chunk)
|
30
|
+
stream = ChunkyPNG::Chunk::ImageData.combine_chunks(ds.data_chunks)
|
31
|
+
decode_png_pixelstream(stream, width, height, color_mode, palette, interlace)
|
32
|
+
end
|
33
|
+
|
34
|
+
def decode_png_pixelstream(stream, width, height, color_mode = ChunkyPNG::COLOR_TRUECOLOR, palette = nil, interlace = ChunkyPNG::INTERLACING_NONE)
|
35
|
+
raise "This palette is not suitable for decoding!" if palette && !palette.can_decode?
|
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
|
46
|
+
|
47
|
+
return case interlace
|
48
|
+
when ChunkyPNG::INTERLACING_NONE then decode_png_without_interlacing(stream, width, height, pixel_size, pixel_decoder)
|
49
|
+
when ChunkyPNG::INTERLACING_ADAM7 then decode_png_with_adam7_interlacing(stream, width, height, pixel_size, pixel_decoder)
|
50
|
+
else raise "Don't know how the handle interlacing method #{interlace}!"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
def decode_png_image_pass(stream, width, height, pixel_size, pixel_decoder, start_pos = 0)
|
57
|
+
pixels = []
|
58
|
+
|
59
|
+
if width > 0
|
60
|
+
decoded_bytes = Array.new(width * pixel_size, 0)
|
61
|
+
height.times do |line_no|
|
62
|
+
|
63
|
+
# get bytes of scanline
|
64
|
+
position = start_pos + line_no * (width * pixel_size + 1)
|
65
|
+
line_length = width * pixel_size
|
66
|
+
bytes = stream.unpack("@#{position}CC#{line_length}")
|
67
|
+
filter = bytes.shift
|
68
|
+
decoded_bytes = decode_png_scanline(filter, bytes, decoded_bytes, pixel_size)
|
69
|
+
|
70
|
+
# decode bytes into colors
|
71
|
+
decoded_bytes.each_slice(pixel_size) { |bytes| pixels << pixel_decoder.call(bytes) }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
new(width, height, pixels)
|
76
|
+
end
|
77
|
+
|
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
|
+
|
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
|
+
|
95
|
+
def decode_png_scanline(filter, bytes, previous_bytes, pixelsize = 3)
|
96
|
+
case filter
|
97
|
+
when ChunkyPNG::FILTER_NONE then decode_png_scanline_none( bytes, previous_bytes, pixelsize)
|
98
|
+
when ChunkyPNG::FILTER_SUB then decode_png_scanline_sub( bytes, previous_bytes, pixelsize)
|
99
|
+
when ChunkyPNG::FILTER_UP then decode_png_scanline_up( bytes, previous_bytes, pixelsize)
|
100
|
+
when ChunkyPNG::FILTER_AVERAGE then decode_png_scanline_average( bytes, previous_bytes, pixelsize)
|
101
|
+
when ChunkyPNG::FILTER_PAETH then decode_png_scanline_paeth( bytes, previous_bytes, pixelsize)
|
102
|
+
else raise "Unknown filter type"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def decode_png_scanline_none(bytes, previous_bytes, pixelsize = 3)
|
107
|
+
bytes
|
108
|
+
end
|
109
|
+
|
110
|
+
def decode_png_scanline_sub(bytes, previous_bytes, pixelsize = 3)
|
111
|
+
bytes.each_with_index { |b, i| bytes[i] = (b + (i >= pixelsize ? bytes[i-pixelsize] : 0)) % 256 }
|
112
|
+
bytes
|
113
|
+
end
|
114
|
+
|
115
|
+
def decode_png_scanline_up(bytes, previous_bytes, pixelsize = 3)
|
116
|
+
bytes.each_with_index { |b, i| bytes[i] = (b + previous_bytes[i]) % 256 }
|
117
|
+
bytes
|
118
|
+
end
|
119
|
+
|
120
|
+
def decode_png_scanline_average(bytes, previous_bytes, pixelsize = 3)
|
121
|
+
bytes.each_with_index do |byte, i|
|
122
|
+
a = (i >= pixelsize) ? bytes[i - pixelsize] : 0
|
123
|
+
b = previous_bytes[i]
|
124
|
+
bytes[i] = (byte + (a + b / 2).floor) % 256
|
125
|
+
end
|
126
|
+
bytes
|
127
|
+
end
|
128
|
+
|
129
|
+
def decode_png_scanline_paeth(bytes, previous_bytes, pixelsize = 3)
|
130
|
+
bytes.each_with_index do |byte, i|
|
131
|
+
a = (i >= pixelsize) ? bytes[i - pixelsize] : 0
|
132
|
+
b = previous_bytes[i]
|
133
|
+
c = (i >= pixelsize) ? previous_bytes[i - pixelsize] : 0
|
134
|
+
p = a + b - c
|
135
|
+
pa = (p - a).abs
|
136
|
+
pb = (p - b).abs
|
137
|
+
pc = (p - c).abs
|
138
|
+
pr = (pa <= pb && pa <= pc) ? a : (pb <= pc ? b : c)
|
139
|
+
bytes[i] = (byte + pr) % 256
|
140
|
+
end
|
141
|
+
bytes
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
module ChunkyPNG
|
2
|
+
class Canvas
|
3
|
+
|
4
|
+
# Methods for encoding a Canvas into a PNG datastream
|
5
|
+
#
|
6
|
+
module PNGEncoding
|
7
|
+
|
8
|
+
def write(io, constraints = {})
|
9
|
+
to_datastream(constraints).write(io)
|
10
|
+
end
|
11
|
+
|
12
|
+
def save(filename, constraints = {})
|
13
|
+
File.open(filename, 'wb') { |io| write(io, constraints) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_blob(constraints = {})
|
17
|
+
to_datastream(constraints).to_blob
|
18
|
+
end
|
19
|
+
|
20
|
+
alias :to_string :to_blob
|
21
|
+
alias :to_s :to_blob
|
22
|
+
|
23
|
+
# Converts this Canvas to a datastream, so that it can be saved as a PNG image.
|
24
|
+
# @param [Hash] constraints The constraints to use when encoding the canvas.
|
25
|
+
def to_datastream(constraints = {})
|
26
|
+
data = encode_png(constraints)
|
27
|
+
ds = Datastream.new
|
28
|
+
ds.header_chunk = Chunk::Header.new(data[:header])
|
29
|
+
ds.palette_chunk = data[:palette_chunk] if data[:palette_chunk]
|
30
|
+
ds.transparency_chunk = data[:transparency_chunk] if data[:transparency_chunk]
|
31
|
+
ds.data_chunks = Chunk::ImageData.split_in_chunks(data[:pixelstream])
|
32
|
+
ds.end_chunk = Chunk::End.new
|
33
|
+
return ds
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def encode_png(constraints = {})
|
39
|
+
encoding = determine_png_encoding(constraints)
|
40
|
+
result = {}
|
41
|
+
result[:header] = { :width => width, :height => height, :color => encoding[:color_mode], :interlace => encoding[:interlace] }
|
42
|
+
|
43
|
+
if encoding[:color_mode] == ChunkyPNG::COLOR_INDEXED
|
44
|
+
result[:palette_chunk] = encoding[:palette].to_plte_chunk
|
45
|
+
result[:transparency_chunk] = encoding[:palette].to_trns_chunk unless encoding[:palette].opaque?
|
46
|
+
end
|
47
|
+
|
48
|
+
result[:pixelstream] = encode_png_pixelstream(encoding[:color_mode], encoding[:palette], encoding[:interlace])
|
49
|
+
return result
|
50
|
+
end
|
51
|
+
|
52
|
+
def determine_png_encoding(constraints = {})
|
53
|
+
encoding = constraints
|
54
|
+
encoding[:palette] ||= palette
|
55
|
+
encoding[:color_mode] ||= encoding[:palette].best_colormode
|
56
|
+
|
57
|
+
encoding[:interlace] = case encoding[:interlace]
|
58
|
+
when nil, false, ChunkyPNG::INTERLACING_NONE then ChunkyPNG::INTERLACING_NONE
|
59
|
+
when true, ChunkyPNG::INTERLACING_ADAM7 then ChunkyPNG::INTERLACING_ADAM7
|
60
|
+
else encoding[:interlace]
|
61
|
+
end
|
62
|
+
|
63
|
+
return encoding
|
64
|
+
end
|
65
|
+
|
66
|
+
def encode_png_pixelstream(color_mode = ChunkyPNG::COLOR_TRUECOLOR, palette = nil, interlace = ChunkyPNG::INTERLACING_NONE)
|
67
|
+
|
68
|
+
if color_mode == ChunkyPNG::COLOR_INDEXED && !palette.can_encode?
|
69
|
+
raise "This palette is not suitable for encoding!"
|
70
|
+
end
|
71
|
+
|
72
|
+
pixel_size = Color.bytesize(color_mode)
|
73
|
+
pixel_encoder = case color_mode
|
74
|
+
when ChunkyPNG::COLOR_TRUECOLOR then lambda { |color| Color.to_truecolor_bytes(color) }
|
75
|
+
when ChunkyPNG::COLOR_TRUECOLOR_ALPHA then lambda { |color| Color.to_truecolor_alpha_bytes(color) }
|
76
|
+
when ChunkyPNG::COLOR_INDEXED then lambda { |color| [palette.index(color)] }
|
77
|
+
when ChunkyPNG::COLOR_GRAYSCALE then lambda { |color| Color.to_grayscale_bytes(color) }
|
78
|
+
when ChunkyPNG::COLOR_GRAYSCALE_ALPHA then lambda { |color| Color.to_grayscale_alpha_bytes(color) }
|
79
|
+
else raise "Cannot encode pixels for this mode: #{color_mode}!"
|
80
|
+
end
|
81
|
+
|
82
|
+
case interlace
|
83
|
+
when ChunkyPNG::INTERLACING_NONE then encode_png_image_without_interlacing(pixel_size, pixel_encoder)
|
84
|
+
when ChunkyPNG::INTERLACING_ADAM7 then encode_png_image_with_interlacing(pixel_size, pixel_encoder)
|
85
|
+
else raise "Unknown interlacing method!"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def encode_png_image_without_interlacing(pixel_size, pixel_encoder)
|
90
|
+
stream = ""
|
91
|
+
encode_png_image_pass_to_stream(stream, pixel_size, pixel_encoder)
|
92
|
+
stream
|
93
|
+
end
|
94
|
+
|
95
|
+
def encode_png_image_with_interlacing(pixel_size, pixel_encoder)
|
96
|
+
stream = ""
|
97
|
+
0.upto(6) do |pass|
|
98
|
+
subcanvas = self.class.adam7_extract_pass(pass, self)
|
99
|
+
subcanvas.encode_png_image_pass_to_stream(stream, pixel_size, pixel_encoder)
|
100
|
+
end
|
101
|
+
stream
|
102
|
+
end
|
103
|
+
|
104
|
+
def encode_png_image_pass_to_stream(stream, pixel_size, pixel_encoder)
|
105
|
+
previous_bytes = Array.new(pixel_size * width, 0)
|
106
|
+
each_scanline do |line|
|
107
|
+
unencoded_bytes = line.map(&pixel_encoder).flatten
|
108
|
+
stream << encode_png_scanline_up(unencoded_bytes, previous_bytes, pixel_size).pack('C*')
|
109
|
+
previous_bytes = unencoded_bytes
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Passes to this canvas of pixel values line by line.
|
114
|
+
# @yield [Array<Fixnum>] An line of fixnums reprsenting pixels
|
115
|
+
def each_scanline(&block)
|
116
|
+
height.times do |i|
|
117
|
+
scanline = pixels[width * i, width]
|
118
|
+
yield(scanline)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def encode_png_scanline(filter, bytes, previous_bytes = nil, pixelsize = 3)
|
123
|
+
case filter
|
124
|
+
when ChunkyPNG::FILTER_NONE then encode_png_scanline_none( bytes, previous_bytes, pixelsize)
|
125
|
+
when ChunkyPNG::FILTER_SUB then encode_png_scanline_sub( bytes, previous_bytes, pixelsize)
|
126
|
+
when ChunkyPNG::FILTER_UP then encode_png_scanline_up( bytes, previous_bytes, pixelsize)
|
127
|
+
when ChunkyPNG::FILTER_AVERAGE then encode_png_scanline_average( bytes, previous_bytes, pixelsize)
|
128
|
+
when ChunkyPNG::FILTER_PAETH then encode_png_scanline_paeth( bytes, previous_bytes, pixelsize)
|
129
|
+
else raise "Unknown filter type"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def encode_png_scanline_none(original_bytes, previous_bytes = nil, pixelsize = 3)
|
134
|
+
[ChunkyPNG::FILTER_NONE] + original_bytes
|
135
|
+
end
|
136
|
+
|
137
|
+
def encode_png_scanline_sub(original_bytes, previous_bytes = nil, pixelsize = 3)
|
138
|
+
encoded_bytes = []
|
139
|
+
original_bytes.length.times do |index|
|
140
|
+
a = (index >= pixelsize) ? original_bytes[index - pixelsize] : 0
|
141
|
+
encoded_bytes[index] = (original_bytes[index] - a) % 256
|
142
|
+
end
|
143
|
+
[ChunkyPNG::FILTER_SUB] + encoded_bytes
|
144
|
+
end
|
145
|
+
|
146
|
+
def encode_png_scanline_up(original_bytes, previous_bytes, pixelsize = 3)
|
147
|
+
encoded_bytes = []
|
148
|
+
original_bytes.length.times do |index|
|
149
|
+
b = previous_bytes[index]
|
150
|
+
encoded_bytes[index] = (original_bytes[index] - b) % 256
|
151
|
+
end
|
152
|
+
[ChunkyPNG::FILTER_UP] + encoded_bytes
|
153
|
+
end
|
154
|
+
|
155
|
+
def encode_png_scanline_average(original_bytes, previous_bytes, pixelsize = 3)
|
156
|
+
encoded_bytes = []
|
157
|
+
original_bytes.length.times do |index|
|
158
|
+
a = (index >= pixelsize) ? original_bytes[index - pixelsize] : 0
|
159
|
+
b = previous_bytes[index]
|
160
|
+
encoded_bytes[index] = (original_bytes[index] - (a + b / 2).floor) % 256
|
161
|
+
end
|
162
|
+
[ChunkyPNG::FILTER_AVERAGE] + encoded_bytes
|
163
|
+
end
|
164
|
+
|
165
|
+
def encode_png_scanline_paeth(original_bytes, previous_bytes, pixelsize = 3)
|
166
|
+
encoded_bytes = []
|
167
|
+
original_bytes.length.times do |i|
|
168
|
+
a = (i >= pixelsize) ? original_bytes[i - pixelsize] : 0
|
169
|
+
b = previous_bytes[i]
|
170
|
+
c = (i >= pixelsize) ? previous_bytes[i - pixelsize] : 0
|
171
|
+
p = a + b - c
|
172
|
+
pa = (p - a).abs
|
173
|
+
pb = (p - b).abs
|
174
|
+
pc = (p - c).abs
|
175
|
+
pr = (pa <= pb && pa <= pc) ? a : (pb <= pc ? b : c)
|
176
|
+
encoded_bytes[i] = (original_bytes[i] - pr) % 256
|
177
|
+
end
|
178
|
+
[ChunkyPNG::FILTER_PAETH] + encoded_bytes
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
data/lib/chunky_png/chunk.rb
CHANGED
@@ -1,52 +1,77 @@
|
|
1
1
|
module ChunkyPNG
|
2
|
-
|
3
|
-
|
2
|
+
|
3
|
+
# A PNG datastream consists of multiple chunks. This module, and the classes
|
4
|
+
# contained within, help with handling these chunks. It supports both
|
5
|
+
# reading and writing chunks.
|
6
|
+
#
|
7
|
+
# All chunck types are instances of the {ChunkyPNG::Chunk::Base} class. For
|
8
|
+
# some chunk types a specialized class is available, e.g. the IHDR chunk is
|
9
|
+
# represented by the {ChunkyPNG::Chunk::Header} class. These specialized
|
10
|
+
# classes help accessing the content of the chunk. All other chunks are
|
11
|
+
# represented by the {ChunkyPNG::Chunk::Generic} class.
|
12
|
+
module Chunk
|
13
|
+
|
14
|
+
# Reads a chunk from an IO stream.
|
15
|
+
#
|
16
|
+
# @param [IO, #read] io The IO stream to read from.
|
17
|
+
# @return [ChunkyPNG::Chung::Base] The loaded chunk instance.
|
4
18
|
def self.read(io)
|
5
19
|
|
6
20
|
length, type = io.read(8).unpack('Na4')
|
7
21
|
content = io.read(length)
|
8
22
|
crc = io.read(4).unpack('N').first
|
9
|
-
|
10
|
-
|
11
|
-
|
23
|
+
|
24
|
+
verify_crc!(type, content, crc)
|
25
|
+
|
12
26
|
CHUNK_TYPES.fetch(type, Generic).read(type, content)
|
13
27
|
end
|
14
28
|
|
29
|
+
# Verifies the CRC of a chunk.
|
30
|
+
# @param [String] type The chunk's type.
|
31
|
+
# @param [String] content The chunk's content.
|
32
|
+
# @param [Fixnum] content The chunk's content.
|
33
|
+
# @raise [RuntimeError] An exception is raised if the found CRC value
|
34
|
+
# is not equal to the expected CRC value.
|
35
|
+
def self.verify_crc!(type, content, found_crc)
|
36
|
+
expected_crc = Zlib.crc32(content, Zlib.crc32(type))
|
37
|
+
raise "Chuck CRC mismatch!" if found_crc != expected_crc
|
38
|
+
end
|
39
|
+
|
15
40
|
class Base
|
16
41
|
attr_accessor :type
|
17
|
-
|
42
|
+
|
18
43
|
def initialize(type, attributes = {})
|
19
44
|
self.type = type
|
20
45
|
attributes.each { |k, v| send("#{k}=", v) }
|
21
46
|
end
|
22
|
-
|
47
|
+
|
23
48
|
def write_with_crc(io, content)
|
24
49
|
io << [content.length].pack('N') << type << content
|
25
50
|
io << [Zlib.crc32(content, Zlib.crc32(type))].pack('N')
|
26
51
|
end
|
27
|
-
|
52
|
+
|
28
53
|
def write(io)
|
29
54
|
write_with_crc(io, content || '')
|
30
55
|
end
|
31
56
|
end
|
32
|
-
|
57
|
+
|
33
58
|
class Generic < Base
|
34
|
-
|
59
|
+
|
35
60
|
attr_accessor :content
|
36
|
-
|
61
|
+
|
37
62
|
def initialize(type, content = '')
|
38
63
|
super(type, :content => content)
|
39
64
|
end
|
40
|
-
|
65
|
+
|
41
66
|
def self.read(type, content)
|
42
67
|
self.new(type, content)
|
43
68
|
end
|
44
69
|
end
|
45
|
-
|
70
|
+
|
46
71
|
class Header < Base
|
47
|
-
|
72
|
+
|
48
73
|
attr_accessor :width, :height, :depth, :color, :compression, :filtering, :interlace
|
49
|
-
|
74
|
+
|
50
75
|
def initialize(attrs = {})
|
51
76
|
super('IHDR', attrs)
|
52
77
|
@depth ||= 8
|
@@ -55,7 +80,7 @@ module ChunkyPNG
|
|
55
80
|
@filtering ||= ChunkyPNG::FILTERING_DEFAULT
|
56
81
|
@interlace ||= ChunkyPNG::INTERLACING_NONE
|
57
82
|
end
|
58
|
-
|
83
|
+
|
59
84
|
def self.read(type, content)
|
60
85
|
fields = content.unpack('NNC5')
|
61
86
|
self.new(:width => fields[0], :height => fields[1], :depth => fields[2], :color => fields[3],
|
@@ -66,17 +91,17 @@ module ChunkyPNG
|
|
66
91
|
[width, height, depth, color, compression, filtering, interlace].pack('NNC5')
|
67
92
|
end
|
68
93
|
end
|
69
|
-
|
94
|
+
|
70
95
|
class End < Base
|
71
96
|
def initialize
|
72
97
|
super('IEND')
|
73
98
|
end
|
74
|
-
|
99
|
+
|
75
100
|
def self.read(type, content)
|
76
101
|
raise 'The IEND chunk should be empty!' if content != ''
|
77
102
|
self.new
|
78
103
|
end
|
79
|
-
|
104
|
+
|
80
105
|
def content
|
81
106
|
''
|
82
107
|
end
|
@@ -84,18 +109,71 @@ module ChunkyPNG
|
|
84
109
|
|
85
110
|
class Palette < Generic
|
86
111
|
end
|
87
|
-
|
112
|
+
|
88
113
|
class Transparency < Generic
|
89
114
|
end
|
90
115
|
|
91
116
|
class ImageData < Generic
|
117
|
+
|
118
|
+
def self.combine_chunks(data_chunks)
|
119
|
+
Zlib::Inflate.inflate(data_chunks.map(&:content).join(''))
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.split_in_chunks(data, chunk_size = 2147483647)
|
123
|
+
streamdata = Zlib::Deflate.deflate(data)
|
124
|
+
# TODO: Split long streamdata over multiple chunks
|
125
|
+
[ ChunkyPNG::Chunk::ImageData.new('IDAT', streamdata) ]
|
126
|
+
end
|
92
127
|
end
|
93
|
-
|
128
|
+
|
129
|
+
class Text < Base
|
130
|
+
|
131
|
+
attr_accessor :keyword, :value
|
132
|
+
|
133
|
+
def initialize(keyword, value)
|
134
|
+
super('tEXt')
|
135
|
+
@keyword, @value = keyword, value
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.read(type, content)
|
139
|
+
keyword, value = content.unpack('Z*a*')
|
140
|
+
new(keyword, value)
|
141
|
+
end
|
142
|
+
|
143
|
+
def content
|
144
|
+
[keyword, value].pack('Z*a*')
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
class CompressedText < Base
|
149
|
+
|
150
|
+
attr_accessor :keyword, :value
|
151
|
+
|
152
|
+
def initialize(keyword, value)
|
153
|
+
super('tEXt')
|
154
|
+
@keyword, @value = keyword, value
|
155
|
+
end
|
156
|
+
|
157
|
+
def self.read(type, content)
|
158
|
+
keyword, compression, value = content.unpack('Z*Ca*')
|
159
|
+
raise "Compression method #{compression.inspect} not supported!" unless compression == ChunkyPNG::COMPRESSION_DEFAULT
|
160
|
+
new(keyword, Zlib::Inflate.inflate(value))
|
161
|
+
end
|
162
|
+
|
163
|
+
def content
|
164
|
+
[keyword, ChunkyPNG::COMPRESSION_DEFAULT, Zlib::Deflate.deflate(value)].pack('Z*Ca*')
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
class InternationalText < Generic
|
169
|
+
# TODO
|
170
|
+
end
|
171
|
+
|
94
172
|
# Maps chunk types to classes.
|
95
173
|
# If a chunk type is not given in this hash, a generic chunk type will be used.
|
96
174
|
CHUNK_TYPES = {
|
97
|
-
'IHDR' => Header, 'IEND' => End, 'IDAT' => ImageData, 'PLTE' => Palette, 'tRNS' => Transparency
|
175
|
+
'IHDR' => Header, 'IEND' => End, 'IDAT' => ImageData, 'PLTE' => Palette, 'tRNS' => Transparency,
|
176
|
+
'tEXt' => Text, 'zTXt' => CompressedText, 'iTXt' => InternationalText
|
98
177
|
}
|
99
|
-
|
100
178
|
end
|
101
179
|
end
|