diff options
author | Valentin Popov <valentin@popov.link> | 2024-01-08 00:21:28 +0300 |
---|---|---|
committer | Valentin Popov <valentin@popov.link> | 2024-01-08 00:21:28 +0300 |
commit | 1b6a04ca5504955c571d1c97504fb45ea0befee4 (patch) | |
tree | 7579f518b23313e8a9748a88ab6173d5e030b227 /vendor/image/src/codecs/pnm/encoder.rs | |
parent | 5ecd8cf2cba827454317368b68571df0d13d7842 (diff) | |
download | fparkan-1b6a04ca5504955c571d1c97504fb45ea0befee4.tar.xz fparkan-1b6a04ca5504955c571d1c97504fb45ea0befee4.zip |
Initial vendor packages
Signed-off-by: Valentin Popov <valentin@popov.link>
Diffstat (limited to 'vendor/image/src/codecs/pnm/encoder.rs')
-rw-r--r-- | vendor/image/src/codecs/pnm/encoder.rs | 673 |
1 files changed, 673 insertions, 0 deletions
diff --git a/vendor/image/src/codecs/pnm/encoder.rs b/vendor/image/src/codecs/pnm/encoder.rs new file mode 100644 index 0000000..9f823d0 --- /dev/null +++ b/vendor/image/src/codecs/pnm/encoder.rs @@ -0,0 +1,673 @@ +//! 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<W: Write> { + 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<W: Write> PnmEncoder<W> { + /// 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<FlatSamples<'s>>, + { + 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<W: Write> ImageEncoder for PnmEncoder<W> { + 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<CheckedImageBuffer<'a>> { + 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<CheckedDimensions<'a>> { + 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<CheckedHeaderColor<'a>> { + 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<CheckedHeader<'a>> { + 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<TupleEncoding<'a>> { + 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<V>(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<V>(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::<BigEndian>(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), + } + } +} |