module ChunkyPNG::Canvas::PNGEncoding
Methods for encoding a Canvas instance into a PNG datastream.
Overview of the encoding process:
-
The image is split up in scanlines (i.e. rows of pixels);
-
All pixels are encoded as a pixelstream, based on the color mode.
-
All the pixel bytes in the pixelstream are adjusted using a filtering method if one is specified.
-
Compress the resulting string using deflate compression.
-
Split compressed data over one or more PNG chunks.
-
These chunks should be embedded in a datastream with at least a IHDR and IEND chunk and possibly a PLTE chunk.
For interlaced images, the initial image is first split into 7 subimages. These images get encoded exactly as above, and the result gets combined before the compression step.
@see ChunkyPNG::Canvas::PNGDecoding @see www.w3.org/TR/PNG/ The W3C PNG format specification
Attributes
The palette used for encoding the image.This is only in used for images that get encoded using indexed colors. @return [ChunkyPNG::Palette]
Public Instance Methods
Writes the canvas to a file, encoded as a PNG image. @param [String] filename The file to save the PNG image to. @param constraints (see #to_datastream) @return [void]
# File lib/chunky_png/canvas/png_encoding.rb, line 41 def save(filename, constraints = {}) File.open(filename, "wb") { |io| write(io, constraints) } end
Encoded the canvas to a PNG formatted string. @param constraints (see #to_datastream) @return [String] The PNG encoded canvas as string.
# File lib/chunky_png/canvas/png_encoding.rb, line 48 def to_blob(constraints = {}) to_datastream(constraints).to_blob end
Converts this Canvas to a datastream, so that it can be saved as a PNG image. @param [Hash, Symbol] constraints The constraints to use when encoding the canvas.
This can either be a hash with different constraints, or a symbol which acts as a preset for some constraints. If no constraints are given, ChunkyPNG will decide for itself how to best create the PNG datastream. Supported presets are <tt>:fast_rgba</tt> for quickly saving images with transparency, <tt>:fast_rgb</tt> for quickly saving opaque images, and <tt>:best_compression</tt> to obtain the smallest possible filesize.
@option constraints [Fixnum] :color_mode The color mode to use. Use one of the
ChunkyPNG::COLOR_* constants.
@option constraints [true, false] :interlace Whether to use interlacing. @option constraints [Fixnum] :compression The compression level for Zlib. This can be a
value between 0 and 9, or a Zlib constant like Zlib::BEST_COMPRESSION.
@option constraints [Fixnum] :bit_depth The bit depth to use. This option is only used
for indexed images, in which case it overrides the determined minimal bit depth. For all the other color modes, a bit depth of 8 is used.
@return [ChunkyPNG::Datastream] The PNG datastream containing the encoded canvas. @see #determine_png_encoding
# File lib/chunky_png/canvas/png_encoding.rb, line 73 def to_datastream(constraints = {}) encoding = determine_png_encoding(constraints) ds = Datastream.new ds.header_chunk = Chunk::Header.new( width: width, height: height, color: encoding[:color_mode], depth: encoding[:bit_depth], interlace: encoding[:interlace] ) if encoding[:color_mode] == ChunkyPNG::COLOR_INDEXED ds.palette_chunk = encoding_palette.to_plte_chunk ds.transparency_chunk = encoding_palette.to_trns_chunk unless encoding_palette.opaque? end data = encode_png_pixelstream(encoding[:color_mode], encoding[:bit_depth], encoding[:interlace], encoding[:filtering]) ds.data_chunks = Chunk::ImageData.split_in_chunks(data, encoding[:compression]) ds.end_chunk = Chunk::End.new ds end
Writes the canvas to an IO stream, encoded as a PNG image. @param [IO] io The output stream to write to. @param constraints (see #to_datastream) @return [void]
# File lib/chunky_png/canvas/png_encoding.rb, line 33 def write(io, constraints = {}) to_datastream(constraints).write(io) end
Protected Instance Methods
Determines the best possible PNG encoding variables for this image, by analyzing the colors used for the image.
You can provide constraints for the encoding variables by passing a hash with encoding variables to this method.
@param [Hash, Symbol] constraints The constraints for the encoding. This can be a
Hash or a preset symbol.
@return [Hash] A hash with encoding options for {ChunkyPNG::Canvas::PNGEncoding#to_datastream}
# File lib/chunky_png/canvas/png_encoding.rb, line 106 def determine_png_encoding(constraints = {}) encoding = case constraints when :fast_rgb then {color_mode: ChunkyPNG::COLOR_TRUECOLOR, compression: Zlib::BEST_SPEED} when :fast_rgba then {color_mode: ChunkyPNG::COLOR_TRUECOLOR_ALPHA, compression: Zlib::BEST_SPEED} when :best_compression then {compression: Zlib::BEST_COMPRESSION, filtering: ChunkyPNG::FILTER_PAETH} when :good_compression then {compression: Zlib::BEST_COMPRESSION, filtering: ChunkyPNG::FILTER_NONE} when :no_compression then {compression: Zlib::NO_COMPRESSION} when :black_and_white then {color_mode: ChunkyPNG::COLOR_GRAYSCALE, bit_depth: 1} when Hash then constraints else raise ChunkyPNG::Exception, "Unknown encoding preset: #{constraints.inspect}" end # Do not create a palette when the encoding is given and does not require a palette. if encoding[:color_mode] if encoding[:color_mode] == ChunkyPNG::COLOR_INDEXED self.encoding_palette = palette encoding[:bit_depth] ||= encoding_palette.determine_bit_depth else encoding[:bit_depth] ||= 8 end else self.encoding_palette = palette suggested_color_mode, suggested_bit_depth = encoding_palette.best_color_settings encoding[:color_mode] ||= suggested_color_mode encoding[:bit_depth] ||= suggested_bit_depth end # Use Zlib's default for compression unless otherwise provided. encoding[:compression] ||= Zlib::DEFAULT_COMPRESSION encoding[:interlace] = case encoding[:interlace] when nil, false then ChunkyPNG::INTERLACING_NONE when true then ChunkyPNG::INTERLACING_ADAM7 else encoding[:interlace] end encoding[:filtering] ||= case encoding[:compression] when Zlib::BEST_COMPRESSION then ChunkyPNG::FILTER_PAETH when Zlib::NO_COMPRESSION..Zlib::BEST_SPEED then ChunkyPNG::FILTER_NONE else ChunkyPNG::FILTER_UP end encoding end
Encodes the canvas to a stream, in a given color mode. @param [String] stream The stream to write to. @param [Integer] color_mode The color mode to use for encoding. @param [Integer] bit_depth The bit depth of the image. @param [Integer] filtering The filtering method to use.
# File lib/chunky_png/canvas/png_encoding.rb, line 205 def encode_png_image_pass_to_stream(stream, color_mode, bit_depth, filtering) start_pos = stream.bytesize pixel_size = Color.pixel_bytesize(color_mode) line_width = Color.scanline_bytesize(color_mode, bit_depth, width) # Determine the filter method encode_method = encode_png_pixels_to_scanline_method(color_mode, bit_depth) filter_method = case filtering when ChunkyPNG::FILTER_NONE then nil when ChunkyPNG::FILTER_SUB then :encode_png_str_scanline_sub when ChunkyPNG::FILTER_UP then :encode_png_str_scanline_up when ChunkyPNG::FILTER_AVERAGE then :encode_png_str_scanline_average when ChunkyPNG::FILTER_PAETH then :encode_png_str_scanline_paeth else raise ArgumentError, "Filtering method #{filtering} is not supported" end 0.upto(height - 1) do |y| stream << send(encode_method, row(y)) end # Now, apply filtering if any if filter_method (height - 1).downto(0) do |y| pos = start_pos + y * (line_width + 1) prev_pos = y == 0 ? nil : pos - (line_width + 1) send(filter_method, stream, pos, prev_pos, line_width, pixel_size) end end end
Encodes the canvas according to the PNG format specification with a given color mode and Adam7 interlacing.
This method will split the original canvas in 7 smaller canvases and encode them one by one, concatenating the resulting strings.
@param [Integer] color_mode The color mode to use for encoding. @param [Integer] bit_depth The bit depth of the image. @param [Integer] filtering The filtering method to use. @return [String] The PNG encoded canvas as string.
# File lib/chunky_png/canvas/png_encoding.rb, line 190 def encode_png_image_with_interlacing(color_mode, bit_depth = 8, filtering = ChunkyPNG::FILTER_NONE) stream = "".b 0.upto(6) do |pass| subcanvas = self.class.adam7_extract_pass(pass, self) subcanvas.encoding_palette = encoding_palette subcanvas.encode_png_image_pass_to_stream(stream, color_mode, bit_depth, filtering) end stream end
Encodes the canvas according to the PNG format specification with a given color mode. @param [Integer] color_mode The color mode to use for encoding. @param [Integer] bit_depth The bit depth of the image. @param [Integer] filtering The filtering method to use. @return [String] The PNG encoded canvas as string.
# File lib/chunky_png/canvas/png_encoding.rb, line 174 def encode_png_image_without_interlacing(color_mode, bit_depth = 8, filtering = ChunkyPNG::FILTER_NONE) stream = "".b encode_png_image_pass_to_stream(stream, color_mode, bit_depth, filtering) stream end
Encodes a line of pixels using 1-bit grayscale mode. @param [Array<Integer>] pixels A row of pixels of the original image. @return [String] The encoded scanline as binary string
# File lib/chunky_png/canvas/png_encoding.rb, line 306 def encode_png_pixels_to_scanline_grayscale_1bit(pixels) chars = [] pixels.each_slice(8) do |p1, p2, p3, p4, p5, p6, p7, p8| chars << ((p1.nil? ? 0 : (p1 & 0x0000ffff) >> 15 << 7) | (p2.nil? ? 0 : (p2 & 0x0000ffff) >> 15 << 6) | (p3.nil? ? 0 : (p3 & 0x0000ffff) >> 15 << 5) | (p4.nil? ? 0 : (p4 & 0x0000ffff) >> 15 << 4) | (p5.nil? ? 0 : (p5 & 0x0000ffff) >> 15 << 3) | (p6.nil? ? 0 : (p6 & 0x0000ffff) >> 15 << 2) | (p7.nil? ? 0 : (p7 & 0x0000ffff) >> 15 << 1) | (p8.nil? ? 0 : (p8 & 0x0000ffff) >> 15)) end chars.pack("xC*") end
Encodes a line of pixels using 2-bit grayscale mode. @param [Array<Integer>] pixels A row of pixels of the original image. @return [String] The encoded scanline as binary string
# File lib/chunky_png/canvas/png_encoding.rb, line 324 def encode_png_pixels_to_scanline_grayscale_2bit(pixels) chars = [] pixels.each_slice(4) do |p1, p2, p3, p4| chars << ((p1.nil? ? 0 : (p1 & 0x0000ffff) >> 14 << 6) | (p2.nil? ? 0 : (p2 & 0x0000ffff) >> 14 << 4) | (p3.nil? ? 0 : (p3 & 0x0000ffff) >> 14 << 2) | (p4.nil? ? 0 : (p4 & 0x0000ffff) >> 14)) end chars.pack("xC*") end
Encodes a line of pixels using 2-bit grayscale mode. @param [Array<Integer>] pixels A row of pixels of the original image. @return [String] The encoded scanline as binary string
# File lib/chunky_png/canvas/png_encoding.rb, line 338 def encode_png_pixels_to_scanline_grayscale_4bit(pixels) chars = [] pixels.each_slice(2) do |p1, p2| chars << ((p1.nil? ? 0 : ((p1 & 0x0000ffff) >> 12) << 4) | (p2.nil? ? 0 : ((p2 & 0x0000ffff) >> 12))) end chars.pack("xC*") end
Encodes a line of pixels using 8-bit grayscale mode. @param [Array<Integer>] pixels A row of pixels of the original image. @return [String] The encoded scanline as binary string
# File lib/chunky_png/canvas/png_encoding.rb, line 349 def encode_png_pixels_to_scanline_grayscale_8bit(pixels) pixels.map { |p| p >> 8 }.pack("xC#{width}") end
Encodes a line of pixels using 8-bit grayscale alpha mode. @param [Array<Integer>] pixels A row of pixels of the original image. @return [String] The encoded scanline as binary string
# File lib/chunky_png/canvas/png_encoding.rb, line 356 def encode_png_pixels_to_scanline_grayscale_alpha_8bit(pixels) pixels.pack("xn#{width}") end
Encodes a line of pixels using 1-bit indexed mode. @param [Array<Integer>] pixels A row of pixels of the original image. @return [String] The encoded scanline as binary string
# File lib/chunky_png/canvas/png_encoding.rb, line 252 def encode_png_pixels_to_scanline_indexed_1bit(pixels) chars = [] pixels.each_slice(8) do |p1, p2, p3, p4, p5, p6, p7, p8| chars << ( (encoding_palette.index(p1) << 7) | (encoding_palette.index(p2) << 6) | (encoding_palette.index(p3) << 5) | (encoding_palette.index(p4) << 4) | (encoding_palette.index(p5) << 3) | (encoding_palette.index(p6) << 2) | (encoding_palette.index(p7) << 1) | encoding_palette.index(p8) ) end chars.pack("xC*") end
Encodes a line of pixels using 2-bit indexed mode. @param [Array<Integer>] pixels A row of pixels of the original image. @return [String] The encoded scanline as binary string
# File lib/chunky_png/canvas/png_encoding.rb, line 272 def encode_png_pixels_to_scanline_indexed_2bit(pixels) chars = [] pixels.each_slice(4) do |p1, p2, p3, p4| chars << ( (encoding_palette.index(p1) << 6) | (encoding_palette.index(p2) << 4) | (encoding_palette.index(p3) << 2) | encoding_palette.index(p4) ) end chars.pack("xC*") end
Encodes a line of pixels using 4-bit indexed mode. @param [Array<Integer>] pixels A row of pixels of the original image. @return [String] The encoded scanline as binary string
# File lib/chunky_png/canvas/png_encoding.rb, line 288 def encode_png_pixels_to_scanline_indexed_4bit(pixels) chars = [] pixels.each_slice(2) do |p1, p2| chars << ((encoding_palette.index(p1) << 4) | encoding_palette.index(p2)) end chars.pack("xC*") end
Encodes a line of pixels using 8-bit indexed mode. @param [Array<Integer>] pixels A row of pixels of the original image. @return [String] The encoded scanline as binary string
# File lib/chunky_png/canvas/png_encoding.rb, line 299 def encode_png_pixels_to_scanline_indexed_8bit(pixels) pixels.map { |p| encoding_palette.index(p) }.pack("xC#{width}") end
Returns the method name to use to decode scanlines into pixels. @param [Integer] color_mode The color mode of the image. @param [Integer] depth The bit depth of the image. @return [Symbol] The method name to use for decoding, to be called on the canvas class. @raise [ChunkyPNG::NotSupported] when the color_mode and/or bit depth is not supported.
# File lib/chunky_png/canvas/png_encoding.rb, line 365 def encode_png_pixels_to_scanline_method(color_mode, depth) encoder_method = case color_mode when ChunkyPNG::COLOR_TRUECOLOR then :"encode_png_pixels_to_scanline_truecolor_#{depth}bit" when ChunkyPNG::COLOR_TRUECOLOR_ALPHA then :"encode_png_pixels_to_scanline_truecolor_alpha_#{depth}bit" when ChunkyPNG::COLOR_INDEXED then :"encode_png_pixels_to_scanline_indexed_#{depth}bit" when ChunkyPNG::COLOR_GRAYSCALE then :"encode_png_pixels_to_scanline_grayscale_#{depth}bit" when ChunkyPNG::COLOR_GRAYSCALE_ALPHA then :"encode_png_pixels_to_scanline_grayscale_alpha_#{depth}bit" end raise ChunkyPNG::NotSupported, "No encoder found for color mode #{color_mode} and #{depth}-bit depth!" unless respond_to?(encoder_method, true) encoder_method end
Encodes a line of pixels using 8-bit truecolor mode. @param [Array<Integer>] pixels A row of pixels of the original image. @return [String] The encoded scanline as binary string
# File lib/chunky_png/canvas/png_encoding.rb, line 238 def encode_png_pixels_to_scanline_truecolor_8bit(pixels) pixels.pack("x" + ("NX" * width)) end
Encodes a line of pixels using 8-bit truecolor alpha mode. @param [Array<Integer>] pixels A row of pixels of the original image. @return [String] The encoded scanline as binary string
# File lib/chunky_png/canvas/png_encoding.rb, line 245 def encode_png_pixels_to_scanline_truecolor_alpha_8bit(pixels) pixels.pack("xN#{width}") end
Encodes the canvas according to the PNG format specification with a given color mode, possibly with interlacing. @param [Integer] color_mode The color mode to use for encoding. @param [Integer] bit_depth The bit depth of the image. @param [Integer] interlace The interlacing method to use. @return [String] The PNG encoded canvas as string.
# File lib/chunky_png/canvas/png_encoding.rb, line 156 def encode_png_pixelstream(color_mode = ChunkyPNG::COLOR_TRUECOLOR, bit_depth = 8, interlace = ChunkyPNG::INTERLACING_NONE, filtering = ChunkyPNG::FILTER_NONE) if color_mode == ChunkyPNG::COLOR_INDEXED raise ChunkyPNG::ExpectationFailed, "This palette is not suitable for encoding!" if encoding_palette.nil? || !encoding_palette.can_encode? raise ChunkyPNG::ExpectationFailed, "This palette has too many colors!" if encoding_palette.size > (1 << bit_depth) end case interlace when ChunkyPNG::INTERLACING_NONE then encode_png_image_without_interlacing(color_mode, bit_depth, filtering) when ChunkyPNG::INTERLACING_ADAM7 then encode_png_image_with_interlacing(color_mode, bit_depth, filtering) else raise ChunkyPNG::NotSupported, "Unknown interlacing method: #{interlace}!" end end
Encodes a scanline of a pixelstream using AVERAGE filtering. This will modify the stream. @param (see encode_png_str_scanline_none) @return [void]
# File lib/chunky_png/canvas/png_encoding.rb, line 416 def encode_png_str_scanline_average(stream, pos, prev_pos, line_width, pixel_size) line_width.downto(1) do |i| a = i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0 b = prev_pos ? stream.getbyte(prev_pos + i) : 0 stream.setbyte(pos + i, (stream.getbyte(pos + i) - ((a + b) >> 1)) & 0xff) end stream.setbyte(pos, ChunkyPNG::FILTER_AVERAGE) end
Encodes a scanline of a pixelstream without filtering. This is a no-op.
@param [String] stream The pixelstream to work on. This string will be
modified. @param [Integer] pos The starting position of the scanline.
@param [Integer, nil] prev_pos The starting position of the previous
scanline. nil
if
this is the first line.
@param [Integer] line_width The number of bytes in this scanline, without counting the filtering
method byte.
@param [Integer] pixel_size The number of bytes used per pixel. @return [void]
# File lib/chunky_png/canvas/png_encoding.rb, line 387 def encode_png_str_scanline_none(stream, pos, prev_pos, line_width, pixel_size) # noop - this method shouldn't get called at all. end
Encodes a scanline of a pixelstream using PAETH filtering. This will modify the stream. @param (see encode_png_str_scanline_none) @return [void]
# File lib/chunky_png/canvas/png_encoding.rb, line 428 def encode_png_str_scanline_paeth(stream, pos, prev_pos, line_width, pixel_size) line_width.downto(1) do |i| a = i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0 b = prev_pos ? stream.getbyte(prev_pos + i) : 0 c = prev_pos && i > pixel_size ? stream.getbyte(prev_pos + i - pixel_size) : 0 p = a + b - c pa = (p - a).abs pb = (p - b).abs pc = (p - c).abs pr = if pa <= pb && pa <= pc a else pb <= pc ? b : c end stream.setbyte(pos + i, (stream.getbyte(pos + i) - pr) & 0xff) end stream.setbyte(pos, ChunkyPNG::FILTER_PAETH) end
Encodes a scanline of a pixelstream using SUB filtering. This will modify the stream. @param (see encode_png_str_scanline_none) @return [void]
# File lib/chunky_png/canvas/png_encoding.rb, line 394 def encode_png_str_scanline_sub(stream, pos, prev_pos, line_width, pixel_size) line_width.downto(1) do |i| a = i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0 stream.setbyte(pos + i, (stream.getbyte(pos + i) - a) & 0xff) end stream.setbyte(pos, ChunkyPNG::FILTER_SUB) end
Encodes a scanline of a pixelstream using UP filtering. This will modify the stream. @param (see encode_png_str_scanline_none) @return [void]
# File lib/chunky_png/canvas/png_encoding.rb, line 405 def encode_png_str_scanline_up(stream, pos, prev_pos, line_width, pixel_size) line_width.downto(1) do |i| b = prev_pos ? stream.getbyte(prev_pos + i) : 0 stream.setbyte(pos + i, (stream.getbyte(pos + i) - b) & 0xff) end stream.setbyte(pos, ChunkyPNG::FILTER_UP) end