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]),
}
pub struct PnmEncoder<W: Write> {
writer: W,
header: HeaderStrategy,
}
struct CheckedImageBuffer<'a> {
_image: FlatSamples<'a>,
_width: u32,
_height: u32,
_color: ExtendedColorType,
}
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> {
pub fn new(writer: W) -> Self {
PnmEncoder {
writer,
header: HeaderStrategy::Dynamic,
}
}
pub fn with_subtype(self, subtype: PnmSubtype) -> Self {
PnmEncoder {
writer: self.writer,
header: HeaderStrategy::Subtype(subtype),
}
}
pub fn with_header(self, header: PnmHeader) -> Self {
PnmEncoder {
writer: self.writer,
header: HeaderStrategy::Chosen(header),
}
}
pub fn with_dynamic_header(self) -> Self {
PnmEncoder {
writer: self.writer,
header: HeaderStrategy::Dynamic,
}
}
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(),
),
}
}
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)
}
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)
}
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> {
#[track_caller]
fn write_image(
mut self,
buf: &[u8],
width: u32,
height: u32,
color_type: ColorType,
) -> ImageResult<()> {
let expected_buffer_len =
(width as u64 * height as u64).saturating_mul(color_type.bytes_per_pixel() as u64);
assert_eq!(
expected_buffer_len,
buf.len() as u64,
"Invalid buffer length: expected {expected_buffer_len} got {} for {width}x{height} image",
buf.len(),
);
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 {
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 {
return Err(ImageError::Parameter(ParameterError::from_kind(
ParameterErrorKind::DimensionMismatch,
)));
}
Ok(CheckedDimensions {
unchecked: self,
width,
height,
})
}
}
impl<'a> CheckedDimensions<'a> {
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,
};
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,
_ => {
return Err(ImageError::Unsupported(
UnsupportedError::from_format_and_kind(
ImageFormat::Pnm.into(),
UnsupportedErrorKind::Color(self.color),
),
));
}
};
if header_maxval < max_sample && !image.all_smaller(header_maxval) {
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<()>
where
V: Default + Eq + Copy,
{
let line_width = (width - 1) / 8 + 1;
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 {
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),
}
}
}