chunky_png 0.0.5 → 0.5.0
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/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
|