| 1 | //! Decoding and Encoding of PNG Images |
| 2 | //! |
| 3 | //! PNG (Portable Network Graphics) is an image format that supports lossless compression. |
| 4 | //! |
| 5 | //! # Related Links |
| 6 | //! * <http://www.w3.org/TR/PNG/> - The PNG Specification |
| 7 | |
| 8 | use std::borrow::Cow; |
| 9 | use std::fmt; |
| 10 | use std::io::{BufRead, Seek, Write}; |
| 11 | |
| 12 | use png::{BlendOp, DisposeOp}; |
| 13 | |
| 14 | use crate::animation::{Delay, Frame, Frames, Ratio}; |
| 15 | use crate::color::{Blend, ColorType, ExtendedColorType}; |
| 16 | use crate::error::{ |
| 17 | DecodingError, EncodingError, ImageError, ImageResult, LimitError, LimitErrorKind, |
| 18 | ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind, |
| 19 | }; |
| 20 | use crate::image::{AnimationDecoder, ImageDecoder, ImageEncoder, ImageFormat}; |
| 21 | use crate::{DynamicImage, GenericImage, ImageBuffer, Luma, LumaA, Rgb, Rgba, RgbaImage}; |
| 22 | use crate::{GenericImageView, Limits}; |
| 23 | |
| 24 | // http://www.w3.org/TR/PNG-Structure.html |
| 25 | // The first eight bytes of a PNG file always contain the following (decimal) values: |
| 26 | pub(crate) const PNG_SIGNATURE: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10]; |
| 27 | |
| 28 | /// PNG decoder |
| 29 | pub struct PngDecoder<R: BufRead + Seek> { |
| 30 | color_type: ColorType, |
| 31 | reader: png::Reader<R>, |
| 32 | limits: Limits, |
| 33 | } |
| 34 | |
| 35 | impl<R: BufRead + Seek> PngDecoder<R> { |
| 36 | /// Creates a new decoder that decodes from the stream ```r``` |
| 37 | pub fn new(r: R) -> ImageResult<PngDecoder<R>> { |
| 38 | Self::with_limits(r, Limits::no_limits()) |
| 39 | } |
| 40 | |
| 41 | /// Creates a new decoder that decodes from the stream ```r``` with the given limits. |
| 42 | pub fn with_limits(r: R, limits: Limits) -> ImageResult<PngDecoder<R>> { |
| 43 | limits.check_support(&crate::LimitSupport::default())?; |
| 44 | |
| 45 | let max_bytes = usize::try_from(limits.max_alloc.unwrap_or(u64::MAX)).unwrap_or(usize::MAX); |
| 46 | let mut decoder = png::Decoder::new_with_limits(r, png::Limits { bytes: max_bytes }); |
| 47 | decoder.set_ignore_text_chunk(true); |
| 48 | |
| 49 | let info = decoder.read_header_info().map_err(ImageError::from_png)?; |
| 50 | limits.check_dimensions(info.width, info.height)?; |
| 51 | |
| 52 | // By default the PNG decoder will scale 16 bpc to 8 bpc, so custom |
| 53 | // transformations must be set. EXPAND preserves the default behavior |
| 54 | // expanding bpc < 8 to 8 bpc. |
| 55 | decoder.set_transformations(png::Transformations::EXPAND); |
| 56 | let reader = decoder.read_info().map_err(ImageError::from_png)?; |
| 57 | let (color_type, bits) = reader.output_color_type(); |
| 58 | let color_type = match (color_type, bits) { |
| 59 | (png::ColorType::Grayscale, png::BitDepth::Eight) => ColorType::L8, |
| 60 | (png::ColorType::Grayscale, png::BitDepth::Sixteen) => ColorType::L16, |
| 61 | (png::ColorType::GrayscaleAlpha, png::BitDepth::Eight) => ColorType::La8, |
| 62 | (png::ColorType::GrayscaleAlpha, png::BitDepth::Sixteen) => ColorType::La16, |
| 63 | (png::ColorType::Rgb, png::BitDepth::Eight) => ColorType::Rgb8, |
| 64 | (png::ColorType::Rgb, png::BitDepth::Sixteen) => ColorType::Rgb16, |
| 65 | (png::ColorType::Rgba, png::BitDepth::Eight) => ColorType::Rgba8, |
| 66 | (png::ColorType::Rgba, png::BitDepth::Sixteen) => ColorType::Rgba16, |
| 67 | |
| 68 | (png::ColorType::Grayscale, png::BitDepth::One) => { |
| 69 | return Err(unsupported_color(ExtendedColorType::L1)) |
| 70 | } |
| 71 | (png::ColorType::GrayscaleAlpha, png::BitDepth::One) => { |
| 72 | return Err(unsupported_color(ExtendedColorType::La1)) |
| 73 | } |
| 74 | (png::ColorType::Rgb, png::BitDepth::One) => { |
| 75 | return Err(unsupported_color(ExtendedColorType::Rgb1)) |
| 76 | } |
| 77 | (png::ColorType::Rgba, png::BitDepth::One) => { |
| 78 | return Err(unsupported_color(ExtendedColorType::Rgba1)) |
| 79 | } |
| 80 | |
| 81 | (png::ColorType::Grayscale, png::BitDepth::Two) => { |
| 82 | return Err(unsupported_color(ExtendedColorType::L2)) |
| 83 | } |
| 84 | (png::ColorType::GrayscaleAlpha, png::BitDepth::Two) => { |
| 85 | return Err(unsupported_color(ExtendedColorType::La2)) |
| 86 | } |
| 87 | (png::ColorType::Rgb, png::BitDepth::Two) => { |
| 88 | return Err(unsupported_color(ExtendedColorType::Rgb2)) |
| 89 | } |
| 90 | (png::ColorType::Rgba, png::BitDepth::Two) => { |
| 91 | return Err(unsupported_color(ExtendedColorType::Rgba2)) |
| 92 | } |
| 93 | |
| 94 | (png::ColorType::Grayscale, png::BitDepth::Four) => { |
| 95 | return Err(unsupported_color(ExtendedColorType::L4)) |
| 96 | } |
| 97 | (png::ColorType::GrayscaleAlpha, png::BitDepth::Four) => { |
| 98 | return Err(unsupported_color(ExtendedColorType::La4)) |
| 99 | } |
| 100 | (png::ColorType::Rgb, png::BitDepth::Four) => { |
| 101 | return Err(unsupported_color(ExtendedColorType::Rgb4)) |
| 102 | } |
| 103 | (png::ColorType::Rgba, png::BitDepth::Four) => { |
| 104 | return Err(unsupported_color(ExtendedColorType::Rgba4)) |
| 105 | } |
| 106 | |
| 107 | (png::ColorType::Indexed, bits) => { |
| 108 | return Err(unsupported_color(ExtendedColorType::Unknown(bits as u8))) |
| 109 | } |
| 110 | }; |
| 111 | |
| 112 | Ok(PngDecoder { |
| 113 | color_type, |
| 114 | reader, |
| 115 | limits, |
| 116 | }) |
| 117 | } |
| 118 | |
| 119 | /// Returns the gamma value of the image or None if no gamma value is indicated. |
| 120 | /// |
| 121 | /// If an sRGB chunk is present this method returns a gamma value of 0.45455 and ignores the |
| 122 | /// value in the gAMA chunk. This is the recommended behavior according to the PNG standard: |
| 123 | /// |
| 124 | /// > When the sRGB chunk is present, [...] decoders that recognize the sRGB chunk but are not |
| 125 | /// > capable of colour management are recommended to ignore the gAMA and cHRM chunks, and use |
| 126 | /// > the values given above as if they had appeared in gAMA and cHRM chunks. |
| 127 | pub fn gamma_value(&self) -> ImageResult<Option<f64>> { |
| 128 | Ok(self |
| 129 | .reader |
| 130 | .info() |
| 131 | .source_gamma |
| 132 | .map(|x| f64::from(x.into_scaled()) / 100_000.0)) |
| 133 | } |
| 134 | |
| 135 | /// Turn this into an iterator over the animation frames. |
| 136 | /// |
| 137 | /// Reading the complete animation requires more memory than reading the data from the IDAT |
| 138 | /// frame–multiple frame buffers need to be reserved at the same time. We further do not |
| 139 | /// support compositing 16-bit colors. In any case this would be lossy as the interface of |
| 140 | /// animation decoders does not support 16-bit colors. |
| 141 | /// |
| 142 | /// If something is not supported or a limit is violated then the decoding step that requires |
| 143 | /// them will fail and an error will be returned instead of the frame. No further frames will |
| 144 | /// be returned. |
| 145 | pub fn apng(self) -> ImageResult<ApngDecoder<R>> { |
| 146 | Ok(ApngDecoder::new(self)) |
| 147 | } |
| 148 | |
| 149 | /// Returns if the image contains an animation. |
| 150 | /// |
| 151 | /// Note that the file itself decides if the default image is considered to be part of the |
| 152 | /// animation. When it is not the common interpretation is to use it as a thumbnail. |
| 153 | /// |
| 154 | /// If a non-animated image is converted into an `ApngDecoder` then its iterator is empty. |
| 155 | pub fn is_apng(&self) -> ImageResult<bool> { |
| 156 | Ok(self.reader.info().animation_control.is_some()) |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | fn unsupported_color(ect: ExtendedColorType) -> ImageError { |
| 161 | ImageError::Unsupported(UnsupportedError::from_format_and_kind( |
| 162 | format:ImageFormat::Png.into(), |
| 163 | kind:UnsupportedErrorKind::Color(ect), |
| 164 | )) |
| 165 | } |
| 166 | |
| 167 | impl<R: BufRead + Seek> ImageDecoder for PngDecoder<R> { |
| 168 | fn dimensions(&self) -> (u32, u32) { |
| 169 | self.reader.info().size() |
| 170 | } |
| 171 | |
| 172 | fn color_type(&self) -> ColorType { |
| 173 | self.color_type |
| 174 | } |
| 175 | |
| 176 | fn icc_profile(&mut self) -> ImageResult<Option<Vec<u8>>> { |
| 177 | Ok(self.reader.info().icc_profile.as_ref().map(|x| x.to_vec())) |
| 178 | } |
| 179 | |
| 180 | fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { |
| 181 | use byteorder_lite::{BigEndian, ByteOrder, NativeEndian}; |
| 182 | |
| 183 | assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); |
| 184 | self.reader.next_frame(buf).map_err(ImageError::from_png)?; |
| 185 | // PNG images are big endian. For 16 bit per channel and larger types, |
| 186 | // the buffer may need to be reordered to native endianness per the |
| 187 | // contract of `read_image`. |
| 188 | // TODO: assumes equal channel bit depth. |
| 189 | let bpc = self.color_type().bytes_per_pixel() / self.color_type().channel_count(); |
| 190 | |
| 191 | match bpc { |
| 192 | 1 => (), // No reodering necessary for u8 |
| 193 | 2 => buf.chunks_exact_mut(2).for_each(|c| { |
| 194 | let v = BigEndian::read_u16(c); |
| 195 | NativeEndian::write_u16(c, v); |
| 196 | }), |
| 197 | _ => unreachable!(), |
| 198 | } |
| 199 | Ok(()) |
| 200 | } |
| 201 | |
| 202 | fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> { |
| 203 | (*self).read_image(buf) |
| 204 | } |
| 205 | |
| 206 | fn set_limits(&mut self, limits: Limits) -> ImageResult<()> { |
| 207 | limits.check_support(&crate::LimitSupport::default())?; |
| 208 | let info = self.reader.info(); |
| 209 | limits.check_dimensions(info.width, info.height)?; |
| 210 | self.limits = limits; |
| 211 | // TODO: add `png::Reader::change_limits()` and call it here |
| 212 | // to also constrain the internal buffer allocations in the PNG crate |
| 213 | Ok(()) |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | /// An [`AnimationDecoder`] adapter of [`PngDecoder`]. |
| 218 | /// |
| 219 | /// See [`PngDecoder::apng`] for more information. |
| 220 | /// |
| 221 | /// [`AnimationDecoder`]: ../trait.AnimationDecoder.html |
| 222 | /// [`PngDecoder`]: struct.PngDecoder.html |
| 223 | /// [`PngDecoder::apng`]: struct.PngDecoder.html#method.apng |
| 224 | pub struct ApngDecoder<R: BufRead + Seek> { |
| 225 | inner: PngDecoder<R>, |
| 226 | /// The current output buffer. |
| 227 | current: Option<RgbaImage>, |
| 228 | /// The previous output buffer, used for dispose op previous. |
| 229 | previous: Option<RgbaImage>, |
| 230 | /// The dispose op of the current frame. |
| 231 | dispose: DisposeOp, |
| 232 | |
| 233 | /// The region to dispose of the previous frame. |
| 234 | dispose_region: Option<(u32, u32, u32, u32)>, |
| 235 | /// The number of image still expected to be able to load. |
| 236 | remaining: u32, |
| 237 | /// The next (first) image is the thumbnail. |
| 238 | has_thumbnail: bool, |
| 239 | } |
| 240 | |
| 241 | impl<R: BufRead + Seek> ApngDecoder<R> { |
| 242 | fn new(inner: PngDecoder<R>) -> Self { |
| 243 | let info = inner.reader.info(); |
| 244 | let remaining = match info.animation_control() { |
| 245 | // The expected number of fcTL in the remaining image. |
| 246 | Some(actl) => actl.num_frames, |
| 247 | None => 0, |
| 248 | }; |
| 249 | // If the IDAT has no fcTL then it is not part of the animation counted by |
| 250 | // num_frames. All following fdAT chunks must be preceded by an fcTL |
| 251 | let has_thumbnail = info.frame_control.is_none(); |
| 252 | ApngDecoder { |
| 253 | inner, |
| 254 | current: None, |
| 255 | previous: None, |
| 256 | dispose: DisposeOp::Background, |
| 257 | dispose_region: None, |
| 258 | remaining, |
| 259 | has_thumbnail, |
| 260 | } |
| 261 | } |
| 262 | |
| 263 | // TODO: thumbnail(&mut self) -> Option<impl ImageDecoder<'_>> |
| 264 | |
| 265 | /// Decode one subframe and overlay it on the canvas. |
| 266 | fn mix_next_frame(&mut self) -> Result<Option<&RgbaImage>, ImageError> { |
| 267 | // The iterator always produces RGBA8 images |
| 268 | const COLOR_TYPE: ColorType = ColorType::Rgba8; |
| 269 | |
| 270 | // Allocate the buffers, honoring the memory limits |
| 271 | let (width, height) = self.inner.dimensions(); |
| 272 | { |
| 273 | let limits = &mut self.inner.limits; |
| 274 | if self.previous.is_none() { |
| 275 | limits.reserve_buffer(width, height, COLOR_TYPE)?; |
| 276 | self.previous = Some(RgbaImage::new(width, height)); |
| 277 | } |
| 278 | |
| 279 | if self.current.is_none() { |
| 280 | limits.reserve_buffer(width, height, COLOR_TYPE)?; |
| 281 | self.current = Some(RgbaImage::new(width, height)); |
| 282 | } |
| 283 | } |
| 284 | |
| 285 | // Remove this image from remaining. |
| 286 | self.remaining = match self.remaining.checked_sub(1) { |
| 287 | None => return Ok(None), |
| 288 | Some(next) => next, |
| 289 | }; |
| 290 | |
| 291 | // Shorten ourselves to 0 in case of error. |
| 292 | let remaining = self.remaining; |
| 293 | self.remaining = 0; |
| 294 | |
| 295 | // Skip the thumbnail that is not part of the animation. |
| 296 | if self.has_thumbnail { |
| 297 | // Clone the limits so that our one-off allocation that's destroyed after this scope doesn't persist |
| 298 | let mut limits = self.inner.limits.clone(); |
| 299 | limits.reserve_usize(self.inner.reader.output_buffer_size())?; |
| 300 | let mut buffer = vec![0; self.inner.reader.output_buffer_size()]; |
| 301 | // TODO: add `png::Reader::change_limits()` and call it here |
| 302 | // to also constrain the internal buffer allocations in the PNG crate |
| 303 | self.inner |
| 304 | .reader |
| 305 | .next_frame(&mut buffer) |
| 306 | .map_err(ImageError::from_png)?; |
| 307 | self.has_thumbnail = false; |
| 308 | } |
| 309 | |
| 310 | self.animatable_color_type()?; |
| 311 | |
| 312 | // We've initialized them earlier in this function |
| 313 | let previous = self.previous.as_mut().unwrap(); |
| 314 | let current = self.current.as_mut().unwrap(); |
| 315 | |
| 316 | // Dispose of the previous frame. |
| 317 | |
| 318 | match self.dispose { |
| 319 | DisposeOp::None => { |
| 320 | previous.clone_from(current); |
| 321 | } |
| 322 | DisposeOp::Background => { |
| 323 | previous.clone_from(current); |
| 324 | if let Some((px, py, width, height)) = self.dispose_region { |
| 325 | let mut region_current = current.sub_image(px, py, width, height); |
| 326 | |
| 327 | // FIXME: This is a workaround for the fact that `pixels_mut` is not implemented |
| 328 | let pixels: Vec<_> = region_current.pixels().collect(); |
| 329 | |
| 330 | for (x, y, _) in &pixels { |
| 331 | region_current.put_pixel(*x, *y, Rgba::from([0, 0, 0, 0])); |
| 332 | } |
| 333 | } else { |
| 334 | // The first frame is always a background frame. |
| 335 | current.pixels_mut().for_each(|pixel| { |
| 336 | *pixel = Rgba::from([0, 0, 0, 0]); |
| 337 | }); |
| 338 | } |
| 339 | } |
| 340 | DisposeOp::Previous => { |
| 341 | let (px, py, width, height) = self |
| 342 | .dispose_region |
| 343 | .expect("The first frame must not set dispose=Previous" ); |
| 344 | let region_previous = previous.sub_image(px, py, width, height); |
| 345 | current |
| 346 | .copy_from(®ion_previous.to_image(), px, py) |
| 347 | .unwrap(); |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | // The allocations from now on are not going to persist, |
| 352 | // and will be destroyed at the end of the scope. |
| 353 | // Clone the limits so that any changes to them die with the allocations. |
| 354 | let mut limits = self.inner.limits.clone(); |
| 355 | |
| 356 | // Read next frame data. |
| 357 | let raw_frame_size = self.inner.reader.output_buffer_size(); |
| 358 | limits.reserve_usize(raw_frame_size)?; |
| 359 | let mut buffer = vec![0; raw_frame_size]; |
| 360 | // TODO: add `png::Reader::change_limits()` and call it here |
| 361 | // to also constrain the internal buffer allocations in the PNG crate |
| 362 | self.inner |
| 363 | .reader |
| 364 | .next_frame(&mut buffer) |
| 365 | .map_err(ImageError::from_png)?; |
| 366 | let info = self.inner.reader.info(); |
| 367 | |
| 368 | // Find out how to interpret the decoded frame. |
| 369 | let (width, height, px, py, blend); |
| 370 | match info.frame_control() { |
| 371 | None => { |
| 372 | width = info.width; |
| 373 | height = info.height; |
| 374 | px = 0; |
| 375 | py = 0; |
| 376 | blend = BlendOp::Source; |
| 377 | } |
| 378 | Some(fc) => { |
| 379 | width = fc.width; |
| 380 | height = fc.height; |
| 381 | px = fc.x_offset; |
| 382 | py = fc.y_offset; |
| 383 | blend = fc.blend_op; |
| 384 | self.dispose = fc.dispose_op; |
| 385 | } |
| 386 | }; |
| 387 | |
| 388 | self.dispose_region = Some((px, py, width, height)); |
| 389 | |
| 390 | // Turn the data into an rgba image proper. |
| 391 | limits.reserve_buffer(width, height, COLOR_TYPE)?; |
| 392 | let source = match self.inner.color_type { |
| 393 | ColorType::L8 => { |
| 394 | let image = ImageBuffer::<Luma<_>, _>::from_raw(width, height, buffer).unwrap(); |
| 395 | DynamicImage::ImageLuma8(image).into_rgba8() |
| 396 | } |
| 397 | ColorType::La8 => { |
| 398 | let image = ImageBuffer::<LumaA<_>, _>::from_raw(width, height, buffer).unwrap(); |
| 399 | DynamicImage::ImageLumaA8(image).into_rgba8() |
| 400 | } |
| 401 | ColorType::Rgb8 => { |
| 402 | let image = ImageBuffer::<Rgb<_>, _>::from_raw(width, height, buffer).unwrap(); |
| 403 | DynamicImage::ImageRgb8(image).into_rgba8() |
| 404 | } |
| 405 | ColorType::Rgba8 => ImageBuffer::<Rgba<_>, _>::from_raw(width, height, buffer).unwrap(), |
| 406 | ColorType::L16 | ColorType::Rgb16 | ColorType::La16 | ColorType::Rgba16 => { |
| 407 | // TODO: to enable remove restriction in `animatable_color_type` method. |
| 408 | unreachable!("16-bit apng not yet support" ) |
| 409 | } |
| 410 | _ => unreachable!("Invalid png color" ), |
| 411 | }; |
| 412 | // We've converted the raw frame to RGBA8 and disposed of the original allocation |
| 413 | limits.free_usize(raw_frame_size); |
| 414 | |
| 415 | match blend { |
| 416 | BlendOp::Source => { |
| 417 | current |
| 418 | .copy_from(&source, px, py) |
| 419 | .expect("Invalid png image not detected in png" ); |
| 420 | } |
| 421 | BlendOp::Over => { |
| 422 | // TODO: investigate speed, speed-ups, and bounds-checks. |
| 423 | for (x, y, p) in source.enumerate_pixels() { |
| 424 | current.get_pixel_mut(x + px, y + py).blend(p); |
| 425 | } |
| 426 | } |
| 427 | } |
| 428 | |
| 429 | // Ok, we can proceed with actually remaining images. |
| 430 | self.remaining = remaining; |
| 431 | // Return composited output buffer. |
| 432 | |
| 433 | Ok(Some(self.current.as_ref().unwrap())) |
| 434 | } |
| 435 | |
| 436 | fn animatable_color_type(&self) -> Result<(), ImageError> { |
| 437 | match self.inner.color_type { |
| 438 | ColorType::L8 | ColorType::Rgb8 | ColorType::La8 | ColorType::Rgba8 => Ok(()), |
| 439 | // TODO: do not handle multi-byte colors. Remember to implement it in `mix_next_frame`. |
| 440 | ColorType::L16 | ColorType::Rgb16 | ColorType::La16 | ColorType::Rgba16 => { |
| 441 | Err(unsupported_color(self.inner.color_type.into())) |
| 442 | } |
| 443 | _ => unreachable!(" {:?} not a valid png color" , self.inner.color_type), |
| 444 | } |
| 445 | } |
| 446 | } |
| 447 | |
| 448 | impl<'a, R: BufRead + Seek + 'a> AnimationDecoder<'a> for ApngDecoder<R> { |
| 449 | fn into_frames(self) -> Frames<'a> { |
| 450 | struct FrameIterator<R: BufRead + Seek>(ApngDecoder<R>); |
| 451 | |
| 452 | impl<R: BufRead + Seek> Iterator for FrameIterator<R> { |
| 453 | type Item = ImageResult<Frame>; |
| 454 | |
| 455 | fn next(&mut self) -> Option<Self::Item> { |
| 456 | let image = match self.0.mix_next_frame() { |
| 457 | Ok(Some(image)) => image.clone(), |
| 458 | Ok(None) => return None, |
| 459 | Err(err) => return Some(Err(err)), |
| 460 | }; |
| 461 | |
| 462 | let info = self.0.inner.reader.info(); |
| 463 | let fc = info.frame_control().unwrap(); |
| 464 | // PNG delays are rations in seconds. |
| 465 | let num = u32::from(fc.delay_num) * 1_000u32; |
| 466 | let denom = match fc.delay_den { |
| 467 | // The standard dictates to replace by 100 when the denominator is 0. |
| 468 | 0 => 100, |
| 469 | d => u32::from(d), |
| 470 | }; |
| 471 | let delay = Delay::from_ratio(Ratio::new(num, denom)); |
| 472 | Some(Ok(Frame::from_parts(image, 0, 0, delay))) |
| 473 | } |
| 474 | } |
| 475 | |
| 476 | Frames::new(Box::new(FrameIterator(self))) |
| 477 | } |
| 478 | } |
| 479 | |
| 480 | /// PNG encoder |
| 481 | pub struct PngEncoder<W: Write> { |
| 482 | w: W, |
| 483 | compression: CompressionType, |
| 484 | filter: FilterType, |
| 485 | icc_profile: Vec<u8>, |
| 486 | } |
| 487 | |
| 488 | /// Compression level of a PNG encoder. The default setting is `Fast`. |
| 489 | #[derive (Clone, Copy, Debug, Eq, PartialEq)] |
| 490 | #[non_exhaustive ] |
| 491 | #[derive (Default)] |
| 492 | pub enum CompressionType { |
| 493 | /// Default compression level |
| 494 | Default, |
| 495 | /// Fast, minimal compression |
| 496 | #[default] |
| 497 | Fast, |
| 498 | /// High compression level |
| 499 | Best, |
| 500 | } |
| 501 | |
| 502 | /// Filter algorithms used to process image data to improve compression. |
| 503 | /// |
| 504 | /// The default filter is `Adaptive`. |
| 505 | #[derive (Clone, Copy, Debug, Eq, PartialEq)] |
| 506 | #[non_exhaustive ] |
| 507 | #[derive (Default)] |
| 508 | pub enum FilterType { |
| 509 | /// No processing done, best used for low bit depth grayscale or data with a |
| 510 | /// low color count |
| 511 | NoFilter, |
| 512 | /// Filters based on previous pixel in the same scanline |
| 513 | Sub, |
| 514 | /// Filters based on the scanline above |
| 515 | Up, |
| 516 | /// Filters based on the average of left and right neighbor pixels |
| 517 | Avg, |
| 518 | /// Algorithm that takes into account the left, upper left, and above pixels |
| 519 | Paeth, |
| 520 | /// Uses a heuristic to select one of the preceding filters for each |
| 521 | /// scanline rather than one filter for the entire image |
| 522 | #[default] |
| 523 | Adaptive, |
| 524 | } |
| 525 | |
| 526 | #[derive (Clone, Copy, Debug, Eq, PartialEq)] |
| 527 | #[non_exhaustive ] |
| 528 | enum BadPngRepresentation { |
| 529 | ColorType(ExtendedColorType), |
| 530 | } |
| 531 | |
| 532 | impl<W: Write> PngEncoder<W> { |
| 533 | /// Create a new encoder that writes its output to ```w``` |
| 534 | pub fn new(w: W) -> PngEncoder<W> { |
| 535 | PngEncoder { |
| 536 | w, |
| 537 | compression: CompressionType::default(), |
| 538 | filter: FilterType::default(), |
| 539 | icc_profile: Vec::new(), |
| 540 | } |
| 541 | } |
| 542 | |
| 543 | /// Create a new encoder that writes its output to `w` with `CompressionType` `compression` and |
| 544 | /// `FilterType` `filter`. |
| 545 | /// |
| 546 | /// It is best to view the options as a _hint_ to the implementation on the smallest or fastest |
| 547 | /// option for encoding a particular image. That is, using options that map directly to a PNG |
| 548 | /// image parameter will use this parameter where possible. But variants that have no direct |
| 549 | /// mapping may be interpreted differently in minor versions. The exact output is expressly |
| 550 | /// __not__ part of the SemVer stability guarantee. |
| 551 | /// |
| 552 | /// Note that it is not optimal to use a single filter type, so an adaptive |
| 553 | /// filter type is selected as the default. The filter which best minimizes |
| 554 | /// file size may change with the type of compression used. |
| 555 | pub fn new_with_quality( |
| 556 | w: W, |
| 557 | compression: CompressionType, |
| 558 | filter: FilterType, |
| 559 | ) -> PngEncoder<W> { |
| 560 | PngEncoder { |
| 561 | w, |
| 562 | compression, |
| 563 | filter, |
| 564 | icc_profile: Vec::new(), |
| 565 | } |
| 566 | } |
| 567 | |
| 568 | fn encode_inner( |
| 569 | self, |
| 570 | data: &[u8], |
| 571 | width: u32, |
| 572 | height: u32, |
| 573 | color: ExtendedColorType, |
| 574 | ) -> ImageResult<()> { |
| 575 | let (ct, bits) = match color { |
| 576 | ExtendedColorType::L8 => (png::ColorType::Grayscale, png::BitDepth::Eight), |
| 577 | ExtendedColorType::L16 => (png::ColorType::Grayscale, png::BitDepth::Sixteen), |
| 578 | ExtendedColorType::La8 => (png::ColorType::GrayscaleAlpha, png::BitDepth::Eight), |
| 579 | ExtendedColorType::La16 => (png::ColorType::GrayscaleAlpha, png::BitDepth::Sixteen), |
| 580 | ExtendedColorType::Rgb8 => (png::ColorType::Rgb, png::BitDepth::Eight), |
| 581 | ExtendedColorType::Rgb16 => (png::ColorType::Rgb, png::BitDepth::Sixteen), |
| 582 | ExtendedColorType::Rgba8 => (png::ColorType::Rgba, png::BitDepth::Eight), |
| 583 | ExtendedColorType::Rgba16 => (png::ColorType::Rgba, png::BitDepth::Sixteen), |
| 584 | _ => { |
| 585 | return Err(ImageError::Unsupported( |
| 586 | UnsupportedError::from_format_and_kind( |
| 587 | ImageFormat::Png.into(), |
| 588 | UnsupportedErrorKind::Color(color), |
| 589 | ), |
| 590 | )) |
| 591 | } |
| 592 | }; |
| 593 | let comp = match self.compression { |
| 594 | CompressionType::Default => png::Compression::Default, |
| 595 | CompressionType::Best => png::Compression::Best, |
| 596 | _ => png::Compression::Fast, |
| 597 | }; |
| 598 | let (filter, adaptive_filter) = match self.filter { |
| 599 | FilterType::NoFilter => ( |
| 600 | png::FilterType::NoFilter, |
| 601 | png::AdaptiveFilterType::NonAdaptive, |
| 602 | ), |
| 603 | FilterType::Sub => (png::FilterType::Sub, png::AdaptiveFilterType::NonAdaptive), |
| 604 | FilterType::Up => (png::FilterType::Up, png::AdaptiveFilterType::NonAdaptive), |
| 605 | FilterType::Avg => (png::FilterType::Avg, png::AdaptiveFilterType::NonAdaptive), |
| 606 | FilterType::Paeth => (png::FilterType::Paeth, png::AdaptiveFilterType::NonAdaptive), |
| 607 | FilterType::Adaptive => (png::FilterType::Sub, png::AdaptiveFilterType::Adaptive), |
| 608 | }; |
| 609 | |
| 610 | let mut info = png::Info::with_size(width, height); |
| 611 | |
| 612 | if !self.icc_profile.is_empty() { |
| 613 | info.icc_profile = Some(Cow::Borrowed(&self.icc_profile)); |
| 614 | } |
| 615 | |
| 616 | let mut encoder = |
| 617 | png::Encoder::with_info(self.w, info).map_err(|e| ImageError::IoError(e.into()))?; |
| 618 | |
| 619 | encoder.set_color(ct); |
| 620 | encoder.set_depth(bits); |
| 621 | encoder.set_compression(comp); |
| 622 | encoder.set_filter(filter); |
| 623 | encoder.set_adaptive_filter(adaptive_filter); |
| 624 | let mut writer = encoder |
| 625 | .write_header() |
| 626 | .map_err(|e| ImageError::IoError(e.into()))?; |
| 627 | writer |
| 628 | .write_image_data(data) |
| 629 | .map_err(|e| ImageError::IoError(e.into())) |
| 630 | } |
| 631 | } |
| 632 | |
| 633 | impl<W: Write> ImageEncoder for PngEncoder<W> { |
| 634 | /// Write a PNG image with the specified width, height, and color type. |
| 635 | /// |
| 636 | /// For color types with 16-bit per channel or larger, the contents of `buf` should be in |
| 637 | /// native endian. `PngEncoder` will automatically convert to big endian as required by the |
| 638 | /// underlying PNG format. |
| 639 | #[track_caller ] |
| 640 | fn write_image( |
| 641 | self, |
| 642 | buf: &[u8], |
| 643 | width: u32, |
| 644 | height: u32, |
| 645 | color_type: ExtendedColorType, |
| 646 | ) -> ImageResult<()> { |
| 647 | use byteorder_lite::{BigEndian, ByteOrder, NativeEndian}; |
| 648 | use ExtendedColorType::*; |
| 649 | |
| 650 | let expected_buffer_len = color_type.buffer_size(width, height); |
| 651 | assert_eq!( |
| 652 | expected_buffer_len, |
| 653 | buf.len() as u64, |
| 654 | "Invalid buffer length: expected {expected_buffer_len} got {} for {width}x {height} image" , |
| 655 | buf.len(), |
| 656 | ); |
| 657 | |
| 658 | // PNG images are big endian. For 16 bit per channel and larger types, |
| 659 | // the buffer may need to be reordered to big endian per the |
| 660 | // contract of `write_image`. |
| 661 | // TODO: assumes equal channel bit depth. |
| 662 | match color_type { |
| 663 | L8 | La8 | Rgb8 | Rgba8 => { |
| 664 | // No reodering necessary for u8 |
| 665 | self.encode_inner(buf, width, height, color_type) |
| 666 | } |
| 667 | L16 | La16 | Rgb16 | Rgba16 => { |
| 668 | // Because the buffer is immutable and the PNG encoder does not |
| 669 | // yet take Write/Read traits, create a temporary buffer for |
| 670 | // big endian reordering. |
| 671 | let mut reordered = vec![0; buf.len()]; |
| 672 | buf.chunks_exact(2) |
| 673 | .zip(reordered.chunks_exact_mut(2)) |
| 674 | .for_each(|(b, r)| BigEndian::write_u16(r, NativeEndian::read_u16(b))); |
| 675 | self.encode_inner(&reordered, width, height, color_type) |
| 676 | } |
| 677 | _ => Err(ImageError::Encoding(EncodingError::new( |
| 678 | ImageFormat::Png.into(), |
| 679 | BadPngRepresentation::ColorType(color_type), |
| 680 | ))), |
| 681 | } |
| 682 | } |
| 683 | |
| 684 | fn set_icc_profile(&mut self, icc_profile: Vec<u8>) -> Result<(), UnsupportedError> { |
| 685 | self.icc_profile = icc_profile; |
| 686 | Ok(()) |
| 687 | } |
| 688 | } |
| 689 | |
| 690 | impl ImageError { |
| 691 | fn from_png(err: png::DecodingError) -> ImageError { |
| 692 | use png::DecodingError::*; |
| 693 | match err { |
| 694 | IoError(err: Error) => ImageError::IoError(err), |
| 695 | // The input image was not a valid PNG. |
| 696 | err: DecodingError @ Format(_) => { |
| 697 | ImageError::Decoding(DecodingError::new(format:ImageFormat::Png.into(), err)) |
| 698 | } |
| 699 | // Other is used when: |
| 700 | // - The decoder is polled for more animation frames despite being done (or not being animated |
| 701 | // in the first place). |
| 702 | // - The output buffer does not have the required size. |
| 703 | err: DecodingError @ Parameter(_) => ImageError::Parameter(ParameterError::from_kind( |
| 704 | ParameterErrorKind::Generic(err.to_string()), |
| 705 | )), |
| 706 | LimitsExceeded => { |
| 707 | ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory)) |
| 708 | } |
| 709 | } |
| 710 | } |
| 711 | } |
| 712 | |
| 713 | impl fmt::Display for BadPngRepresentation { |
| 714 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| 715 | match self { |
| 716 | Self::ColorType(color_type: &ExtendedColorType) => { |
| 717 | write!(f, "The color {color_type:?} can not be represented in PNG." ) |
| 718 | } |
| 719 | } |
| 720 | } |
| 721 | } |
| 722 | |
| 723 | impl std::error::Error for BadPngRepresentation {} |
| 724 | |
| 725 | #[cfg (test)] |
| 726 | mod tests { |
| 727 | use super::*; |
| 728 | use std::io::{BufReader, Cursor, Read}; |
| 729 | |
| 730 | #[test ] |
| 731 | fn ensure_no_decoder_off_by_one() { |
| 732 | let dec = PngDecoder::new(BufReader::new( |
| 733 | std::fs::File::open("tests/images/png/bugfixes/debug_triangle_corners_widescreen.png" ) |
| 734 | .unwrap(), |
| 735 | )) |
| 736 | .expect("Unable to read PNG file (does it exist?)" ); |
| 737 | |
| 738 | assert_eq![(2000, 1000), dec.dimensions()]; |
| 739 | |
| 740 | assert_eq![ |
| 741 | ColorType::Rgb8, |
| 742 | dec.color_type(), |
| 743 | "Image MUST have the Rgb8 format" |
| 744 | ]; |
| 745 | |
| 746 | let correct_bytes = crate::image::decoder_to_vec(dec) |
| 747 | .expect("Unable to read file" ) |
| 748 | .bytes() |
| 749 | .map(|x| x.expect("Unable to read byte" )) |
| 750 | .collect::<Vec<u8>>(); |
| 751 | |
| 752 | assert_eq![6_000_000, correct_bytes.len()]; |
| 753 | } |
| 754 | |
| 755 | #[test ] |
| 756 | fn underlying_error() { |
| 757 | use std::error::Error; |
| 758 | |
| 759 | let mut not_png = |
| 760 | std::fs::read("tests/images/png/bugfixes/debug_triangle_corners_widescreen.png" ) |
| 761 | .unwrap(); |
| 762 | not_png[0] = 0; |
| 763 | |
| 764 | let error = PngDecoder::new(Cursor::new(¬_png)).err().unwrap(); |
| 765 | let _ = error |
| 766 | .source() |
| 767 | .unwrap() |
| 768 | .downcast_ref::<png::DecodingError>() |
| 769 | .expect("Caused by a png error" ); |
| 770 | } |
| 771 | |
| 772 | #[test ] |
| 773 | fn encode_bad_color_type() { |
| 774 | // regression test for issue #1663 |
| 775 | let image = DynamicImage::new_rgb32f(1, 1); |
| 776 | let mut target = Cursor::new(vec![]); |
| 777 | let _ = image.write_to(&mut target, ImageFormat::Png); |
| 778 | } |
| 779 | } |
| 780 | |