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
Source
# File lib/chunky_png/canvas/png_encoding.rb 42 def save(filename, constraints = {}) 43 File.open(filename, "wb") { |io| write(io, constraints) } 44 end
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]
Source
# File lib/chunky_png/canvas/png_encoding.rb 49 def to_blob(constraints = {}) 50 to_datastream(constraints).to_blob 51 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.
Source
# 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( 79 width: width, 80 height: height, 81 color: encoding[:color_mode], 82 depth: encoding[:bit_depth], 83 interlace: encoding[:interlace] 84 ) 85 86 if encoding[:color_mode] == ChunkyPNG::COLOR_INDEXED 87 ds.palette_chunk = encoding_palette.to_plte_chunk 88 ds.transparency_chunk = encoding_palette.to_trns_chunk unless encoding_palette.opaque? 89 end 90 data = encode_png_pixelstream(encoding[:color_mode], encoding[:bit_depth], encoding[:interlace], encoding[:filtering]) 91 ds.data_chunks = Chunk::ImageData.split_in_chunks(data, encoding[:compression]) 92 ds.end_chunk = Chunk::End.new 93 ds 94 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
Source
# File lib/chunky_png/canvas/png_encoding.rb 34 def write(io, constraints = {}) 35 to_datastream(constraints).write(io) 36 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]
Protected Instance Methods
Source
# File lib/chunky_png/canvas/png_encoding.rb 107 def determine_png_encoding(constraints = {}) 108 encoding = case constraints 109 when :fast_rgb then {color_mode: ChunkyPNG::COLOR_TRUECOLOR, compression: Zlib::BEST_SPEED} 110 when :fast_rgba then {color_mode: ChunkyPNG::COLOR_TRUECOLOR_ALPHA, compression: Zlib::BEST_SPEED} 111 when :best_compression then {compression: Zlib::BEST_COMPRESSION, filtering: ChunkyPNG::FILTER_PAETH} 112 when :good_compression then {compression: Zlib::BEST_COMPRESSION, filtering: ChunkyPNG::FILTER_NONE} 113 when :no_compression then {compression: Zlib::NO_COMPRESSION} 114 when :black_and_white then {color_mode: ChunkyPNG::COLOR_GRAYSCALE, bit_depth: 1} 115 when Hash then constraints 116 else raise ChunkyPNG::Exception, "Unknown encoding preset: #{constraints.inspect}" 117 end 118 119 # Do not create a palette when the encoding is given and does not require a palette. 120 if encoding[:color_mode] 121 if encoding[:color_mode] == ChunkyPNG::COLOR_INDEXED 122 self.encoding_palette = palette 123 encoding[:bit_depth] ||= encoding_palette.determine_bit_depth 124 else 125 encoding[:bit_depth] ||= 8 126 end 127 else 128 self.encoding_palette = palette 129 suggested_color_mode, suggested_bit_depth = encoding_palette.best_color_settings 130 encoding[:color_mode] ||= suggested_color_mode 131 encoding[:bit_depth] ||= suggested_bit_depth 132 end 133 134 # Use Zlib's default for compression unless otherwise provided. 135 encoding[:compression] ||= Zlib::DEFAULT_COMPRESSION 136 137 encoding[:interlace] = case encoding[:interlace] 138 when nil, false then ChunkyPNG::INTERLACING_NONE 139 when true then ChunkyPNG::INTERLACING_ADAM7 140 else encoding[:interlace] 141 end 142 143 encoding[:filtering] ||= case encoding[:compression] 144 when Zlib::BEST_COMPRESSION then ChunkyPNG::FILTER_PAETH 145 when Zlib::NO_COMPRESSION..Zlib::BEST_SPEED then ChunkyPNG::FILTER_NONE 146 else ChunkyPNG::FILTER_UP 147 end 148 encoding 149 end
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}
Source
# File lib/chunky_png/canvas/png_encoding.rb 206 def encode_png_image_pass_to_stream(stream, color_mode, bit_depth, filtering) 207 start_pos = stream.bytesize 208 pixel_size = Color.pixel_bytesize(color_mode) 209 line_width = Color.scanline_bytesize(color_mode, bit_depth, width) 210 211 # Determine the filter method 212 encode_method = encode_png_pixels_to_scanline_method(color_mode, bit_depth) 213 filter_method = case filtering 214 when ChunkyPNG::FILTER_NONE then nil 215 when ChunkyPNG::FILTER_SUB then :encode_png_str_scanline_sub 216 when ChunkyPNG::FILTER_UP then :encode_png_str_scanline_up 217 when ChunkyPNG::FILTER_AVERAGE then :encode_png_str_scanline_average 218 when ChunkyPNG::FILTER_PAETH then :encode_png_str_scanline_paeth 219 else raise ArgumentError, "Filtering method #{filtering} is not supported" 220 end 221 222 0.upto(height - 1) do |y| 223 stream << send(encode_method, row(y)) 224 end 225 226 # Now, apply filtering if any 227 if filter_method 228 (height - 1).downto(0) do |y| 229 pos = start_pos + y * (line_width + 1) 230 prev_pos = y == 0 ? nil : pos - (line_width + 1) 231 send(filter_method, stream, pos, prev_pos, line_width, pixel_size) 232 end 233 end 234 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.
Source
# File lib/chunky_png/canvas/png_encoding.rb 191 def encode_png_image_with_interlacing(color_mode, bit_depth = 8, filtering = ChunkyPNG::FILTER_NONE) 192 stream = "".b 193 0.upto(6) do |pass| 194 subcanvas = self.class.adam7_extract_pass(pass, self) 195 subcanvas.encoding_palette = encoding_palette 196 subcanvas.encode_png_image_pass_to_stream(stream, color_mode, bit_depth, filtering) 197 end 198 stream 199 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.
Source
# File lib/chunky_png/canvas/png_encoding.rb 175 def encode_png_image_without_interlacing(color_mode, bit_depth = 8, filtering = ChunkyPNG::FILTER_NONE) 176 stream = "".b 177 encode_png_image_pass_to_stream(stream, color_mode, bit_depth, filtering) 178 stream 179 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.
Source
# File lib/chunky_png/canvas/png_encoding.rb 307 def encode_png_pixels_to_scanline_grayscale_1bit(pixels) 308 chars = [] 309 pixels.each_slice(8) do |p1, p2, p3, p4, p5, p6, p7, p8| 310 chars << ((p1.nil? ? 0 : (p1 & 0x0000ffff) >> 15 << 7) | 311 (p2.nil? ? 0 : (p2 & 0x0000ffff) >> 15 << 6) | 312 (p3.nil? ? 0 : (p3 & 0x0000ffff) >> 15 << 5) | 313 (p4.nil? ? 0 : (p4 & 0x0000ffff) >> 15 << 4) | 314 (p5.nil? ? 0 : (p5 & 0x0000ffff) >> 15 << 3) | 315 (p6.nil? ? 0 : (p6 & 0x0000ffff) >> 15 << 2) | 316 (p7.nil? ? 0 : (p7 & 0x0000ffff) >> 15 << 1) | 317 (p8.nil? ? 0 : (p8 & 0x0000ffff) >> 15)) 318 end 319 chars.pack("xC*") 320 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
Source
# File lib/chunky_png/canvas/png_encoding.rb 325 def encode_png_pixels_to_scanline_grayscale_2bit(pixels) 326 chars = [] 327 pixels.each_slice(4) do |p1, p2, p3, p4| 328 chars << ((p1.nil? ? 0 : (p1 & 0x0000ffff) >> 14 << 6) | 329 (p2.nil? ? 0 : (p2 & 0x0000ffff) >> 14 << 4) | 330 (p3.nil? ? 0 : (p3 & 0x0000ffff) >> 14 << 2) | 331 (p4.nil? ? 0 : (p4 & 0x0000ffff) >> 14)) 332 end 333 chars.pack("xC*") 334 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
Source
# File lib/chunky_png/canvas/png_encoding.rb 339 def encode_png_pixels_to_scanline_grayscale_4bit(pixels) 340 chars = [] 341 pixels.each_slice(2) do |p1, p2| 342 chars << ((p1.nil? ? 0 : ((p1 & 0x0000ffff) >> 12) << 4) | (p2.nil? ? 0 : ((p2 & 0x0000ffff) >> 12))) 343 end 344 chars.pack("xC*") 345 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
Source
# File lib/chunky_png/canvas/png_encoding.rb 350 def encode_png_pixels_to_scanline_grayscale_8bit(pixels) 351 pixels.map { |p| p >> 8 }.pack("xC#{width}") 352 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
Source
# File lib/chunky_png/canvas/png_encoding.rb 357 def encode_png_pixels_to_scanline_grayscale_alpha_8bit(pixels) 358 pixels.pack("xn#{width}") 359 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
Source
# File lib/chunky_png/canvas/png_encoding.rb 253 def encode_png_pixels_to_scanline_indexed_1bit(pixels) 254 chars = [] 255 pixels.each_slice(8) do |p1, p2, p3, p4, p5, p6, p7, p8| 256 chars << ( 257 (encoding_palette.index(p1) << 7) | 258 (encoding_palette.index(p2) << 6) | 259 (encoding_palette.index(p3) << 5) | 260 (encoding_palette.index(p4) << 4) | 261 (encoding_palette.index(p5) << 3) | 262 (encoding_palette.index(p6) << 2) | 263 (encoding_palette.index(p7) << 1) | 264 encoding_palette.index(p8) 265 ) 266 end 267 chars.pack("xC*") 268 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
Source
# File lib/chunky_png/canvas/png_encoding.rb 273 def encode_png_pixels_to_scanline_indexed_2bit(pixels) 274 chars = [] 275 pixels.each_slice(4) do |p1, p2, p3, p4| 276 chars << ( 277 (encoding_palette.index(p1) << 6) | 278 (encoding_palette.index(p2) << 4) | 279 (encoding_palette.index(p3) << 2) | 280 encoding_palette.index(p4) 281 ) 282 end 283 chars.pack("xC*") 284 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
Source
# File lib/chunky_png/canvas/png_encoding.rb 289 def encode_png_pixels_to_scanline_indexed_4bit(pixels) 290 chars = [] 291 pixels.each_slice(2) do |p1, p2| 292 chars << ((encoding_palette.index(p1) << 4) | encoding_palette.index(p2)) 293 end 294 chars.pack("xC*") 295 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
Source
# File lib/chunky_png/canvas/png_encoding.rb 300 def encode_png_pixels_to_scanline_indexed_8bit(pixels) 301 pixels.map { |p| encoding_palette.index(p) }.pack("xC#{width}") 302 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
Source
# File lib/chunky_png/canvas/png_encoding.rb 366 def encode_png_pixels_to_scanline_method(color_mode, depth) 367 encoder_method = case color_mode 368 when ChunkyPNG::COLOR_TRUECOLOR then :"encode_png_pixels_to_scanline_truecolor_#{depth}bit" 369 when ChunkyPNG::COLOR_TRUECOLOR_ALPHA then :"encode_png_pixels_to_scanline_truecolor_alpha_#{depth}bit" 370 when ChunkyPNG::COLOR_INDEXED then :"encode_png_pixels_to_scanline_indexed_#{depth}bit" 371 when ChunkyPNG::COLOR_GRAYSCALE then :"encode_png_pixels_to_scanline_grayscale_#{depth}bit" 372 when ChunkyPNG::COLOR_GRAYSCALE_ALPHA then :"encode_png_pixels_to_scanline_grayscale_alpha_#{depth}bit" 373 end 374 375 raise ChunkyPNG::NotSupported, "No encoder found for color mode #{color_mode} and #{depth}-bit depth!" unless respond_to?(encoder_method, true) 376 encoder_method 377 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.
Source
# File lib/chunky_png/canvas/png_encoding.rb 239 def encode_png_pixels_to_scanline_truecolor_8bit(pixels) 240 pixels.pack("x" + ("NX" * width)) 241 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
Source
# File lib/chunky_png/canvas/png_encoding.rb 246 def encode_png_pixels_to_scanline_truecolor_alpha_8bit(pixels) 247 pixels.pack("xN#{width}") 248 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
Source
# File lib/chunky_png/canvas/png_encoding.rb 157 def encode_png_pixelstream(color_mode = ChunkyPNG::COLOR_TRUECOLOR, bit_depth = 8, interlace = ChunkyPNG::INTERLACING_NONE, filtering = ChunkyPNG::FILTER_NONE) 158 if color_mode == ChunkyPNG::COLOR_INDEXED 159 raise ChunkyPNG::ExpectationFailed, "This palette is not suitable for encoding!" if encoding_palette.nil? || !encoding_palette.can_encode? 160 raise ChunkyPNG::ExpectationFailed, "This palette has too many colors!" if encoding_palette.size > (1 << bit_depth) 161 end 162 163 case interlace 164 when ChunkyPNG::INTERLACING_NONE then encode_png_image_without_interlacing(color_mode, bit_depth, filtering) 165 when ChunkyPNG::INTERLACING_ADAM7 then encode_png_image_with_interlacing(color_mode, bit_depth, filtering) 166 else raise ChunkyPNG::NotSupported, "Unknown interlacing method: #{interlace}!" 167 end 168 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.
Source
# File lib/chunky_png/canvas/png_encoding.rb 417 def encode_png_str_scanline_average(stream, pos, prev_pos, line_width, pixel_size) 418 line_width.downto(1) do |i| 419 a = i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0 420 b = prev_pos ? stream.getbyte(prev_pos + i) : 0 421 stream.setbyte(pos + i, (stream.getbyte(pos + i) - ((a + b) >> 1)) & 0xff) 422 end 423 stream.setbyte(pos, ChunkyPNG::FILTER_AVERAGE) 424 end
Encodes a scanline of a pixelstream using AVERAGE filtering. This will modify the stream. @param (see encode_png_str_scanline_none
) @return [void]
Source
# File lib/chunky_png/canvas/png_encoding.rb 388 def encode_png_str_scanline_none(stream, pos, prev_pos, line_width, pixel_size) 389 # noop - this method shouldn't get called at all. 390 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]
Source
# File lib/chunky_png/canvas/png_encoding.rb 429 def encode_png_str_scanline_paeth(stream, pos, prev_pos, line_width, pixel_size) 430 line_width.downto(1) do |i| 431 a = i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0 432 b = prev_pos ? stream.getbyte(prev_pos + i) : 0 433 c = prev_pos && i > pixel_size ? stream.getbyte(prev_pos + i - pixel_size) : 0 434 p = a + b - c 435 pa = (p - a).abs 436 pb = (p - b).abs 437 pc = (p - c).abs 438 pr = if pa <= pb && pa <= pc 439 a 440 else 441 pb <= pc ? b : c 442 end 443 444 stream.setbyte(pos + i, (stream.getbyte(pos + i) - pr) & 0xff) 445 end 446 stream.setbyte(pos, ChunkyPNG::FILTER_PAETH) 447 end
Encodes a scanline of a pixelstream using PAETH filtering. This will modify the stream. @param (see encode_png_str_scanline_none
) @return [void]
Source
# File lib/chunky_png/canvas/png_encoding.rb 395 def encode_png_str_scanline_sub(stream, pos, prev_pos, line_width, pixel_size) 396 line_width.downto(1) do |i| 397 a = i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0 398 stream.setbyte(pos + i, (stream.getbyte(pos + i) - a) & 0xff) 399 end 400 stream.setbyte(pos, ChunkyPNG::FILTER_SUB) 401 end
Encodes a scanline of a pixelstream using SUB filtering. This will modify the stream. @param (see encode_png_str_scanline_none
) @return [void]
Source
# File lib/chunky_png/canvas/png_encoding.rb 406 def encode_png_str_scanline_up(stream, pos, prev_pos, line_width, pixel_size) 407 line_width.downto(1) do |i| 408 b = prev_pos ? stream.getbyte(prev_pos + i) : 0 409 stream.setbyte(pos + i, (stream.getbyte(pos + i) - b) & 0xff) 410 end 411 stream.setbyte(pos, ChunkyPNG::FILTER_UP) 412 end
Encodes a scanline of a pixelstream using UP filtering. This will modify the stream. @param (see encode_png_str_scanline_none
) @return [void]