Module ChunkyPNG::Canvas::PNGEncoding
In: lib/chunky_png/canvas/png_encoding.rb

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

Methods

Attributes

encoding_palette  [RW]  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]

[Source]

    # File lib/chunky_png/canvas/png_encoding.rb, line 42
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.

[Source]

    # File lib/chunky_png/canvas/png_encoding.rb, line 49
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

[Source]

    # File lib/chunky_png/canvas/png_encoding.rb, line 74
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
to_s(constraints = {})

Alias for to_blob

to_string(constraints = {})

Alias for to_blob

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]

[Source]

    # File lib/chunky_png/canvas/png_encoding.rb, line 34
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}

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 102
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.

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 203
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.

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 188
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.

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 172
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

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 300
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

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 318
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

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 332
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

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 343
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

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 350
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

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 250
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

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 268
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

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 282
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

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 293
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.

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 360
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""encode_png_pixels_to_scanline_truecolor_#{depth}bit"
363:           when ChunkyPNG::COLOR_TRUECOLOR_ALPHA; "encode_png_pixels_to_scanline_truecolor_alpha_#{depth}bit""encode_png_pixels_to_scanline_truecolor_alpha_#{depth}bit"
364:           when ChunkyPNG::COLOR_INDEXED;         "encode_png_pixels_to_scanline_indexed_#{depth}bit""encode_png_pixels_to_scanline_indexed_#{depth}bit"
365:           when ChunkyPNG::COLOR_GRAYSCALE;       "encode_png_pixels_to_scanline_grayscale_#{depth}bit""encode_png_pixels_to_scanline_grayscale_#{depth}bit"
366:           when ChunkyPNG::COLOR_GRAYSCALE_ALPHA; "encode_png_pixels_to_scanline_grayscale_alpha_#{depth}bit""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)
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

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 236
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

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 243
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.

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 153
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]

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 414
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]

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 385
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]

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 426
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]

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 392
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]

[Source]

     # File lib/chunky_png/canvas/png_encoding.rb, line 403
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

[Validate]