//! Encoding of PNM Images use std::fmt; use std::io; use std::io::Write; use super::AutoBreak; use super::{ArbitraryHeader, ArbitraryTuplType, BitmapHeader, GraymapHeader, PixmapHeader}; use super::{HeaderRecord, PnmHeader, PnmSubtype, SampleEncoding}; use crate::color::{ColorType, ExtendedColorType}; use crate::error::{ ImageError, ImageResult, ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind, }; use crate::image::{ImageEncoder, ImageFormat}; use byteorder::{BigEndian, WriteBytesExt}; enum HeaderStrategy { Dynamic, Subtype(PnmSubtype), Chosen(PnmHeader), } #[derive(Clone, Copy)] pub enum FlatSamples<'a> { U8(&'a [u8]), U16(&'a [u16]), } /// Encodes images to any of the `pnm` image formats. pub struct PnmEncoder { writer: W, header: HeaderStrategy, } /// Encapsulate the checking system in the type system. Non of the fields are actually accessed /// but requiring them forces us to validly construct the struct anyways. struct CheckedImageBuffer<'a> { _image: FlatSamples<'a>, _width: u32, _height: u32, _color: ExtendedColorType, } // Check the header against the buffer. Each struct produces the next after a check. struct UncheckedHeader<'a> { header: &'a PnmHeader, } struct CheckedDimensions<'a> { unchecked: UncheckedHeader<'a>, width: u32, height: u32, } struct CheckedHeaderColor<'a> { dimensions: CheckedDimensions<'a>, color: ExtendedColorType, } struct CheckedHeader<'a> { color: CheckedHeaderColor<'a>, encoding: TupleEncoding<'a>, _image: CheckedImageBuffer<'a>, } enum TupleEncoding<'a> { PbmBits { samples: FlatSamples<'a>, width: u32, }, Ascii { samples: FlatSamples<'a>, }, Bytes { samples: FlatSamples<'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`. pub fn new(writer: W) -> Self { PnmEncoder { writer, header: HeaderStrategy::Dynamic, } } /// Encode a specific pnm subtype 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`. pub fn with_subtype(self, subtype: PnmSubtype) -> Self { PnmEncoder { writer: self.writer, header: HeaderStrategy::Subtype(subtype), } } /// Enforce the use of a chosen header. /// /// While this option gives the most control over the actual written data, the encoding process /// will error in case the header data and image parameters do not agree. It is the users /// obligation to ensure that the width and height are set accordingly, for example. /// /// 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`. pub fn with_header(self, header: PnmHeader) -> Self { PnmEncoder { writer: self.writer, header: HeaderStrategy::Chosen(header), } } /// Create the header dynamically for each 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 will overwrite the effect of earlier calls to `with_subtype` and `with_header`. pub fn with_dynamic_header(self) -> Self { PnmEncoder { writer: self.writer, header: HeaderStrategy::Dynamic, } } /// Encode an image whose samples are represented as `u8`. /// /// Some `pnm` subtypes are incompatible with some color options, a chosen header most /// certainly with any deviation from the original decoded image. pub fn encode<'s, S>( &mut self, image: S, width: u32, height: u32, color: ColorType, ) -> ImageResult<()> where S: Into>, { let image = image.into(); match self.header { HeaderStrategy::Dynamic => { self.write_dynamic_header(image, width, height, color.into()) } HeaderStrategy::Subtype(subtype) => { self.write_subtyped_header(subtype, image, width, height, color.into()) } HeaderStrategy::Chosen(ref header) => Self::write_with_header( &mut self.writer, header, image, width, height, color.into(), ), } } /// 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, width: u32, height: u32, color: ExtendedColorType, ) -> ImageResult<()> { let depth = u32::from(color.channel_count()); let (maxval, tupltype) = match color { ExtendedColorType::L1 => (1, ArbitraryTuplType::BlackAndWhite), ExtendedColorType::L8 => (0xff, ArbitraryTuplType::Grayscale), ExtendedColorType::L16 => (0xffff, ArbitraryTuplType::Grayscale), ExtendedColorType::La1 => (1, ArbitraryTuplType::BlackAndWhiteAlpha), ExtendedColorType::La8 => (0xff, ArbitraryTuplType::GrayscaleAlpha), ExtendedColorType::La16 => (0xffff, ArbitraryTuplType::GrayscaleAlpha), ExtendedColorType::Rgb8 => (0xff, ArbitraryTuplType::RGB), ExtendedColorType::Rgb16 => (0xffff, ArbitraryTuplType::RGB), ExtendedColorType::Rgba8 => (0xff, ArbitraryTuplType::RGBAlpha), ExtendedColorType::Rgba16 => (0xffff, ArbitraryTuplType::RGBAlpha), _ => { return Err(ImageError::Unsupported( UnsupportedError::from_format_and_kind( ImageFormat::Pnm.into(), UnsupportedErrorKind::Color(color), ), )) } }; let header = PnmHeader { decoded: HeaderRecord::Arbitrary(ArbitraryHeader { width, height, depth, maxval, tupltype: Some(tupltype), }), encoded: None, }; Self::write_with_header(&mut self.writer, &header, image, width, height, color) } /// Try to encode the image with the chosen format, give its corresponding pixel encoding type. fn write_subtyped_header( &mut self, 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) | (PnmSubtype::Bitmap(encoding), ExtendedColorType::L1) => PnmHeader { decoded: HeaderRecord::Bitmap(BitmapHeader { encoding, width, height, }), encoded: None, }, (_, _) => { return Err(ImageError::Parameter(ParameterError::from_kind( ParameterErrorKind::Generic( "Color type can not be represented in the chosen format".to_owned(), ), ))); } }; Self::write_with_header(&mut self.writer, &header, image, width, height, color) } /// Try to encode the image with the chosen header, checking if values are correct. /// /// Returns how the body should be written if successful. fn write_with_header( writer: &mut dyn Write, header: &PnmHeader, image: FlatSamples, width: u32, height: u32, color: ExtendedColorType, ) -> ImageResult<()> { let unchecked = UncheckedHeader { header }; unchecked .check_header_dimensions(width, height)? .check_header_color(color)? .check_sample_values(image)? .write_header(writer)? .write_image(writer) } } impl ImageEncoder for PnmEncoder { fn write_image( mut self, buf: &[u8], width: u32, height: u32, color_type: ColorType, ) -> ImageResult<()> { self.encode(buf, width, height, color_type) } } impl<'a> CheckedImageBuffer<'a> { fn check( image: FlatSamples<'a>, width: u32, height: u32, color: ExtendedColorType, ) -> ImageResult> { let components = color.channel_count() as usize; let uwidth = width as usize; let uheight = height as usize; let expected_len = components .checked_mul(uwidth) .and_then(|v| v.checked_mul(uheight)); if Some(image.len()) != expected_len { // Image buffer does not correspond to size and colour. return Err(ImageError::Parameter(ParameterError::from_kind( ParameterErrorKind::DimensionMismatch, ))); } Ok(CheckedImageBuffer { _image: image, _width: width, _height: height, _color: color, }) } } impl<'a> UncheckedHeader<'a> { fn check_header_dimensions( self, width: u32, height: u32, ) -> ImageResult> { if self.header.width() != width || self.header.height() != height { // Chosen header does not match Image dimensions. return Err(ImageError::Parameter(ParameterError::from_kind( ParameterErrorKind::DimensionMismatch, ))); } Ok(CheckedDimensions { unchecked: self, width, height, }) } } impl<'a> CheckedDimensions<'a> { // Check color compatibility with the header. This will only error when we are certain that // the combination is bogus (e.g. combining Pixmap and Palette) but allows uncertain // combinations (basically a ArbitraryTuplType::Custom with any color of fitting depth). fn check_header_color(self, color: ExtendedColorType) -> ImageResult> { let components = u32::from(color.channel_count()); match *self.unchecked.header { PnmHeader { decoded: HeaderRecord::Bitmap(_), .. } => match color { ExtendedColorType::L1 | ExtendedColorType::L8 | ExtendedColorType::L16 => (), _ => { return Err(ImageError::Parameter(ParameterError::from_kind( ParameterErrorKind::Generic( "PBM format only support luma color types".to_owned(), ), ))) } }, PnmHeader { decoded: HeaderRecord::Graymap(_), .. } => match color { ExtendedColorType::L1 | ExtendedColorType::L8 | ExtendedColorType::L16 => (), _ => { return Err(ImageError::Parameter(ParameterError::from_kind( ParameterErrorKind::Generic( "PGM format only support luma color types".to_owned(), ), ))) } }, PnmHeader { decoded: HeaderRecord::Pixmap(_), .. } => match color { ExtendedColorType::Rgb8 => (), _ => { return Err(ImageError::Parameter(ParameterError::from_kind( ParameterErrorKind::Generic( "PPM format only support ExtendedColorType::Rgb8".to_owned(), ), ))) } }, PnmHeader { decoded: HeaderRecord::Arbitrary(ArbitraryHeader { depth, ref tupltype, .. }), .. } => match (tupltype, color) { (&Some(ArbitraryTuplType::BlackAndWhite), ExtendedColorType::L1) => (), (&Some(ArbitraryTuplType::BlackAndWhiteAlpha), ExtendedColorType::La8) => (), (&Some(ArbitraryTuplType::Grayscale), ExtendedColorType::L1) => (), (&Some(ArbitraryTuplType::Grayscale), ExtendedColorType::L8) => (), (&Some(ArbitraryTuplType::Grayscale), ExtendedColorType::L16) => (), (&Some(ArbitraryTuplType::GrayscaleAlpha), ExtendedColorType::La8) => (), (&Some(ArbitraryTuplType::RGB), ExtendedColorType::Rgb8) => (), (&Some(ArbitraryTuplType::RGBAlpha), ExtendedColorType::Rgba8) => (), (&None, _) if depth == components => (), (&Some(ArbitraryTuplType::Custom(_)), _) if depth == components => (), _ if depth != components => { return Err(ImageError::Parameter(ParameterError::from_kind( ParameterErrorKind::Generic(format!( "Depth mismatch: header {} vs. color {}", depth, components )), ))) } _ => { return Err(ImageError::Parameter(ParameterError::from_kind( ParameterErrorKind::Generic( "Invalid color type for selected PAM color type".to_owned(), ), ))) } }, } Ok(CheckedHeaderColor { dimensions: self, color, }) } } impl<'a> CheckedHeaderColor<'a> { fn check_sample_values(self, image: FlatSamples<'a>) -> ImageResult> { let header_maxval = match self.dimensions.unchecked.header.decoded { HeaderRecord::Bitmap(_) => 1, HeaderRecord::Graymap(GraymapHeader { maxwhite, .. }) => maxwhite, HeaderRecord::Pixmap(PixmapHeader { maxval, .. }) => maxval, HeaderRecord::Arbitrary(ArbitraryHeader { maxval, .. }) => maxval, }; // 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::L8 | ExtendedColorType::La8 | ExtendedColorType::Rgb8 | ExtendedColorType::Rgba8 | ExtendedColorType::Bgr8 | ExtendedColorType::Bgra8 => 0xff, ExtendedColorType::L16 | ExtendedColorType::La16 | ExtendedColorType::Rgb16 | ExtendedColorType::Rgba16 => 0xffff, _ => { // Unsupported target color type. return Err(ImageError::Unsupported( UnsupportedError::from_format_and_kind( ImageFormat::Pnm.into(), UnsupportedErrorKind::Color(self.color), ), )); } }; // Avoid the performance heavy check if possible, e.g. if the header has been chosen by us. if header_maxval < max_sample && !image.all_smaller(header_maxval) { // Sample value greater than allowed for chosen header. return Err(ImageError::Unsupported( UnsupportedError::from_format_and_kind( ImageFormat::Pnm.into(), UnsupportedErrorKind::GenericFeature( "Sample value greater than allowed for chosen header".to_owned(), ), ), )); } let encoding = image.encoding_for(&self.dimensions.unchecked.header.decoded); let image = CheckedImageBuffer::check( image, self.dimensions.width, self.dimensions.height, self.color, )?; Ok(CheckedHeader { color: self, encoding, _image: image, }) } } impl<'a> CheckedHeader<'a> { fn write_header(self, writer: &mut dyn Write) -> ImageResult> { self.header().write(writer)?; Ok(self.encoding) } fn header(&self) -> &PnmHeader { self.color.dimensions.unchecked.header } } struct SampleWriter<'a>(&'a mut dyn Write); impl<'a> SampleWriter<'a> { fn write_samples_ascii(self, samples: V) -> io::Result<()> where V: Iterator, V::Item: fmt::Display, { let mut auto_break_writer = AutoBreak::new(self.0, 70); for value in samples { write!(auto_break_writer, "{} ", value)?; } auto_break_writer.flush() } fn write_pbm_bits(self, samples: &[V], width: u32) -> io::Result<()> /* Default gives 0 for all primitives. TODO: replace this with `Zeroable` once it hits stable */ where V: Default + Eq + Copy, { // The length of an encoded scanline let line_width = (width - 1) / 8 + 1; // We'll be writing single bytes, so buffer let mut line_buffer = Vec::with_capacity(line_width as usize); for line in samples.chunks(width as usize) { for byte_bits in line.chunks(8) { let mut byte = 0u8; for i in 0..8 { // Black pixels are encoded as 1s if let Some(&v) = byte_bits.get(i) { if v == V::default() { byte |= 1u8 << (7 - i) } } } line_buffer.push(byte) } self.0.write_all(line_buffer.as_slice())?; line_buffer.clear(); } self.0.flush() } } impl<'a> FlatSamples<'a> { fn len(&self) -> usize { match *self { FlatSamples::U8(arr) => arr.len(), FlatSamples::U16(arr) => arr.len(), } } fn all_smaller(&self, max_val: u32) -> bool { match *self { FlatSamples::U8(arr) => arr.iter().any(|&val| u32::from(val) > max_val), FlatSamples::U16(arr) => arr.iter().any(|&val| u32::from(val) > max_val), } } fn encoding_for(&self, header: &HeaderRecord) -> TupleEncoding<'a> { match *header { HeaderRecord::Bitmap(BitmapHeader { encoding: SampleEncoding::Binary, width, .. }) => TupleEncoding::PbmBits { samples: *self, width, }, HeaderRecord::Bitmap(BitmapHeader { encoding: SampleEncoding::Ascii, .. }) => TupleEncoding::Ascii { samples: *self }, HeaderRecord::Arbitrary(_) => TupleEncoding::Bytes { samples: *self }, HeaderRecord::Graymap(GraymapHeader { encoding: SampleEncoding::Ascii, .. }) | HeaderRecord::Pixmap(PixmapHeader { encoding: SampleEncoding::Ascii, .. }) => TupleEncoding::Ascii { samples: *self }, HeaderRecord::Graymap(GraymapHeader { encoding: SampleEncoding::Binary, .. }) | HeaderRecord::Pixmap(PixmapHeader { encoding: SampleEncoding::Binary, .. }) => TupleEncoding::Bytes { samples: *self }, } } } impl<'a> From<&'a [u8]> for FlatSamples<'a> { fn from(samples: &'a [u8]) -> Self { FlatSamples::U8(samples) } } impl<'a> From<&'a [u16]> for FlatSamples<'a> { fn from(samples: &'a [u16]) -> Self { FlatSamples::U16(samples) } } impl<'a> TupleEncoding<'a> { fn write_image(&self, writer: &mut dyn Write) -> ImageResult<()> { match *self { TupleEncoding::PbmBits { samples: FlatSamples::U8(samples), width, } => SampleWriter(writer) .write_pbm_bits(samples, width) .map_err(ImageError::IoError), TupleEncoding::PbmBits { samples: FlatSamples::U16(samples), width, } => SampleWriter(writer) .write_pbm_bits(samples, width) .map_err(ImageError::IoError), TupleEncoding::Bytes { samples: FlatSamples::U8(samples), } => writer.write_all(samples).map_err(ImageError::IoError), TupleEncoding::Bytes { samples: FlatSamples::U16(samples), } => samples.iter().try_for_each(|&sample| { writer .write_u16::(sample) .map_err(ImageError::IoError) }), TupleEncoding::Ascii { samples: FlatSamples::U8(samples), } => SampleWriter(writer) .write_samples_ascii(samples.iter()) .map_err(ImageError::IoError), TupleEncoding::Ascii { samples: FlatSamples::U16(samples), } => SampleWriter(writer) .write_samples_ascii(samples.iter()) .map_err(ImageError::IoError), } } }