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 ChunkyPNG::Canvas::PNGEncoding#to_datastream
) @return [void]
# File lib/chunky_png/canvas/png_encoding.rb 42 def save(filename, constraints = {}) 43 File.open(filename, 'wb') { |io| write(io, constraints) } 44 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.
# File lib/chunky_png/canvas/png_encoding.rb 49 def to_blob(constraints = {}) 50 to_datastream(constraints).to_blob 51 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 ChunkyPNG::Canvas::PNGEncoding#determine_png_encoding
# File lib/chunky_png/canvas/png_encoding.rb 74 def to_datastream(constraints = {}) 75 encoding = determine_png_encoding(constraints) 76 77 ds = Datastream.new 78 ds.header_chunk = Chunk::Header.new(:width => width, :height => height, 79 :color => encoding[:color_mode], :depth => encoding[:bit_depth], :interlace => encoding[:interlace]) 80 81 if encoding[:color_mode] == ChunkyPNG::COLOR_INDEXED 82 ds.palette_chunk = encoding_palette.to_plte_chunk 83 ds.transparency_chunk = encoding_palette.to_trns_chunk unless encoding_palette.opaque? 84 end 85 data = encode_png_pixelstream(encoding[:color_mode], encoding[:bit_depth], encoding[:interlace], encoding[:filtering]) 86 ds.data_chunks = Chunk::ImageData.split_in_chunks(data, encoding[:compression]) 87 ds.end_chunk = Chunk::End.new 88 return ds 89 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 ChunkyPNG::Canvas::PNGEncoding#to_datastream
) @return [void]
# File lib/chunky_png/canvas/png_encoding.rb 34 def write(io, constraints = {}) 35 to_datastream(constraints).write(io) 36 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 102 def determine_png_encoding(constraints = {}) 103 104 encoding = case constraints 105 when :fast_rgb; { :color_mode => ChunkyPNG::COLOR_TRUECOLOR, :compression => Zlib::BEST_SPEED } 106 when :fast_rgba; { :color_mode => ChunkyPNG::COLOR_TRUECOLOR_ALPHA, :compression => Zlib::BEST_SPEED } 107 when :best_compression; { :compression => Zlib::BEST_COMPRESSION, :filtering => ChunkyPNG::FILTER_PAETH } 108 when :good_compression; { :compression => Zlib::BEST_COMPRESSION, :filtering => ChunkyPNG::FILTER_NONE } 109 when :no_compression; { :compression => Zlib::NO_COMPRESSION } 110 when :black_and_white; { :color_mode => ChunkyPNG::COLOR_GRAYSCALE, :bit_depth => 1 } 111 when Hash; constraints 112 else raise ChunkyPNG::Exception, "Unknown encoding preset: #{constraints.inspect}" 113 end 114 115 # Do not create a palette when the encoding is given and does not require a palette. 116 if encoding[:color_mode] 117 if encoding[:color_mode] == ChunkyPNG::COLOR_INDEXED 118 self.encoding_palette = self.palette 119 encoding[:bit_depth] ||= self.encoding_palette.determine_bit_depth 120 else 121 encoding[:bit_depth] ||= 8 122 end 123 else 124 self.encoding_palette = self.palette 125 suggested_color_mode, suggested_bit_depth = encoding_palette.best_color_settings 126 encoding[:color_mode] ||= suggested_color_mode 127 encoding[:bit_depth] ||= suggested_bit_depth 128 end 129 130 # Use Zlib's default for compression unless otherwise provided. 131 encoding[:compression] ||= Zlib::DEFAULT_COMPRESSION 132 133 encoding[:interlace] = case encoding[:interlace] 134 when nil, false, ChunkyPNG::INTERLACING_NONE; ChunkyPNG::INTERLACING_NONE 135 when true, ChunkyPNG::INTERLACING_ADAM7; ChunkyPNG::INTERLACING_ADAM7 136 else encoding[:interlace] 137 end 138 139 encoding[:filtering] ||= case encoding[:compression] 140 when Zlib::BEST_COMPRESSION; ChunkyPNG::FILTER_PAETH 141 when Zlib::NO_COMPRESSION..Zlib::BEST_SPEED; ChunkyPNG::FILTER_NONE 142 else ChunkyPNG::FILTER_UP 143 end 144 return encoding 145 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 203 def encode_png_image_pass_to_stream(stream, color_mode, bit_depth, filtering) 204 205 start_pos = stream.bytesize 206 pixel_size = Color.pixel_bytesize(color_mode) 207 line_width = Color.scanline_bytesize(color_mode, bit_depth, width) 208 209 # Determine the filter method 210 encode_method = encode_png_pixels_to_scanline_method(color_mode, bit_depth) 211 filter_method = case filtering 212 when ChunkyPNG::FILTER_SUB; :encode_png_str_scanline_sub 213 when ChunkyPNG::FILTER_UP; :encode_png_str_scanline_up 214 when ChunkyPNG::FILTER_AVERAGE; :encode_png_str_scanline_average 215 when ChunkyPNG::FILTER_PAETH; :encode_png_str_scanline_paeth 216 else nil 217 end 218 219 0.upto(height - 1) do |y| 220 stream << send(encode_method, row(y)) 221 end 222 223 # Now, apply filtering if any 224 if filter_method 225 (height - 1).downto(0) do |y| 226 pos = start_pos + y * (line_width + 1) 227 prev_pos = (y == 0) ? nil : pos - (line_width + 1) 228 send(filter_method, stream, pos, prev_pos, line_width, pixel_size) 229 end 230 end 231 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 188 def encode_png_image_with_interlacing(color_mode, bit_depth = 8, filtering = ChunkyPNG::FILTER_NONE) 189 stream = ChunkyPNG::Datastream.empty_bytearray 190 0.upto(6) do |pass| 191 subcanvas = self.class.adam7_extract_pass(pass, self) 192 subcanvas.encoding_palette = encoding_palette 193 subcanvas.encode_png_image_pass_to_stream(stream, color_mode, bit_depth, filtering) 194 end 195 stream 196 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 172 def encode_png_image_without_interlacing(color_mode, bit_depth = 8, filtering = ChunkyPNG::FILTER_NONE) 173 stream = ChunkyPNG::Datastream.empty_bytearray 174 encode_png_image_pass_to_stream(stream, color_mode, bit_depth, filtering) 175 stream 176 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 300 def encode_png_pixels_to_scanline_grayscale_1bit(pixels) 301 chars = [] 302 pixels.each_slice(8) do |p1, p2, p3, p4, p5, p6, p7, p8| 303 chars << ((p1.nil? ? 0 : (p1 & 0x0000ffff) >> 15 << 7) | 304 (p2.nil? ? 0 : (p2 & 0x0000ffff) >> 15 << 6) | 305 (p3.nil? ? 0 : (p3 & 0x0000ffff) >> 15 << 5) | 306 (p4.nil? ? 0 : (p4 & 0x0000ffff) >> 15 << 4) | 307 (p5.nil? ? 0 : (p5 & 0x0000ffff) >> 15 << 3) | 308 (p6.nil? ? 0 : (p6 & 0x0000ffff) >> 15 << 2) | 309 (p7.nil? ? 0 : (p7 & 0x0000ffff) >> 15 << 1) | 310 (p8.nil? ? 0 : (p8 & 0x0000ffff) >> 15)) 311 end 312 chars.pack('xC*') 313 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 318 def encode_png_pixels_to_scanline_grayscale_2bit(pixels) 319 chars = [] 320 pixels.each_slice(4) do |p1, p2, p3, p4| 321 chars << ((p1.nil? ? 0 : (p1 & 0x0000ffff) >> 14 << 6) | 322 (p2.nil? ? 0 : (p2 & 0x0000ffff) >> 14 << 4) | 323 (p3.nil? ? 0 : (p3 & 0x0000ffff) >> 14 << 2) | 324 (p4.nil? ? 0 : (p4 & 0x0000ffff) >> 14)) 325 end 326 chars.pack('xC*') 327 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 332 def encode_png_pixels_to_scanline_grayscale_4bit(pixels) 333 chars = [] 334 pixels.each_slice(2) do |p1, p2| 335 chars << ((p1.nil? ? 0 : ((p1 & 0x0000ffff) >> 12) << 4) | (p2.nil? ? 0 : ((p2 & 0x0000ffff) >> 12))) 336 end 337 chars.pack('xC*') 338 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 343 def encode_png_pixels_to_scanline_grayscale_8bit(pixels) 344 pixels.map { |p| p >> 8 }.pack("xC#{width}") 345 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 350 def encode_png_pixels_to_scanline_grayscale_alpha_8bit(pixels) 351 pixels.pack("xn#{width}") 352 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 250 def encode_png_pixels_to_scanline_indexed_1bit(pixels) 251 chars = [] 252 pixels.each_slice(8) do |p1, p2, p3, p4, p5, p6, p7, p8| 253 chars << ((encoding_palette.index(p1) << 7) | 254 (encoding_palette.index(p2) << 6) | 255 (encoding_palette.index(p3) << 5) | 256 (encoding_palette.index(p4) << 4) | 257 (encoding_palette.index(p5) << 3) | 258 (encoding_palette.index(p6) << 2) | 259 (encoding_palette.index(p7) << 1) | 260 (encoding_palette.index(p8))) 261 end 262 chars.pack('xC*') 263 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 268 def encode_png_pixels_to_scanline_indexed_2bit(pixels) 269 chars = [] 270 pixels.each_slice(4) do |p1, p2, p3, p4| 271 chars << ((encoding_palette.index(p1) << 6) | 272 (encoding_palette.index(p2) << 4) | 273 (encoding_palette.index(p3) << 2) | 274 (encoding_palette.index(p4))) 275 end 276 chars.pack('xC*') 277 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 282 def encode_png_pixels_to_scanline_indexed_4bit(pixels) 283 chars = [] 284 pixels.each_slice(2) do |p1, p2| 285 chars << ((encoding_palette.index(p1) << 4) | (encoding_palette.index(p2))) 286 end 287 chars.pack('xC*') 288 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 293 def encode_png_pixels_to_scanline_indexed_8bit(pixels) 294 pixels.map { |p| encoding_palette.index(p) }.pack("xC#{width}") 295 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 360 def encode_png_pixels_to_scanline_method(color_mode, depth) 361 encoder_method = case color_mode 362 when ChunkyPNG::COLOR_TRUECOLOR; :"encode_png_pixels_to_scanline_truecolor_#{depth}bit" 363 when ChunkyPNG::COLOR_TRUECOLOR_ALPHA; :"encode_png_pixels_to_scanline_truecolor_alpha_#{depth}bit" 364 when ChunkyPNG::COLOR_INDEXED; :"encode_png_pixels_to_scanline_indexed_#{depth}bit" 365 when ChunkyPNG::COLOR_GRAYSCALE; :"encode_png_pixels_to_scanline_grayscale_#{depth}bit" 366 when ChunkyPNG::COLOR_GRAYSCALE_ALPHA; :"encode_png_pixels_to_scanline_grayscale_alpha_#{depth}bit" 367 else nil 368 end 369 370 raise ChunkyPNG::NotSupported, "No encoder found for color mode #{color_mode} and #{depth}-bit depth!" unless respond_to?(encoder_method, true) 371 encoder_method 372 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 236 def encode_png_pixels_to_scanline_truecolor_8bit(pixels) 237 pixels.pack('x' + ('NX' * width)) 238 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 243 def encode_png_pixels_to_scanline_truecolor_alpha_8bit(pixels) 244 pixels.pack("xN#{width}") 245 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 153 def encode_png_pixelstream(color_mode = ChunkyPNG::COLOR_TRUECOLOR, bit_depth = 8, interlace = ChunkyPNG::INTERLACING_NONE, filtering = ChunkyPNG::FILTER_NONE) 154 155 if color_mode == ChunkyPNG::COLOR_INDEXED 156 raise ChunkyPNG::ExpectationFailed, "This palette is not suitable for encoding!" if encoding_palette.nil? || !encoding_palette.can_encode? 157 raise ChunkyPNG::ExpectationFailed, "This palette has too many colors!" if encoding_palette.size > (1 << bit_depth) 158 end 159 160 case interlace 161 when ChunkyPNG::INTERLACING_NONE; encode_png_image_without_interlacing(color_mode, bit_depth, filtering) 162 when ChunkyPNG::INTERLACING_ADAM7; encode_png_image_with_interlacing(color_mode, bit_depth, filtering) 163 else raise ChunkyPNG::NotSupported, "Unknown interlacing method: #{interlace}!" 164 end 165 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 414 def encode_png_str_scanline_average(stream, pos, prev_pos, line_width, pixel_size) 415 line_width.downto(1) do |i| 416 a = (i > pixel_size) ? stream.getbyte(pos + i - pixel_size) : 0 417 b = prev_pos ? stream.getbyte(prev_pos + i) : 0 418 stream.setbyte(pos + i, (stream.getbyte(pos + i) - ((a + b) >> 1)) & 0xff) 419 end 420 stream.setbyte(pos, ChunkyPNG::FILTER_AVERAGE) 421 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 385 def encode_png_str_scanline_none(stream, pos, prev_pos, line_width, pixel_size) 386 # noop - this method shouldn't get called at all. 387 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 426 def encode_png_str_scanline_paeth(stream, pos, prev_pos, line_width, pixel_size) 427 line_width.downto(1) do |i| 428 a = (i > pixel_size) ? stream.getbyte(pos + i - pixel_size) : 0 429 b = (prev_pos) ? stream.getbyte(prev_pos + i) : 0 430 c = (prev_pos && i > pixel_size) ? stream.getbyte(prev_pos + i - pixel_size) : 0 431 p = a + b - c 432 pa = (p - a).abs 433 pb = (p - b).abs 434 pc = (p - c).abs 435 pr = (pa <= pb && pa <= pc) ? a : (pb <= pc ? b : c) 436 stream.setbyte(pos + i, (stream.getbyte(pos + i) - pr) & 0xff) 437 end 438 stream.setbyte(pos, ChunkyPNG::FILTER_PAETH) 439 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 392 def encode_png_str_scanline_sub(stream, pos, prev_pos, line_width, pixel_size) 393 line_width.downto(1) do |i| 394 a = (i > pixel_size) ? stream.getbyte(pos + i - pixel_size) : 0 395 stream.setbyte(pos + i, (stream.getbyte(pos + i) - a) & 0xff) 396 end 397 stream.setbyte(pos, ChunkyPNG::FILTER_SUB) 398 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 403 def encode_png_str_scanline_up(stream, pos, prev_pos, line_width, pixel_size) 404 line_width.downto(1) do |i| 405 b = prev_pos ? stream.getbyte(prev_pos + i) : 0 406 stream.setbyte(pos + i, (stream.getbyte(pos + i) - b) & 0xff) 407 end 408 stream.setbyte(pos, ChunkyPNG::FILTER_UP) 409 end