module ChunkyPNG class Canvas # 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 exectly as above, and the result gets combined # before the compression step. # # @see ChunkyPNG::Canvas::PNGDecoding # @see http://www.w3.org/TR/PNG/ The W3C PNG format specification module PNGEncoding # The palette used for encoding the image.This is only in used for images # that get encoded using indexed colors. # @return [ChunkyPNG::Palette] attr_accessor :encoding_palette # Writes the canvas to an IO stream, encoded as a PNG image. # @param [IO] io The output stream to write to. # @param constraints (see ChunkyPNG::Canvas::PNGEncoding#to_datastream) def write(io, constraints = {}) to_datastream(constraints).write(io) end # Writes the canvas to a file, encoded as a PNG image. # @param [String] filname The file to save the PNG image to. # @param constraints (see ChunkyPNG::Canvas::PNGEncoding#to_datastream) def save(filename, constraints = {}) File.open(filename, 'wb') { |io| write(io, constraints) } end # Encoded the canvas to a PNG formatted string. # @param constraints (see ChunkyPNG::Canvas::PNGEncoding#to_datastream) # @return [String] The PNG encoded canvas as string. def to_blob(constraints = {}) to_datastream(constraints).to_blob end alias_method :to_string, :to_blob alias_method :to_s, :to_blob # 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 :fast_rgba for quickly saving images with transparency, # :fast_rgb for quickly saving opaque images, and :best_compression 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. # @return [ChunkyPNG::Datastream] The PNG datastream containing the encoded canvas. # @see ChunkyPNG::Canvas::PNGEncoding#determine_png_encoding 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], :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[:interlace], encoding[:filtering]) ds.data_chunks = Chunk::ImageData.split_in_chunks(data, encoding[:compression]) ds.end_chunk = Chunk::End.new return ds end protected # 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} def determine_png_encoding(constraints = {}) if constraints == :fast_rgb encoding = { :color_mode => ChunkyPNG::COLOR_TRUECOLOR, :compression => Zlib::BEST_SPEED } elsif constraints == :fast_rgba encoding = { :color_mode => ChunkyPNG::COLOR_TRUECOLOR_ALPHA, :compression => Zlib::BEST_SPEED } elsif constraints == :best_compression encoding = { :compression => Zlib::BEST_COMPRESSION, :filtering => ChunkyPNG::FILTER_PAETH } elsif constraints == :good_compression encoding = { :compression => Zlib::BEST_COMPRESSION, :filtering => ChunkyPNG::FILTER_NONE } elsif constraints == :no_compression encoding = { :compression => Zlib::NO_COMPRESSION } else encoding = constraints end # Do not create a pallete 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 = self.palette end else self.encoding_palette = self.palette encoding[:color_mode] ||= encoding_palette.best_colormode end # Use Zlib's default for compression unless otherwise provided. encoding[:compression] ||= Zlib::DEFAULT_COMPRESSION encoding[:interlace] = case encoding[:interlace] when nil, false, ChunkyPNG::INTERLACING_NONE; ChunkyPNG::INTERLACING_NONE when true, ChunkyPNG::INTERLACING_ADAM7; ChunkyPNG::INTERLACING_ADAM7 else encoding[:interlace] end encoding[:filtering] ||= case encoding[:compression] when Zlib::BEST_COMPRESSION; ChunkyPNG::FILTER_PAETH when Zlib::NO_COMPRESSION..Zlib::BEST_SPEED; ChunkyPNG::FILTER_NONE else ChunkyPNG::FILTER_UP end return encoding 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] interlace The interlacing method to use. # @return [String] The PNG encoded canvas as string. def encode_png_pixelstream(color_mode = ChunkyPNG::COLOR_TRUECOLOR, interlace = ChunkyPNG::INTERLACING_NONE, filtering = ChunkyPNG::FILTER_NONE) if color_mode == ChunkyPNG::COLOR_INDEXED && (encoding_palette.nil? || !encoding_palette.can_encode?) raise ChunkyPNG::ExpectationFailed, "This palette is not suitable for encoding!" end case interlace when ChunkyPNG::INTERLACING_NONE; encode_png_image_without_interlacing(color_mode, filtering) when ChunkyPNG::INTERLACING_ADAM7; encode_png_image_with_interlacing(color_mode, filtering) else raise ChunkyPNG::NotSupported, "Unknown interlacing method: #{interlace}!" end 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] filtering The filtering method to use. # @return [String] The PNG encoded canvas as string. def encode_png_image_without_interlacing(color_mode, filtering = ChunkyPNG::FILTER_NONE) stream = ChunkyPNG::Datastream.empty_bytearray encode_png_image_pass_to_stream(stream, color_mode, filtering) stream end # Encodes the canvas according to the PNG format specification with a given color # mode and Adam7 interlacing. # # This method will split the original canva 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] filtering The filtering method to use. # @return [String] The PNG encoded canvas as string. def encode_png_image_with_interlacing(color_mode, filtering = ChunkyPNG::FILTER_NONE) stream = ChunkyPNG::Datastream.empty_bytearray 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, filtering) end stream 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] filtering The filtering method to use. def encode_png_image_pass_to_stream(stream, color_mode, filtering) start_pos = stream.bytesize pixel_size = Color.bytesize(color_mode) line_width = pixel_size * width # Encode the whole image without filtering stream << case color_mode when ChunkyPNG::COLOR_TRUECOLOR; pixels.pack(('x' + ('NX' * width)) * height) when ChunkyPNG::COLOR_TRUECOLOR_ALPHA; pixels.pack("xN#{width}" * height) when ChunkyPNG::COLOR_INDEXED; pixels.map { |p| encoding_palette.index(p) }.pack("xC#{width}" * height) when ChunkyPNG::COLOR_GRAYSCALE; pixels.map { |p| p >> 8 }.pack("xC#{width}" * height) when ChunkyPNG::COLOR_GRAYSCALE_ALPHA; pixels.pack("xn#{width}" * height) else raise ChunkyPNG::NotSupported, "Cannot encode pixels for this mode: #{color_mode}!" end # Determine the filter method filter_method = case filtering when ChunkyPNG::FILTER_SUB; :encode_png_str_scanline_sub when ChunkyPNG::FILTER_UP; :encode_png_str_scanline_up when ChunkyPNG::FILTER_AVERAGE; :encode_png_str_scanline_average when ChunkyPNG::FILTER_PAETH; :encode_png_str_scanline_paeth else nil 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 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 [nil] 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 SUB filtering. This will modify the stream. # @param (see #encode_png_str_scanline_none) # @return [nil] 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 [nil] 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 # Encodes a scanline of a pixelstream using AVERAGE filtering. This will modify the stream. # @param (see #encode_png_str_scanline_none) # @return [nil] 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 using PAETH filtering. This will modify the stream. # @param (see #encode_png_str_scanline_none) # @return [nil] 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 = (pa <= pb && pa <= pc) ? a : (pb <= pc ? b : c) stream.setbyte(pos + i, (stream.getbyte(pos + i) - pr) & 0xff) end stream.setbyte(pos, ChunkyPNG::FILTER_PAETH) end end end end