diff --git a/src/codecs/pnm/encoder.rs b/src/codecs/pnm/encoder.rs index b8b21f241c..f866d0ca3b 100644 --- a/src/codecs/pnm/encoder.rs +++ b/src/codecs/pnm/encoder.rs @@ -18,7 +18,7 @@ use crate::{ImageEncoder, ImageFormat}; use byteorder_lite::{BigEndian, WriteBytesExt}; enum HeaderStrategy { - Dynamic, + DynamicPnm, Subtype(PnmSubtype), Chosen(PnmHeader), } @@ -82,23 +82,25 @@ enum TupleEncoding<'a> { impl PnmEncoder { /// Create new `PnmEncoder` from the `writer`. /// - /// The encoded images will have some `pnm` format. If more control over the image type is - /// required, use either one of `with_subtype` or `with_header`. For more information on the - /// behaviour, see `with_dynamic_header`. + /// By default, this will create an image in PAM format, not PNM. If a different + /// format or more control over the image type is required, use any of `with_subtype`, + /// `with_header`, or `with_dynamic_pnm_header`. + /// + /// For more information on the default behaviour, see `with_dynamic_header`. pub fn new(writer: W) -> Self { PnmEncoder { writer, - header: HeaderStrategy::Dynamic, + header: HeaderStrategy::Subtype(PnmSubtype::ArbitraryMap), } } - /// Encode a specific pnm subtype image. + /// Encode a specific PNM subtype or PAM image. /// /// The magic number and encoding type will be chosen as provided while the rest of the header /// data will be generated dynamically. Trying to encode incompatible images (e.g. encoding an /// RGB image as Graymap) will result in an error. /// - /// This will overwrite the effect of earlier calls to `with_header` and `with_dynamic_header`. + /// This will overwrite the effect of earlier calls to set the header or subtype. pub fn with_subtype(self, subtype: PnmSubtype) -> Self { PnmEncoder { writer: self.writer, @@ -114,7 +116,7 @@ impl PnmEncoder { /// /// Choose this option if you want a lossless decoding/encoding round trip. /// - /// This will overwrite the effect of earlier calls to `with_subtype` and `with_dynamic_header`. + /// This will overwrite the effect of earlier calls to set the header or subtype. pub fn with_header(self, header: PnmHeader) -> Self { PnmEncoder { writer: self.writer, @@ -122,17 +124,33 @@ impl PnmEncoder { } } - /// Create the header dynamically for each image. + /// Encode a PAM image. /// - /// This is the default option upon creation of the encoder. With this, most images should be - /// encodable but the specific format chosen is out of the users control. The pnm subtype is - /// chosen arbitrarily by the library. + /// This is equivalent to `with_subtype(PnmSubtype::ArbitraryMap)`. /// - /// This will overwrite the effect of earlier calls to `with_subtype` and `with_header`. + /// This will overwrite the effect of earlier calls to set the header or subtype. pub fn with_dynamic_header(self) -> Self { PnmEncoder { writer: self.writer, - header: HeaderStrategy::Dynamic, + header: HeaderStrategy::Subtype(PnmSubtype::ArbitraryMap), + } + } + + /// Automatically choose a PNM header for each image. + /// + /// With this, most images without an alpha channel should be encodable but the specific + /// format chosen is out of the users control. + /// + /// The chosen format will be one of PBM (black and white), PGM (grayscale), or PPM (color). + /// + /// To encode an image with an alpha channel, use `with_subtype(PnmSubtype::Arbitrary)` + /// to configure a PAM header. + /// + /// This will overwrite the effect of earlier calls to set the header or subtype. + pub fn with_dynamic_pnm_header(self) -> Self { + PnmEncoder { + writer: self.writer, + header: HeaderStrategy::DynamicPnm, } } @@ -201,27 +219,24 @@ impl PnmEncoder { height: u32, color: ExtendedColorType, ) -> ImageResult<()> { - match self.header { - HeaderStrategy::Dynamic => self.write_dynamic_header(samples, width, height, color), + let header = match self.header { + HeaderStrategy::DynamicPnm => &Self::choose_dynamic_pnm_header(width, height, color)?, HeaderStrategy::Subtype(subtype) => { - self.write_subtyped_header(subtype, samples, width, height, color) + &Self::choose_subtyped_header(subtype, width, height, color)? } - HeaderStrategy::Chosen(ref header) => { - Self::write_with_header(&mut self.writer, header, samples, width, height, color) - } - } + HeaderStrategy::Chosen(ref header) => header, + }; + Self::write_with_header(&mut self.writer, header, samples, width, height, color) } - /// Choose any valid pnm format that the image can be expressed in and write its header. + /// Choose any valid PNM format that the image can be expressed in and write its header. /// /// Returns how the body should be written if successful. - fn write_dynamic_header( - &mut self, - image: FlatSamples, + fn choose_pam_header( width: u32, height: u32, color: ExtendedColorType, - ) -> ImageResult<()> { + ) -> ImageResult { let depth = u32::from(color.channel_count()); let (maxval, tupltype) = match color { ExtendedColorType::L1 => (1, ArbitraryTuplType::BlackAndWhite), @@ -237,14 +252,14 @@ impl PnmEncoder { _ => { return Err(ImageError::Unsupported( UnsupportedError::from_format_and_kind( - ImageFormat::Pnm.into(), + ImageFormat::Pam.into(), UnsupportedErrorKind::Color(color), ), )) } }; - let header = PnmHeader { + Ok(PnmHeader { decoded: HeaderRecord::Arbitrary(ArbitraryHeader { width, height, @@ -253,43 +268,58 @@ impl PnmEncoder { tupltype: Some(tupltype), }), encoded: None, - }; + }) + } - Self::write_with_header(&mut self.writer, &header, image, width, height, color) + /// Choose any valid PNM format that the image can be expressed in and write its header. + /// + /// Returns how the body should be written if successful. + fn choose_dynamic_pnm_header( + width: u32, + height: u32, + color: ExtendedColorType, + ) -> ImageResult { + let subtype = match color { + ExtendedColorType::L1 => PnmSubtype::Bitmap(SampleEncoding::Binary), + ExtendedColorType::L8 | ExtendedColorType::L16 => { + PnmSubtype::Graymap(SampleEncoding::Binary) + } + ExtendedColorType::Rgb8 | ExtendedColorType::Rgb16 => { + PnmSubtype::Pixmap(SampleEncoding::Binary) + } + _ => { + return Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Pnm.into(), + UnsupportedErrorKind::Color(color), + ), + )) + } + }; + Self::choose_subtyped_header(subtype, width, height, color) } - /// Try to encode the image with the chosen format, give its corresponding pixel encoding type. - fn write_subtyped_header( - &mut self, + /// Choose how to encode the image with the chosen format, given its corresponding pixel encoding type. + fn choose_subtyped_header( subtype: PnmSubtype, - image: FlatSamples, width: u32, height: u32, color: ExtendedColorType, - ) -> ImageResult<()> { - let header = match (subtype, color) { - (PnmSubtype::ArbitraryMap, color) => { - return self.write_dynamic_header(image, width, height, color) - } - (PnmSubtype::Pixmap(encoding), ExtendedColorType::Rgb8) => PnmHeader { - decoded: HeaderRecord::Pixmap(PixmapHeader { - encoding, - width, - height, - maxval: 255, - }), - encoded: None, - }, - (PnmSubtype::Graymap(encoding), ExtendedColorType::L8) => PnmHeader { - decoded: HeaderRecord::Graymap(GraymapHeader { - encoding, - width, - height, - maxwhite: 255, - }), - encoded: None, - }, - (PnmSubtype::Bitmap(encoding), ExtendedColorType::L8 | ExtendedColorType::L1) => { + ) -> ImageResult { + Ok(match subtype { + PnmSubtype::Bitmap(encoding) => { + if !matches!( + color, + ExtendedColorType::L1 | ExtendedColorType::L8 | ExtendedColorType::L16 + ) { + return Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Pbm.into(), + UnsupportedErrorKind::Color(color), + ), + )); + } + PnmHeader { decoded: HeaderRecord::Bitmap(BitmapHeader { encoding, @@ -299,17 +329,56 @@ impl PnmEncoder { encoded: None, } } - (_, _) => { - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Pnm.into(), - UnsupportedErrorKind::Color(color), - ), - )) + PnmSubtype::Graymap(encoding) => { + let maxwhite = match color { + ExtendedColorType::L8 => 0xff, + ExtendedColorType::L16 => 0xffff, + _ => { + return Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Pgm.into(), + UnsupportedErrorKind::Color(color), + ), + )) + } + }; + + PnmHeader { + decoded: HeaderRecord::Graymap(GraymapHeader { + encoding, + height, + width, + maxwhite, + }), + encoded: None, + } } - }; + PnmSubtype::Pixmap(encoding) => { + let maxval = match color { + ExtendedColorType::Rgb8 => 0xff, + ExtendedColorType::Rgb16 => 0xffff, + _ => { + return Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Ppm.into(), + UnsupportedErrorKind::Color(color), + ), + )) + } + }; - Self::write_with_header(&mut self.writer, &header, image, width, height, color) + PnmHeader { + decoded: HeaderRecord::Pixmap(PixmapHeader { + encoding, + height, + width, + maxval, + }), + encoded: None, + } + } + PnmSubtype::ArbitraryMap => Self::choose_pam_header(width, height, color)?, + }) } /// Try to encode the image with the chosen header, checking if values are correct. @@ -419,9 +488,10 @@ impl<'a> CheckedDimensions<'a> { ExtendedColorType::L1 | ExtendedColorType::L8 | ExtendedColorType::L16 => (), _ => { return Err(ImageError::Parameter(ParameterError::from_kind( - ParameterErrorKind::Generic( - "PBM format only support luma color types".to_owned(), - ), + ParameterErrorKind::Generic(format!( + "PBM format only support luma color types, not {:?}", + color + )), ))) } }, @@ -432,9 +502,10 @@ impl<'a> CheckedDimensions<'a> { ExtendedColorType::L1 | ExtendedColorType::L8 | ExtendedColorType::L16 => (), _ => { return Err(ImageError::Parameter(ParameterError::from_kind( - ParameterErrorKind::Generic( - "PGM format only support luma color types".to_owned(), - ), + ParameterErrorKind::Generic(format!( + "PGM format only support luma color types, not {:?}", + color + )), ))) } }, @@ -442,12 +513,13 @@ impl<'a> CheckedDimensions<'a> { decoded: HeaderRecord::Pixmap(_), .. } => match color { - ExtendedColorType::Rgb8 => (), + ExtendedColorType::Rgb8 | ExtendedColorType::Rgb16 => (), _ => { return Err(ImageError::Parameter(ParameterError::from_kind( - ParameterErrorKind::Generic( - "PPM format only support ExtendedColorType::Rgb8".to_owned(), - ), + ParameterErrorKind::Generic(format!( + "PPM format only support Rgb8 or Rgb16, not {:?}", + color + )), ))) } }, @@ -461,12 +533,13 @@ impl<'a> CheckedDimensions<'a> { .. } => match (tupltype, color) { (&Some(ArbitraryTuplType::BlackAndWhite), ExtendedColorType::L1) => (), - (&Some(ArbitraryTuplType::BlackAndWhiteAlpha), ExtendedColorType::La8) => (), + (&Some(ArbitraryTuplType::BlackAndWhiteAlpha), ExtendedColorType::La1) => (), (&Some(ArbitraryTuplType::Grayscale), ExtendedColorType::L1) => (), (&Some(ArbitraryTuplType::Grayscale), ExtendedColorType::L8) => (), (&Some(ArbitraryTuplType::Grayscale), ExtendedColorType::L16) => (), (&Some(ArbitraryTuplType::GrayscaleAlpha), ExtendedColorType::La8) => (), + (&Some(ArbitraryTuplType::GrayscaleAlpha), ExtendedColorType::La16) => (), (&Some(ArbitraryTuplType::RGB), ExtendedColorType::Rgb8) => (), (&Some(ArbitraryTuplType::RGB), ExtendedColorType::Rgb16) => (), @@ -484,9 +557,10 @@ impl<'a> CheckedDimensions<'a> { } _ => { return Err(ImageError::Parameter(ParameterError::from_kind( - ParameterErrorKind::Generic( - "Invalid color type for selected PAM color type".to_owned(), - ), + ParameterErrorKind::Generic(format!( + "Invalid color type {:?} for selected PAM color type {:?}", + color, tupltype + )), ))) } }, @@ -511,7 +585,7 @@ impl<'a> CheckedHeaderColor<'a> { // We trust the image color bit count to be correct at least. let max_sample = match self.color { ExtendedColorType::Unknown(n) if n <= 16 => (1 << n) - 1, - ExtendedColorType::L1 => 1, + ExtendedColorType::L1 | ExtendedColorType::La1 => 1, ExtendedColorType::L8 | ExtendedColorType::La8 | ExtendedColorType::Rgb8 @@ -728,33 +802,6 @@ impl TupleEncoding<'_> { } } -#[test] -fn pbm_allows_black() { - let imgbuf = crate::DynamicImage::new_luma8(50, 50); - - let mut buffer = vec![]; - let encoder = - PnmEncoder::new(&mut buffer).with_subtype(PnmSubtype::Bitmap(SampleEncoding::Ascii)); - - imgbuf - .write_with_encoder(encoder) - .expect("all-zeroes is a black image"); -} - -#[test] -fn pbm_allows_white() { - let imgbuf = - crate::DynamicImage::ImageLuma8(crate::ImageBuffer::from_pixel(50, 50, crate::Luma([1]))); - - let mut buffer = vec![]; - let encoder = - PnmEncoder::new(&mut buffer).with_subtype(PnmSubtype::Bitmap(SampleEncoding::Ascii)); - - imgbuf - .write_with_encoder(encoder) - .expect("all-zeroes is a white image"); -} - #[test] fn pbm_verifies_pixels() { let imgbuf = diff --git a/src/codecs/pnm/mod.rs b/src/codecs/pnm/mod.rs index ef4efcd5d3..ee25e2b929 100644 --- a/src/codecs/pnm/mod.rs +++ b/src/codecs/pnm/mod.rs @@ -1,9 +1,18 @@ -//! Decoding of netpbm image formats (pbm, pgm, ppm and pam). +//! Decoding and Encoding of netpbm image formats (PBM, PGM, PPM, PNM, PAM). //! -//! The formats pbm, pgm and ppm are fully supported. Only the official subformats -//! (`BLACKANDWHITE`, `GRAYSCALE`, `RGB`, `BLACKANDWHITE_ALPHA`, `GRAYSCALE_ALPHA`, -//! and `RGB_ALPHA`) of pam are supported; custom tuple types have no clear -//! interpretation as an image and will be rejected. +//! The PnmDecoder supports all PBM, PGM, and PPM subformats. (PNM images may contain +//! any of PBM, PGM, or PPM content.) Only the official subformats (`BLACKANDWHITE`, +//! `GRAYSCALE`, `RGB`, `BLACKANDWHITE_ALPHA`, `GRAYSCALE_ALPHA`, and `RGB_ALPHA`) +//! of PAM are decodable; custom tuple types have no clear interpretation as an +//! image and will be rejected. +//! +//! # Related Links +//! * - specification for PBM +//! * - specification for PGM +//! * - specification for PPM +//! * - definition for PNM +//! * - specification for PAM + use self::autobreak::AutoBreak; pub use self::decoder::PnmDecoder; pub use self::encoder::PnmEncoder; diff --git a/src/io/format.rs b/src/io/format.rs index 7f2a0b029e..d78bb1b7d1 100644 --- a/src/io/format.rs +++ b/src/io/format.rs @@ -21,9 +21,21 @@ pub enum ImageFormat { /// An Image in WEBP Format WebP, + /// An Image in PBM Format + Pbm, + + /// An Image in PGM Format + Pgm, + + /// An Image in PPM Format + Ppm, + /// An Image in general PNM Format Pnm, + /// An Image in PAM Format + Pam, + /// An Image in TIFF Format Tiff, @@ -84,7 +96,11 @@ impl ImageFormat { "ico" => ImageFormat::Ico, "hdr" => ImageFormat::Hdr, "exr" => ImageFormat::OpenExr, - "pbm" | "pam" | "ppm" | "pgm" | "pnm" => ImageFormat::Pnm, + "pbm" => ImageFormat::Pbm, + "pgm" => ImageFormat::Pgm, + "ppm" => ImageFormat::Ppm, + "pnm" => ImageFormat::Pnm, + "pam" => ImageFormat::Pam, "ff" => ImageFormat::Farbfeld, "qoi" => ImageFormat::Qoi, _ => return None, @@ -154,10 +170,11 @@ impl ImageFormat { "image/x-icon" | "image/vnd.microsoft.icon" => Some(ImageFormat::Ico), "image/vnd.radiance" => Some(ImageFormat::Hdr), "image/x-exr" => Some(ImageFormat::OpenExr), - "image/x-portable-bitmap" - | "image/x-portable-graymap" - | "image/x-portable-pixmap" - | "image/x-portable-anymap" => Some(ImageFormat::Pnm), + "image/x-portable-bitmap" => Some(ImageFormat::Pbm), + "image/x-portable-graymap" => Some(ImageFormat::Pgm), + "image/x-portable-pixmap" => Some(ImageFormat::Ppm), + "image/x-portable-anymap" => Some(ImageFormat::Ppm), + "image/x-portable-arbitrarymap" => Some(ImageFormat::Pam), // Qoi's MIME type is being worked on. // See: https://github.com/phoboslab/qoi/issues/167 "image/x-qoi" => Some(ImageFormat::Qoi), @@ -200,8 +217,11 @@ impl ImageFormat { ImageFormat::Ico => "image/x-icon", ImageFormat::Hdr => "image/vnd.radiance", ImageFormat::OpenExr => "image/x-exr", - // return the most general MIME type + ImageFormat::Pbm => "image/x-portable-bitmap", + ImageFormat::Pgm => "image/x-portable-graymap", + ImageFormat::Ppm => "image/x-portable-pixmap", ImageFormat::Pnm => "image/x-portable-anymap", + ImageFormat::Pam => "image/x-portable-arbitrarymap", // Qoi's MIME type is being worked on. // See: https://github.com/phoboslab/qoi/issues/167 ImageFormat::Qoi => "image/x-qoi", @@ -226,7 +246,11 @@ impl ImageFormat { ImageFormat::Ico => true, ImageFormat::Hdr => true, ImageFormat::OpenExr => true, + ImageFormat::Pbm => true, + ImageFormat::Pgm => true, + ImageFormat::Ppm => true, ImageFormat::Pnm => true, + ImageFormat::Pam => true, ImageFormat::Farbfeld => true, ImageFormat::Avif => true, ImageFormat::Qoi => true, @@ -246,7 +270,11 @@ impl ImageFormat { ImageFormat::Bmp => true, ImageFormat::Tiff => true, ImageFormat::Tga => true, + ImageFormat::Pbm => true, + ImageFormat::Pgm => true, + ImageFormat::Ppm => true, ImageFormat::Pnm => true, + ImageFormat::Pam => true, ImageFormat::Farbfeld => true, ImageFormat::Avif => true, ImageFormat::WebP => true, @@ -273,7 +301,11 @@ impl ImageFormat { ImageFormat::Jpeg => &["jpg", "jpeg"], ImageFormat::Gif => &["gif"], ImageFormat::WebP => &["webp"], - ImageFormat::Pnm => &["pbm", "pam", "ppm", "pgm", "pnm"], + ImageFormat::Pbm => &["pbm"], + ImageFormat::Pgm => &["pgm"], + ImageFormat::Ppm => &["ppm"], + ImageFormat::Pnm => &["pnm"], + ImageFormat::Pam => &["pam"], ImageFormat::Tiff => &["tiff", "tif"], ImageFormat::Tga => &["tga"], ImageFormat::Bmp => &["bmp"], @@ -305,7 +337,11 @@ impl ImageFormat { ImageFormat::Ico => cfg!(feature = "ico"), ImageFormat::Hdr => cfg!(feature = "hdr"), ImageFormat::OpenExr => cfg!(feature = "exr"), - ImageFormat::Pnm => cfg!(feature = "pnm"), + ImageFormat::Pbm + | ImageFormat::Pgm + | ImageFormat::Ppm + | ImageFormat::Pnm + | ImageFormat::Pam => cfg!(feature = "pnm"), ImageFormat::Farbfeld => cfg!(feature = "ff"), ImageFormat::Avif => cfg!(feature = "avif-native"), ImageFormat::Qoi => cfg!(feature = "qoi"), @@ -327,7 +363,11 @@ impl ImageFormat { ImageFormat::Bmp => cfg!(feature = "bmp"), ImageFormat::Tiff => cfg!(feature = "tiff"), ImageFormat::Tga => cfg!(feature = "tga"), - ImageFormat::Pnm => cfg!(feature = "pnm"), + ImageFormat::Pbm + | ImageFormat::Pgm + | ImageFormat::Ppm + | ImageFormat::Pnm + | ImageFormat::Pam => cfg!(feature = "pnm"), ImageFormat::Farbfeld => cfg!(feature = "ff"), ImageFormat::Avif => cfg!(feature = "avif"), ImageFormat::WebP => cfg!(feature = "webp"), @@ -347,7 +387,11 @@ impl ImageFormat { ImageFormat::Bmp, ImageFormat::Tiff, ImageFormat::Tga, + ImageFormat::Pbm, + ImageFormat::Pgm, + ImageFormat::Ppm, ImageFormat::Pnm, + ImageFormat::Pam, ImageFormat::Farbfeld, ImageFormat::Avif, ImageFormat::WebP, @@ -385,10 +429,11 @@ mod tests { assert_eq!(from_path("./a.Ico").unwrap(), ImageFormat::Ico); assert_eq!(from_path("./a.hdr").unwrap(), ImageFormat::Hdr); assert_eq!(from_path("./a.exr").unwrap(), ImageFormat::OpenExr); - assert_eq!(from_path("./a.pbm").unwrap(), ImageFormat::Pnm); - assert_eq!(from_path("./a.pAM").unwrap(), ImageFormat::Pnm); - assert_eq!(from_path("./a.Ppm").unwrap(), ImageFormat::Pnm); - assert_eq!(from_path("./a.pgm").unwrap(), ImageFormat::Pnm); + assert_eq!(from_path("./a.pbm").unwrap(), ImageFormat::Pbm); + assert_eq!(from_path("./a.pgm").unwrap(), ImageFormat::Pgm); + assert_eq!(from_path("./a.Ppm").unwrap(), ImageFormat::Ppm); + assert_eq!(from_path("./a.pnm").unwrap(), ImageFormat::Pnm); + assert_eq!(from_path("./a.PAM").unwrap(), ImageFormat::Pam); assert_eq!(from_path("./a.AViF").unwrap(), ImageFormat::Avif); assert!(from_path("./a.txt").is_err()); assert!(from_path("./a").is_err()); @@ -396,11 +441,7 @@ mod tests { #[test] fn image_formats_are_recognized() { - use ImageFormat::*; - const ALL_FORMATS: &[ImageFormat] = &[ - Avif, Png, Jpeg, Gif, WebP, Pnm, Tiff, Tga, Bmp, Ico, Hdr, Farbfeld, OpenExr, - ]; - for &format in ALL_FORMATS { + for format in ImageFormat::all() { let mut file = Path::new("file.nothing").to_owned(); for ext in format.extensions_str() { assert!(file.set_extension(ext)); diff --git a/src/io/free_functions.rs b/src/io/free_functions.rs index 650de27a89..eae59697d8 100644 --- a/src/io/free_functions.rs +++ b/src/io/free_functions.rs @@ -67,7 +67,28 @@ pub(crate) fn encoder_for_format<'a, W: Write + Seek>( #[cfg(feature = "jpeg")] ImageFormat::Jpeg => Box::new(jpeg::JpegEncoder::new(buffered_write)), #[cfg(feature = "pnm")] - ImageFormat::Pnm => Box::new(pnm::PnmEncoder::new(buffered_write)), + ImageFormat::Pbm => Box::new( + pnm::PnmEncoder::new(buffered_write) + .with_subtype(pnm::PnmSubtype::Bitmap(pnm::SampleEncoding::Binary)), + ), + #[cfg(feature = "pnm")] + ImageFormat::Pgm => Box::new( + pnm::PnmEncoder::new(buffered_write) + .with_subtype(pnm::PnmSubtype::Graymap(pnm::SampleEncoding::Binary)), + ), + #[cfg(feature = "pnm")] + ImageFormat::Ppm => Box::new( + pnm::PnmEncoder::new(buffered_write) + .with_subtype(pnm::PnmSubtype::Pixmap(pnm::SampleEncoding::Binary)), + ), + #[cfg(feature = "pnm")] + ImageFormat::Pnm => { + Box::new(pnm::PnmEncoder::new(buffered_write).with_dynamic_pnm_header()) + } + #[cfg(feature = "pnm")] + ImageFormat::Pam => Box::new( + pnm::PnmEncoder::new(buffered_write).with_subtype(pnm::PnmSubtype::ArbitraryMap), + ), #[cfg(feature = "gif")] ImageFormat::Gif => Box::new(gif::GifEncoder::new(buffered_write)), #[cfg(feature = "ico")] @@ -120,13 +141,13 @@ static MAGIC_BYTES: [(&[u8], &[u8], ImageFormat); 21] = [ (b"\0\0\0\0ftypavif", b"\xFF\xFF\0\0", ImageFormat::Avif), (&[0x76, 0x2f, 0x31, 0x01], b"", ImageFormat::OpenExr), // = &exr::meta::magic_number::BYTES (b"qoif", b"", ImageFormat::Qoi), - (b"P1", b"", ImageFormat::Pnm), - (b"P2", b"", ImageFormat::Pnm), - (b"P3", b"", ImageFormat::Pnm), - (b"P4", b"", ImageFormat::Pnm), - (b"P5", b"", ImageFormat::Pnm), - (b"P6", b"", ImageFormat::Pnm), - (b"P7", b"", ImageFormat::Pnm), + (b"P1", b"", ImageFormat::Pbm), + (b"P2", b"", ImageFormat::Pgm), + (b"P3", b"", ImageFormat::Ppm), + (b"P4", b"", ImageFormat::Pbm), + (b"P5", b"", ImageFormat::Pgm), + (b"P6", b"", ImageFormat::Ppm), + (b"P7", b"", ImageFormat::Pam), (b"farbfeld", b"", ImageFormat::Farbfeld), ]; @@ -193,13 +214,15 @@ fn test_guess_format_agrees_with_extension() { for fmt in ImageFormat::all() { use ImageFormat::*; let found = found.contains(&fmt); - if matches!( - fmt, - Bmp | Farbfeld | Gif | Ico | Hdr | Jpeg | OpenExr | Png | Pnm | Qoi | Tiff | WebP - ) { - assert!(found, "No {fmt:?} test files found"); - } else { - assert!(!found, "Add `{fmt:?}` to test_guess_format"); + match fmt { + Pnm => (), // Pnm images are always detected as one of Pbm, Pgm, Ppm + Bmp | Farbfeld | Gif | Ico | Hdr | Jpeg | OpenExr | Png | Pbm | Pgm | Ppm | Pam + | Qoi | Tiff | WebP => { + assert!(found, "No {fmt:?} test files found"); + } + _ => { + assert!(!found, "Add `{fmt:?}` to test_guess_format"); + } } } diff --git a/src/io/image_reader_type.rs b/src/io/image_reader_type.rs index fa70045ed3..ce7fb50ec3 100644 --- a/src/io/image_reader_type.rs +++ b/src/io/image_reader_type.rs @@ -48,7 +48,7 @@ enum Format { /// /// It is also possible to make a guess based on the content. This is especially handy if the /// source is some blob in memory and you have constructed the reader in another way. Here is an -/// example with a `pnm` black-and-white subformat that encodes its pixel matrix with ascii values. +/// example with a `pbm` black-and-white subformat that encodes its pixel matrix with ascii values. /// /// ``` /// # use image::ImageError; @@ -64,7 +64,7 @@ enum Format { /// let mut reader = ImageReader::new(Cursor::new(raw_data)) /// .with_guessed_format() /// .expect("Cursor io never fails"); -/// assert_eq!(reader.format(), Some(ImageFormat::Pnm)); +/// assert_eq!(reader.format(), Some(ImageFormat::Pbm)); /// /// # #[cfg(feature = "pnm")] /// let image = reader.decode()?; @@ -224,7 +224,11 @@ impl<'a, R: 'a + BufRead + Seek> ImageReader { #[cfg(feature = "exr")] ImageFormat::OpenExr => Box::new(openexr::OpenExrDecoder::new(reader)?), #[cfg(feature = "pnm")] - ImageFormat::Pnm => Box::new(pnm::PnmDecoder::new(reader)?), + ImageFormat::Pbm + | ImageFormat::Pgm + | ImageFormat::Ppm + | ImageFormat::Pnm + | ImageFormat::Pam => Box::new(pnm::PnmDecoder::new(reader)?), #[cfg(feature = "ff")] ImageFormat::Farbfeld => Box::new(farbfeld::FarbfeldDecoder::new(reader)?), #[cfg(feature = "qoi")] diff --git a/src/lib.rs b/src/lib.rs index 4cd7e0c37c..b93463615f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -199,22 +199,22 @@ pub use crate::images::flat; /// /// # Supported formats /// -/// | Feature | Format | Notes -/// | ------- | -------- | ----- -/// | `avif` | AVIF | Decoding requires the `avif-native` feature, uses the libdav1d C library. -/// | `bmp` | BMP | -/// | `exr` | OpenEXR | -/// | `ff` | Farbfeld | -/// | `gif` | GIF | -/// | `hdr` | HDR | -/// | `ico` | ICO | -/// | `jpeg` | JPEG | -/// | `png` | PNG | -/// | `pnm` | PNM | -/// | `qoi` | QOI | -/// | `tga` | TGA | -/// | `tiff` | TIFF | -/// | `webp` | WebP | Only lossless encoding is currently supported. +/// | Feature | Format(s) | Notes +/// | ------- | ----------------------- | ----- +/// | `avif` | AVIF | Decoding requires the `avif-native` feature, uses the libdav1d C library. +/// | `bmp` | BMP | +/// | `exr` | OpenEXR | +/// | `ff` | Farbfeld | +/// | `gif` | GIF | +/// | `hdr` | HDR | +/// | `ico` | ICO | +/// | `jpeg` | JPEG | +/// | `png` | PNG | +/// | `pnm` | PBM, PGM, PPM, PNM, PAM | +/// | `qoi` | QOI | +/// | `tga` | TGA | +/// | `tiff` | TIFF | +/// | `webp` | WebP | Only lossless encoding is currently supported. /// /// ## A note on format specific features /// diff --git a/tests/images/pnm/pam/l1.pam b/tests/images/pnm/pam/l1.pam new file mode 100644 index 0000000000..6c0fac7a06 Binary files /dev/null and b/tests/images/pnm/pam/l1.pam differ diff --git a/tests/images/pnm/pam/l16.pam b/tests/images/pnm/pam/l16.pam new file mode 100644 index 0000000000..7e443a0047 Binary files /dev/null and b/tests/images/pnm/pam/l16.pam differ diff --git a/tests/images/pnm/pam/l8.pam b/tests/images/pnm/pam/l8.pam new file mode 100644 index 0000000000..71d6078200 Binary files /dev/null and b/tests/images/pnm/pam/l8.pam differ diff --git a/tests/images/pnm/pam/la1.pam b/tests/images/pnm/pam/la1.pam new file mode 100644 index 0000000000..bf3fb0603c Binary files /dev/null and b/tests/images/pnm/pam/la1.pam differ diff --git a/tests/images/pnm/pam/la16.pam b/tests/images/pnm/pam/la16.pam new file mode 100644 index 0000000000..f9af4a54f3 Binary files /dev/null and b/tests/images/pnm/pam/la16.pam differ diff --git a/tests/images/pnm/pam/la8.pam b/tests/images/pnm/pam/la8.pam new file mode 100644 index 0000000000..af748d6b13 Binary files /dev/null and b/tests/images/pnm/pam/la8.pam differ diff --git a/tests/images/pnm/pam/r16.pam b/tests/images/pnm/pam/r16.pam new file mode 100644 index 0000000000..f62f4674f5 Binary files /dev/null and b/tests/images/pnm/pam/r16.pam differ diff --git a/tests/images/pnm/pam/r8.pam b/tests/images/pnm/pam/r8.pam new file mode 100644 index 0000000000..d2c8bc9dc6 Binary files /dev/null and b/tests/images/pnm/pam/r8.pam differ diff --git a/tests/images/pnm/pam/ra16.pam b/tests/images/pnm/pam/ra16.pam new file mode 100644 index 0000000000..c5668053a5 Binary files /dev/null and b/tests/images/pnm/pam/ra16.pam differ diff --git a/tests/images/pnm/pam/ra8.pam b/tests/images/pnm/pam/ra8.pam new file mode 100644 index 0000000000..85a09a9aab Binary files /dev/null and b/tests/images/pnm/pam/ra8.pam differ diff --git a/tests/images/pbm/images/issue-794.pbm b/tests/images/pnm/pbm/issue-794.pbm similarity index 100% rename from tests/images/pbm/images/issue-794.pbm rename to tests/images/pnm/pbm/issue-794.pbm diff --git a/tests/images/pnm/pbm/l1a.pbm b/tests/images/pnm/pbm/l1a.pbm new file mode 100644 index 0000000000..e6bdbc5c9b --- /dev/null +++ b/tests/images/pnm/pbm/l1a.pbm @@ -0,0 +1,3 @@ +P1 + 9 7 1001100010110101 100110101100000100010 + 110101100110 10110011010001 diff --git a/tests/images/pnm/pbm/l1b.pnm b/tests/images/pnm/pbm/l1b.pnm new file mode 100644 index 0000000000..2965e914db Binary files /dev/null and b/tests/images/pnm/pbm/l1b.pnm differ diff --git a/tests/images/pbm/images/narrow.pbm b/tests/images/pnm/pbm/narrow.pbm similarity index 100% rename from tests/images/pbm/images/narrow.pbm rename to tests/images/pnm/pbm/narrow.pbm diff --git a/tests/images/pnm/pgm/l16a.pgm b/tests/images/pnm/pgm/l16a.pgm new file mode 100644 index 0000000000..216b6c653f --- /dev/null +++ b/tests/images/pnm/pgm/l16a.pgm @@ -0,0 +1,6 @@ +P2 +9 7 1285 +0 1014 1019 15 20 1254 1254 1254 41 969 70 75 985 86 1206 96 101 1217 +930 136 141 946 151 1158 162 167 1181 891 896 902 907 217 1111 1132 +1143 238 852 267 272 868 283 1063 293 298 1108 813 333 338 829 348 +1015 359 364 1072 774 399 404 790 414 968 1010 1033 435 diff --git a/tests/images/pnm/pgm/l16b.pnm b/tests/images/pnm/pgm/l16b.pnm new file mode 100644 index 0000000000..0f6719bede Binary files /dev/null and b/tests/images/pnm/pgm/l16b.pnm differ diff --git a/tests/images/pnm/pgm/l8a.pgm b/tests/images/pnm/pgm/l8a.pgm new file mode 100644 index 0000000000..8305cf8456 --- /dev/null +++ b/tests/images/pnm/pgm/l8a.pgm @@ -0,0 +1,3 @@ +P2 9 7 85 0 67 67 1 1 83 83 83 2 64 4 5 65 5 79 6 6 80 61 9 9 62 10 76 10 11 78 +59 59 59 60 14 73 75 75 15 56 17 18 57 18 70 19 19 73 53 22 22 55 23 +67 23 24 71 51 26 26 52 27 64 67 68 28 diff --git a/tests/images/pnm/pgm/l8b.pgm b/tests/images/pnm/pgm/l8b.pgm new file mode 100644 index 0000000000..1f5626152c Binary files /dev/null and b/tests/images/pnm/pgm/l8b.pgm differ diff --git a/tests/images/pnm/ppm/r16a.ppm b/tests/images/pnm/ppm/r16a.ppm new file mode 100644 index 0000000000..cff8751ca9 --- /dev/null +++ b/tests/images/pnm/ppm/r16a.ppm @@ -0,0 +1,16 @@ +P3 # Comment +# This is a comment +9 7 # This too +3855 +0 0 0 3855 2891 2141 3855 2891 2355 0 0 642 0 0 856 3855 3855 2570 +3855 3855 2570 3855 3855 2570 0 0 1713 3304 2891 1927 0 275 214 0 275 +428 3304 2891 2570 0 275 856 3549 3746 2570 0 275 1285 0 275 1499 3365 + 3848 2570 2753 2891 1927 0 550 214 0 550 428 2753 2891 2570 0 550 856 + 3243 3637 2570 0 550 1285 0 550 1499 2875 3841 2570 2202 2891 1927 +2202 2891 2141 2202 2891 2355 2202 2891 2570 0 826 856 2937 3528 2570 +2753 3671 2570 2570 3773 2570 0 826 1713 1652 2891 1927 0 1101 214 0 +1101 428 1652 2891 2570 0 1101 856 2631 3419 2570 0 1101 1285 0 1101 +1499 1896 3827 2570 1101 2891 1927 0 1376 214 0 1376 428 1101 2891 +2570 0 1376 856 2325 3311 2570 0 1376 1285 0 1376 1499 1407 3821 2570 +550 2891 1927 0 1652 214 0 1652 428 550 2891 2570 0 1652 856 2019 3202 + 2570 1652 3487 2570 1285 3691 2570 0 1652 1713 diff --git a/tests/images/pnm/ppm/r16b.ppm b/tests/images/pnm/ppm/r16b.ppm new file mode 100644 index 0000000000..7fe91883f4 Binary files /dev/null and b/tests/images/pnm/ppm/r16b.ppm differ diff --git a/tests/images/pnm/ppm/r8a.ppm b/tests/images/pnm/ppm/r8a.ppm new file mode 100644 index 0000000000..852be87a1c --- /dev/null +++ b/tests/images/pnm/ppm/r8a.ppm @@ -0,0 +1,10 @@ +P3 +9 7 51 +0 0 0 51 38 28 51 38 31 0 0 8 0 0 11 51 51 34 51 51 34 51 51 34 0 0 22 + 43 38 25 0 3 2 0 3 5 43 38 34 0 3 11 47 49 34 0 3 17 0 3 19 44 51 34 +36 38 25 0 7 2 0 7 5 36 38 34 0 7 11 43 48 34 0 7 17 0 7 19 38 50 34 +29 38 25 29 38 28 29 38 31 29 38 34 0 11 11 38 46 34 36 48 34 34 50 34 + 0 11 22 21 38 25 0 14 2 0 14 5 21 38 34 0 14 11 34 45 34 0 14 17 0 14 + 19 25 50 34 14 38 25 0 18 2 0 18 5 14 38 34 0 18 11 30 43 34 0 18 17 +0 18 19 18 50 34 7 38 25 0 21 2 0 21 5 7 38 34 0 21 11 26 42 34 21 46 +34 17 48 34 0 21 22 diff --git a/tests/images/pnm/ppm/r8b.pnm b/tests/images/pnm/ppm/r8b.pnm new file mode 100644 index 0000000000..7b3f14cba6 Binary files /dev/null and b/tests/images/pnm/ppm/r8b.pnm differ diff --git a/tests/reference/pnm/pam/l1.pam.png b/tests/reference/pnm/pam/l1.pam.png new file mode 100644 index 0000000000..7e1a00db79 Binary files /dev/null and b/tests/reference/pnm/pam/l1.pam.png differ diff --git a/tests/reference/pnm/pam/l16.pam.png b/tests/reference/pnm/pam/l16.pam.png new file mode 100644 index 0000000000..8548176e19 Binary files /dev/null and b/tests/reference/pnm/pam/l16.pam.png differ diff --git a/tests/reference/pnm/pam/l8.pam.png b/tests/reference/pnm/pam/l8.pam.png new file mode 100644 index 0000000000..c1f3ffd214 Binary files /dev/null and b/tests/reference/pnm/pam/l8.pam.png differ diff --git a/tests/reference/pnm/pam/la1.pam.png b/tests/reference/pnm/pam/la1.pam.png new file mode 100644 index 0000000000..4f8ee5eae9 Binary files /dev/null and b/tests/reference/pnm/pam/la1.pam.png differ diff --git a/tests/reference/pnm/pam/la16.pam.png b/tests/reference/pnm/pam/la16.pam.png new file mode 100644 index 0000000000..c595ecdb16 Binary files /dev/null and b/tests/reference/pnm/pam/la16.pam.png differ diff --git a/tests/reference/pnm/pam/la8.pam.png b/tests/reference/pnm/pam/la8.pam.png new file mode 100644 index 0000000000..f812a4d178 Binary files /dev/null and b/tests/reference/pnm/pam/la8.pam.png differ diff --git a/tests/reference/pnm/pam/r16.pam.png b/tests/reference/pnm/pam/r16.pam.png new file mode 100644 index 0000000000..af61e8bd26 Binary files /dev/null and b/tests/reference/pnm/pam/r16.pam.png differ diff --git a/tests/reference/pnm/pam/r8.pam.png b/tests/reference/pnm/pam/r8.pam.png new file mode 100644 index 0000000000..26bf9eca8b Binary files /dev/null and b/tests/reference/pnm/pam/r8.pam.png differ diff --git a/tests/reference/pnm/pam/ra16.pam.png b/tests/reference/pnm/pam/ra16.pam.png new file mode 100644 index 0000000000..f4509ff786 Binary files /dev/null and b/tests/reference/pnm/pam/ra16.pam.png differ diff --git a/tests/reference/pnm/pam/ra8.pam.png b/tests/reference/pnm/pam/ra8.pam.png new file mode 100644 index 0000000000..04587f8e19 Binary files /dev/null and b/tests/reference/pnm/pam/ra8.pam.png differ diff --git a/tests/reference/pbm/images/issue-794.pbm.png b/tests/reference/pnm/pbm/issue-794.pbm.png similarity index 100% rename from tests/reference/pbm/images/issue-794.pbm.png rename to tests/reference/pnm/pbm/issue-794.pbm.png diff --git a/tests/reference/pnm/pbm/l1a.pbm.png b/tests/reference/pnm/pbm/l1a.pbm.png new file mode 100644 index 0000000000..7e1a00db79 Binary files /dev/null and b/tests/reference/pnm/pbm/l1a.pbm.png differ diff --git a/tests/reference/pnm/pbm/l1b.pnm.png b/tests/reference/pnm/pbm/l1b.pnm.png new file mode 100644 index 0000000000..7e1a00db79 Binary files /dev/null and b/tests/reference/pnm/pbm/l1b.pnm.png differ diff --git a/tests/reference/pbm/images/narrow.pbm.png b/tests/reference/pnm/pbm/narrow.pbm.png similarity index 100% rename from tests/reference/pbm/images/narrow.pbm.png rename to tests/reference/pnm/pbm/narrow.pbm.png diff --git a/tests/reference/pnm/pgm/l16a.pgm.png b/tests/reference/pnm/pgm/l16a.pgm.png new file mode 100644 index 0000000000..6069b040e9 Binary files /dev/null and b/tests/reference/pnm/pgm/l16a.pgm.png differ diff --git a/tests/reference/pnm/pgm/l16b.pnm.png b/tests/reference/pnm/pgm/l16b.pnm.png new file mode 100644 index 0000000000..6069b040e9 Binary files /dev/null and b/tests/reference/pnm/pgm/l16b.pnm.png differ diff --git a/tests/reference/pnm/pgm/l8a.pgm.png b/tests/reference/pnm/pgm/l8a.pgm.png new file mode 100644 index 0000000000..e37a4dfc75 Binary files /dev/null and b/tests/reference/pnm/pgm/l8a.pgm.png differ diff --git a/tests/reference/pnm/pgm/l8b.pgm.png b/tests/reference/pnm/pgm/l8b.pgm.png new file mode 100644 index 0000000000..e37a4dfc75 Binary files /dev/null and b/tests/reference/pnm/pgm/l8b.pgm.png differ diff --git a/tests/reference/pnm/ppm/r16a.ppm.png b/tests/reference/pnm/ppm/r16a.ppm.png new file mode 100644 index 0000000000..8e768f4ad4 Binary files /dev/null and b/tests/reference/pnm/ppm/r16a.ppm.png differ diff --git a/tests/reference/pnm/ppm/r16b.ppm.png b/tests/reference/pnm/ppm/r16b.ppm.png new file mode 100644 index 0000000000..8e768f4ad4 Binary files /dev/null and b/tests/reference/pnm/ppm/r16b.ppm.png differ diff --git a/tests/reference/pnm/ppm/r8a.ppm.png b/tests/reference/pnm/ppm/r8a.ppm.png new file mode 100644 index 0000000000..fb928c1133 Binary files /dev/null and b/tests/reference/pnm/ppm/r8a.ppm.png differ diff --git a/tests/reference/pnm/ppm/r8b.pnm.png b/tests/reference/pnm/ppm/r8b.pnm.png new file mode 100644 index 0000000000..fb928c1133 Binary files /dev/null and b/tests/reference/pnm/ppm/r8b.pnm.png differ diff --git a/tests/save_pnm.rs b/tests/save_pnm.rs index 81eb2230b1..791a4d4b00 100644 --- a/tests/save_pnm.rs +++ b/tests/save_pnm.rs @@ -2,50 +2,98 @@ #![cfg(all(feature = "png", feature = "pnm"))] extern crate image; -use std::fs; +use std::{fs::File, io::Read, path::Path}; -use image::{GenericImageView as _, Luma, Rgb}; +fn compare_exact(dst_path: &str, ref_path: &str) { + let mut output = Vec::new(); + let mut reference = Vec::new(); + File::open(dst_path) + .unwrap() + .read_to_end(&mut output) + .unwrap(); + File::open(ref_path) + .unwrap() + .read_to_end(&mut reference) + .unwrap(); + assert_eq!(output, reference); +} -#[test] -fn save_16bit_to_pbm() { - let output_dir = "tests/output/pbm/images"; - fs::create_dir_all(output_dir).expect("failed to create output directory"); - let output_file = "tests/output/pbm/images/basi0g16.pbm"; +/// Convert `src_path` to `dst_path`, and compare with the content of +/// `ref_path`. +fn convert_and_check(src_path: &str, dst_path: &str, ref_path: &str) { + let img = image::open(src_path).unwrap(); + std::fs::create_dir_all(Path::new(dst_path).parent().unwrap()).unwrap(); + img.save(dst_path).unwrap(); + compare_exact(dst_path, ref_path); +} - let img = image::open("tests/images/png/16bpc/basi0g16.png").expect("failed to load image"); - img.save(output_file).expect("failed to save image"); +fn convert_and_check_pbm(src_path: &str, dst_path: &str, ref_path: &str) { + let img = image::open(src_path).unwrap(); - // inspect image written - let img = image::open(output_file).expect("failed to load saved image"); - assert_eq!(img.color(), image::ColorType::L16); - assert_eq!(img.dimensions(), (32, 32)); + let mut img = img.to_luma8(); + img.iter_mut().for_each(|x| { + *x = match *x { + 255 => 1, + 0 => 0, + _ => unreachable!(), + } + }); - let img = img.as_luma16().unwrap(); + std::fs::create_dir_all(Path::new(dst_path).parent().unwrap()).unwrap(); + img.save(dst_path).unwrap(); + compare_exact(dst_path, ref_path); +} - // inspect a few pixels - assert_eq!(*img.get_pixel(0, 0), Luma([0])); - assert_eq!(*img.get_pixel(31, 0), Luma([47871])); - assert_eq!(*img.get_pixel(22, 29), Luma([65535])); +#[test] +fn save_pbm() { + convert_and_check_pbm( + "tests/images/pnm/pbm/l1a.pbm", + "tests/output/pnm/pbm/l1b.pbm", + "tests/images/pnm/pbm/l1b.pnm", + ); } #[test] -fn save_16bit_to_ppm() { - let output_dir = "tests/output/ppm/images"; - fs::create_dir_all(output_dir).expect("failed to create output directory"); - let output_file = "tests/output/ppm/images/basn2c16.ppm"; +fn save_pgm() { + convert_and_check( + "tests/images/pnm/pgm/l8a.pgm", + "tests/output/pnm/pgm/l8b.pnm", + "tests/images/pnm/pgm/l8b.pgm", + ); - let img = image::open("tests/images/png/16bpc/basn2c16.png").expect("failed to load image"); - img.save(output_file).expect("failed to save image"); + convert_and_check( + "tests/images/pnm/pgm/l16a.pgm", + "tests/output/pnm/pgm/l16b.pnm", + "tests/images/pnm/pgm/l16b.pnm", + ); +} - // inspect image written - let img = image::open(output_file).expect("failed to load saved image"); - assert_eq!(img.color(), image::ColorType::Rgb16); - assert_eq!(img.dimensions(), (32, 32)); +#[test] +fn save_ppm() { + convert_and_check( + "tests/images/pnm/ppm/r8a.ppm", + "tests/output/pnm/ppm/r8b.ppm", + "tests/images/pnm/ppm/r8b.pnm", + ); - let img = img.as_rgb16().unwrap(); + convert_and_check( + "tests/images/pnm/ppm/r16a.ppm", + "tests/output/pnm/ppm/r8a.pnm", + "tests/images/pnm/ppm/r16b.ppm", + ); +} + +#[test] +fn save_pam() { + convert_and_check( + "tests/images/pnm/pam/ra16.pam", + "tests/output/pnm/pam/ra16.pam", + "tests/images/pnm/pam/ra16.pam", + ); - // inspect a few pixels - assert_eq!(*img.get_pixel(0, 0), Rgb([65535, 65535, 0])); - assert_eq!(*img.get_pixel(31, 0), Rgb([0, 65535, 0])); - assert_eq!(*img.get_pixel(22, 29), Rgb([19026, 4228, 42281])); + convert_and_check( + "tests/images/pnm/pam/la8.pam", + "tests/output/pnm/pam/la8.pam", + "tests/images/pnm/pam/la8.pam", + ); }