module ChunkyPNG
class Canvas
# The PNGDecoding contains methods for decoding PNG datastreams to create a Canvas object.
# The datastream can be provided as filename, string or IO object.
module PNGDecoding
def from_blob(str)
from_datastream(ChunkyPNG::Datastream.from_blob(str))
end
alias :from_string :from_blob
def from_file(filename)
from_datastream(ChunkyPNG::Datastream.from_file(filename))
end
def from_io(io)
from_datastream(ChunkyPNG::Datastream.from_io(io))
end
def from_datastream(ds)
raise "Only 8-bit color depth is currently supported by ChunkyPNG!" unless ds.header_chunk.depth == 8
width = ds.header_chunk.width
height = ds.header_chunk.height
color_mode = ds.header_chunk.color
interlace = ds.header_chunk.interlace
palette = ChunkyPNG::Palette.from_chunks(ds.palette_chunk, ds.transparency_chunk)
stream = ChunkyPNG::Chunk::ImageData.combine_chunks(ds.data_chunks)
decode_png_pixelstream(stream, width, height, color_mode, palette, interlace)
end
def decode_png_pixelstream(stream, width, height, color_mode = ChunkyPNG::COLOR_TRUECOLOR, palette = nil, interlace = ChunkyPNG::INTERLACING_NONE)
raise "This palette is not suitable for decoding!" if palette && !palette.can_decode?
pixel_size = Color.bytesize(color_mode)
pixel_decoder = case color_mode
when ChunkyPNG::COLOR_TRUECOLOR then lambda { |bytes| ChunkyPNG::Color.rgb(*bytes) }
when ChunkyPNG::COLOR_TRUECOLOR_ALPHA then lambda { |bytes| ChunkyPNG::Color.rgba(*bytes) }
when ChunkyPNG::COLOR_INDEXED then lambda { |bytes| palette[bytes.first] }
when ChunkyPNG::COLOR_GRAYSCALE then lambda { |bytes| ChunkyPNG::Color.grayscale(*bytes) }
when ChunkyPNG::COLOR_GRAYSCALE_ALPHA then lambda { |bytes| ChunkyPNG::Color.grayscale(*bytes) }
else raise "No suitable pixel decoder found for color mode #{color_mode}!"
end
return case interlace
when ChunkyPNG::INTERLACING_NONE then decode_png_without_interlacing(stream, width, height, pixel_size, pixel_decoder)
when ChunkyPNG::INTERLACING_ADAM7 then decode_png_with_adam7_interlacing(stream, width, height, pixel_size, pixel_decoder)
else raise "Don't know how the handle interlacing method #{interlace}!"
end
end
protected
def decode_png_image_pass(stream, width, height, pixel_size, pixel_decoder, start_pos = 0)
pixels = []
if width > 0
decoded_bytes = Array.new(width * pixel_size, 0)
for line_no in 0...height do
# get bytes of scanline
position = start_pos + line_no * (width * pixel_size + 1)
line_length = width * pixel_size
bytes = stream.unpack("@#{position}CC#{line_length}")
filter = bytes.shift
decoded_bytes = decode_png_scanline(filter, bytes, decoded_bytes, pixel_size)
# decode bytes into colors
decoded_bytes.each_slice(pixel_size) { |bytes| pixels << pixel_decoder.call(bytes) }
end
end
new(width, height, pixels)
end
def decode_png_without_interlacing(stream, width, height, pixel_size, pixel_decoder)
raise "Invalid stream length!" unless stream.length == width * height * pixel_size + height
decode_png_image_pass(stream, width, height, pixel_size, pixel_decoder)
end
def decode_png_with_adam7_interlacing(stream, width, height, pixel_size, pixel_decoder)
start_pos = 0
canvas = ChunkyPNG::Canvas.new(width, height)
0.upto(6) do |pass|
sm_width, sm_height = adam7_pass_size(pass, width, height)
sm = decode_png_image_pass(stream, sm_width, sm_height, pixel_size, pixel_decoder, start_pos)
adam7_merge_pass(pass, canvas, sm)
start_pos += (sm_width * sm_height * pixel_size) + sm_height
end
canvas
end
def decode_png_scanline(filter, bytes, previous_bytes, pixelsize = 3)
case filter
when ChunkyPNG::FILTER_NONE then decode_png_scanline_none( bytes, previous_bytes, pixelsize)
when ChunkyPNG::FILTER_SUB then decode_png_scanline_sub( bytes, previous_bytes, pixelsize)
when ChunkyPNG::FILTER_UP then decode_png_scanline_up( bytes, previous_bytes, pixelsize)
when ChunkyPNG::FILTER_AVERAGE then decode_png_scanline_average( bytes, previous_bytes, pixelsize)
when ChunkyPNG::FILTER_PAETH then decode_png_scanline_paeth( bytes, previous_bytes, pixelsize)
else raise "Unknown filter type"
end
end
def decode_png_scanline_none(bytes, previous_bytes, pixelsize = 3)
bytes
end
def decode_png_scanline_sub(bytes, previous_bytes, pixelsize = 3)
bytes.each_with_index { |b, i| bytes[i] = (b + (i >= pixelsize ? bytes[i-pixelsize] : 0)) % 256 }
bytes
end
def decode_png_scanline_up(bytes, previous_bytes, pixelsize = 3)
bytes.each_with_index { |b, i| bytes[i] = (b + previous_bytes[i]) % 256 }
bytes
end
def decode_png_scanline_average(bytes, previous_bytes, pixelsize = 3)
bytes.each_with_index do |byte, i|
a = (i >= pixelsize) ? bytes[i - pixelsize] : 0
b = previous_bytes[i]
bytes[i] = (byte + (a + b / 2).floor) % 256
end
bytes
end
def decode_png_scanline_paeth(bytes, previous_bytes, pixelsize = 3)
bytes.each_with_index do |byte, i|
a = (i >= pixelsize) ? bytes[i - pixelsize] : 0
b = previous_bytes[i]
c = (i >= pixelsize) ? previous_bytes[i - pixelsize] : 0
p = a + b - c
pa = (p - a).abs
pb = (p - b).abs
pc = (p - c).abs
pr = (pa <= pb && pa <= pc) ? a : (pb <= pc ? b : c)
bytes[i] = (byte + pr) % 256
end
bytes
end
end
end
end