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 | |