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 40 def save(filename, constraints = {}) 41 File.open(filename, "wb") { |io| write(io, constraints) } 42 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 47 def to_blob(constraints = {}) 48 to_datastream(constraints).to_blob 49 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 72 def to_datastream(constraints = {}) 73 encoding = determine_png_encoding(constraints) 74 75 ds = Datastream.new 76 ds.header_chunk = Chunk::Header.new( 77 width: width, 78 height: height, 79 color: encoding[:color_mode], 80 depth: encoding[:bit_depth], 81 interlace: encoding[:interlace] 82 ) 83 84 if encoding[:color_mode] == ChunkyPNG::COLOR_INDEXED 85 ds.palette_chunk = encoding_palette.to_plte_chunk 86 ds.transparency_chunk = encoding_palette.to_trns_chunk unless encoding_palette.opaque? 87 end 88 data = encode_png_pixelstream(encoding[:color_mode], encoding[:bit_depth], encoding[:interlace], encoding[:filtering]) 89 ds.data_chunks = Chunk::ImageData.split_in_chunks(data, encoding[:compression]) 90 ds.end_chunk = Chunk::End.new 91 ds 92 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 32 def write(io, constraints = {}) 33 to_datastream(constraints).write(io) 34 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 105 def determine_png_encoding(constraints = {}) 106 encoding = case constraints 107 when :fast_rgb then {color_mode: ChunkyPNG::COLOR_TRUECOLOR, compression: Zlib::BEST_SPEED} 108 when :fast_rgba then {color_mode: ChunkyPNG::COLOR_TRUECOLOR_ALPHA, compression: Zlib::BEST_SPEED} 109 when :best_compression then {compression: Zlib::BEST_COMPRESSION, filtering: ChunkyPNG::FILTER_PAETH} 110 when :good_compression then {compression: Zlib::BEST_COMPRESSION, filtering: ChunkyPNG::FILTER_NONE} 111 when :no_compression then {compression: Zlib::NO_COMPRESSION} 112 when :black_and_white then {color_mode: ChunkyPNG::COLOR_GRAYSCALE, bit_depth: 1} 113 when Hash then constraints 114 else raise ChunkyPNG::Exception, "Unknown encoding preset: #{constraints.inspect}" 115 end 116 117 # Do not create a palette when the encoding is given and does not require a palette. 118 if encoding[:color_mode] 119 if encoding[:color_mode] == ChunkyPNG::COLOR_INDEXED 120 self.encoding_palette = palette 121 encoding[:bit_depth] ||= encoding_palette.determine_bit_depth 122 else 123 encoding[:bit_depth] ||= 8 124 end 125 else 126 self.encoding_palette = palette 127 suggested_color_mode, suggested_bit_depth = encoding_palette.best_color_settings 128 encoding[:color_mode] ||= suggested_color_mode 129 encoding[:bit_depth] ||= suggested_bit_depth 130 end 131 132 # Use Zlib's default for compression unless otherwise provided. 133 encoding[:compression] ||= Zlib::DEFAULT_COMPRESSION 134 135 encoding[:interlace] = case encoding[:interlace] 136 when nil, false then ChunkyPNG::INTERLACING_NONE 137 when true then ChunkyPNG::INTERLACING_ADAM7 138 else encoding[:interlace] 139 end 140 141 encoding[:filtering] ||= case encoding[:compression] 142 when Zlib::BEST_COMPRESSION then ChunkyPNG::FILTER_PAETH 143 when Zlib::NO_COMPRESSION..Zlib::BEST_SPEED then ChunkyPNG::FILTER_NONE 144 else ChunkyPNG::FILTER_UP 145 end 146 encoding 147 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 204 def encode_png_image_pass_to_stream(stream, color_mode, bit_depth, filtering) 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_NONE then nil 213 when ChunkyPNG::FILTER_SUB then :encode_png_str_scanline_sub 214 when ChunkyPNG::FILTER_UP then :encode_png_str_scanline_up 215 when ChunkyPNG::FILTER_AVERAGE then :encode_png_str_scanline_average 216 when ChunkyPNG::FILTER_PAETH then :encode_png_str_scanline_paeth 217 else raise ArgumentError, "Filtering method #{filtering} is not supported" 218 end 219 220 0.upto(height - 1) do |y| 221 stream << send(encode_method, row(y)) 222 end 223 224 # Now, apply filtering if any 225 if filter_method 226 (height - 1).downto(0) do |y| 227 pos = start_pos + y * (line_width + 1) 228 prev_pos = y == 0 ? nil : pos - (line_width + 1) 229 send(filter_method, stream, pos, prev_pos, line_width, pixel_size) 230 end 231 end 232 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 189 def encode_png_image_with_interlacing(color_mode, bit_depth = 8, filtering = ChunkyPNG::FILTER_NONE) 190 stream = ChunkyPNG::Datastream.empty_bytearray 191 0.upto(6) do |pass| 192 subcanvas = self.class.adam7_extract_pass(pass, self) 193 subcanvas.encoding_palette = encoding_palette 194 subcanvas.encode_png_image_pass_to_stream(stream, color_mode, bit_depth, filtering) 195 end 196 stream 197 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 173 def encode_png_image_without_interlacing(color_mode, bit_depth = 8, filtering = ChunkyPNG::FILTER_NONE) 174 stream = ChunkyPNG::Datastream.empty_bytearray 175 encode_png_image_pass_to_stream(stream, color_mode, bit_depth, filtering) 176 stream 177 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 305 def encode_png_pixels_to_scanline_grayscale_1bit(pixels) 306 chars = [] 307 pixels.each_slice(8) do |p1, p2, p3, p4, p5, p6, p7, p8| 308 chars << ((p1.nil? ? 0 : (p1 & 0x0000ffff) >> 15 << 7) | 309 (p2.nil? ? 0 : (p2 & 0x0000ffff) >> 15 << 6) | 310 (p3.nil? ? 0 : (p3 & 0x0000ffff) >> 15 << 5) | 311 (p4.nil? ? 0 : (p4 & 0x0000ffff) >> 15 << 4) | 312 (p5.nil? ? 0 : (p5 & 0x0000ffff) >> 15 << 3) | 313 (p6.nil? ? 0 : (p6 & 0x0000ffff) >> 15 << 2) | 314 (p7.nil? ? 0 : (p7 & 0x0000ffff) >> 15 << 1) | 315 (p8.nil? ? 0 : (p8 & 0x0000ffff) >> 15)) 316 end 317 chars.pack("xC*") 318 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 323 def encode_png_pixels_to_scanline_grayscale_2bit(pixels) 324 chars = [] 325 pixels.each_slice(4) do |p1, p2, p3, p4| 326 chars << ((p1.nil? ? 0 : (p1 & 0x0000ffff) >> 14 << 6) | 327 (p2.nil? ? 0 : (p2 & 0x0000ffff) >> 14 << 4) | 328 (p3.nil? ? 0 : (p3 & 0x0000ffff) >> 14 << 2) | 329 (p4.nil? ? 0 : (p4 & 0x0000ffff) >> 14)) 330 end 331 chars.pack("xC*") 332 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 337 def encode_png_pixels_to_scanline_grayscale_4bit(pixels) 338 chars = [] 339 pixels.each_slice(2) do |p1, p2| 340 chars << ((p1.nil? ? 0 : ((p1 & 0x0000ffff) >> 12) << 4) | (p2.nil? ? 0 : ((p2 & 0x0000ffff) >> 12))) 341 end 342 chars.pack("xC*") 343 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 348 def encode_png_pixels_to_scanline_grayscale_8bit(pixels) 349 pixels.map { |p| p >> 8 }.pack("xC#{width}") 350 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 355 def encode_png_pixels_to_scanline_grayscale_alpha_8bit(pixels) 356 pixels.pack("xn#{width}") 357 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 251 def encode_png_pixels_to_scanline_indexed_1bit(pixels) 252 chars = [] 253 pixels.each_slice(8) do |p1, p2, p3, p4, p5, p6, p7, p8| 254 chars << ( 255 (encoding_palette.index(p1) << 7) | 256 (encoding_palette.index(p2) << 6) | 257 (encoding_palette.index(p3) << 5) | 258 (encoding_palette.index(p4) << 4) | 259 (encoding_palette.index(p5) << 3) | 260 (encoding_palette.index(p6) << 2) | 261 (encoding_palette.index(p7) << 1) | 262 encoding_palette.index(p8) 263 ) 264 end 265 chars.pack("xC*") 266 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 271 def encode_png_pixels_to_scanline_indexed_2bit(pixels) 272 chars = [] 273 pixels.each_slice(4) do |p1, p2, p3, p4| 274 chars << ( 275 (encoding_palette.index(p1) << 6) | 276 (encoding_palette.index(p2) << 4) | 277 (encoding_palette.index(p3) << 2) | 278 encoding_palette.index(p4) 279 ) 280 end 281 chars.pack("xC*") 282 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 287 def encode_png_pixels_to_scanline_indexed_4bit(pixels) 288 chars = [] 289 pixels.each_slice(2) do |p1, p2| 290 chars << ((encoding_palette.index(p1) << 4) | encoding_palette.index(p2)) 291 end 292 chars.pack("xC*") 293 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 298 def encode_png_pixels_to_scanline_indexed_8bit(pixels) 299 pixels.map { |p| encoding_palette.index(p) }.pack("xC#{width}") 300 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 364 def encode_png_pixels_to_scanline_method(color_mode, depth) 365 encoder_method = case color_mode 366 when ChunkyPNG::COLOR_TRUECOLOR then :"encode_png_pixels_to_scanline_truecolor_#{depth}bit" 367 when ChunkyPNG::COLOR_TRUECOLOR_ALPHA then :"encode_png_pixels_to_scanline_truecolor_alpha_#{depth}bit" 368 when ChunkyPNG::COLOR_INDEXED then :"encode_png_pixels_to_scanline_indexed_#{depth}bit" 369 when ChunkyPNG::COLOR_GRAYSCALE then :"encode_png_pixels_to_scanline_grayscale_#{depth}bit" 370 when ChunkyPNG::COLOR_GRAYSCALE_ALPHA then :"encode_png_pixels_to_scanline_grayscale_alpha_#{depth}bit" 371 end 372 373 raise ChunkyPNG::NotSupported, "No encoder found for color mode #{color_mode} and #{depth}-bit depth!" unless respond_to?(encoder_method, true) 374 encoder_method 375 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 237 def encode_png_pixels_to_scanline_truecolor_8bit(pixels) 238 pixels.pack("x" + ("NX" * width)) 239 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 244 def encode_png_pixels_to_scanline_truecolor_alpha_8bit(pixels) 245 pixels.pack("xN#{width}") 246 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 155 def encode_png_pixelstream(color_mode = ChunkyPNG::COLOR_TRUECOLOR, bit_depth = 8, interlace = ChunkyPNG::INTERLACING_NONE, filtering = ChunkyPNG::FILTER_NONE) 156 if color_mode == ChunkyPNG::COLOR_INDEXED 157 raise ChunkyPNG::ExpectationFailed, "This palette is not suitable for encoding!" if encoding_palette.nil? || !encoding_palette.can_encode? 158 raise ChunkyPNG::ExpectationFailed, "This palette has too many colors!" if encoding_palette.size > (1 << bit_depth) 159 end 160 161 case interlace 162 when ChunkyPNG::INTERLACING_NONE then encode_png_image_without_interlacing(color_mode, bit_depth, filtering) 163 when ChunkyPNG::INTERLACING_ADAM7 then encode_png_image_with_interlacing(color_mode, bit_depth, filtering) 164 else raise ChunkyPNG::NotSupported, "Unknown interlacing method: #{interlace}!" 165 end 166 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 415 def encode_png_str_scanline_average(stream, pos, prev_pos, line_width, pixel_size) 416 line_width.downto(1) do |i| 417 a = i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0 418 b = prev_pos ? stream.getbyte(prev_pos + i) : 0 419 stream.setbyte(pos + i, (stream.getbyte(pos + i) - ((a + b) >> 1)) & 0xff) 420 end 421 stream.setbyte(pos, ChunkyPNG::FILTER_AVERAGE) 422 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 386 def encode_png_str_scanline_none(stream, pos, prev_pos, line_width, pixel_size) 387 # noop - this method shouldn't get called at all. 388 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 427 def encode_png_str_scanline_paeth(stream, pos, prev_pos, line_width, pixel_size) 428 line_width.downto(1) do |i| 429 a = i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0 430 b = prev_pos ? stream.getbyte(prev_pos + i) : 0 431 c = prev_pos && i > pixel_size ? stream.getbyte(prev_pos + i - pixel_size) : 0 432 p = a + b - c 433 pa = (p - a).abs 434 pb = (p - b).abs 435 pc = (p - c).abs 436 pr = if pa <= pb && pa <= pc 437 a 438 else 439 pb <= pc ? b : c 440 end 441 442 stream.setbyte(pos + i, (stream.getbyte(pos + i) - pr) & 0xff) 443 end 444 stream.setbyte(pos, ChunkyPNG::FILTER_PAETH) 445 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 393 def encode_png_str_scanline_sub(stream, pos, prev_pos, line_width, pixel_size) 394 line_width.downto(1) do |i| 395 a = i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0 396 stream.setbyte(pos + i, (stream.getbyte(pos + i) - a) & 0xff) 397 end 398 stream.setbyte(pos, ChunkyPNG::FILTER_SUB) 399 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 404 def encode_png_str_scanline_up(stream, pos, prev_pos, line_width, pixel_size) 405 line_width.downto(1) do |i| 406 b = prev_pos ? stream.getbyte(prev_pos + i) : 0 407 stream.setbyte(pos + i, (stream.getbyte(pos + i) - b) & 0xff) 408 end 409 stream.setbyte(pos, ChunkyPNG::FILTER_UP) 410 end