use alloc::format;
use core::fmt::{Debug, Display, Formatter};
use zune_core::bit_depth::BitType;
use zune_core::bytestream::{ZByteIoError, ZByteWriterTrait, ZWriter};
use zune_core::colorspace::ColorSpace;
use zune_core::options::EncoderOptions;
pub enum PPMEncodeErrors {
Static(&'static str),
TooShortInput(usize, usize),
UnsupportedColorspace(ColorSpace),
IoError(ZByteIoError)
}
impl Debug for PPMEncodeErrors {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
match self {
PPMEncodeErrors::Static(ref errors) => {
writeln!(f, "{errors}")
}
PPMEncodeErrors::TooShortInput(expected, found) => {
writeln!(f, "Expected input of length {expected} but found {found}")
}
PPMEncodeErrors::UnsupportedColorspace(colorspace) => {
writeln!(f, "Unsupported colorspace {colorspace:?} for ppm")
}
PPMEncodeErrors::IoError(err) => {
writeln!(f, "I/O error: {:?}", err)
}
}
}
}
impl From<ZByteIoError> for PPMEncodeErrors {
fn from(value: ZByteIoError) -> Self {
Self::IoError(value)
}
}
enum PPMVersions {
P5,
P6,
P7
}
impl Display for PPMVersions {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
match self {
Self::P6 => write!(f, "P6"),
Self::P5 => write!(f, "P5"),
Self::P7 => write!(f, "P7")
}
}
}
pub struct PPMEncoder<'a> {
data: &'a [u8],
options: EncoderOptions
}
impl<'a> PPMEncoder<'a> {
pub fn new(data: &'a [u8], options: EncoderOptions) -> PPMEncoder<'a> {
PPMEncoder { data, options }
}
fn encode_headers<T: ZByteWriterTrait>(
&self, stream: &mut ZWriter<T>
) -> Result<(), PPMEncodeErrors> {
let version = version_for_colorspace(self.options.colorspace()).ok_or(
PPMEncodeErrors::UnsupportedColorspace(self.options.colorspace())
)?;
let width = self.options.width();
let height = self.options.height();
let components = self.options.colorspace().num_components();
let max_val = self.options.depth().max_value();
let colorspace = self.options.colorspace();
let header = match version {
PPMVersions::P5 | PPMVersions::P6 => {
format!("{version}\n{width}\n{height}\n{max_val}\n")
}
PPMVersions::P7 => {
let tuple_type = convert_tuple_type_to_pam(colorspace);
format!(
"P7\nWIDTH {width}\nHEIGHT {height}\nDEPTH {components}\nMAXVAL {max_val}\nTUPLTYPE {tuple_type}\n ENDHDR\n",
)
}
};
stream.write_all(header.as_bytes()).unwrap();
Ok(())
}
pub fn encode<T: ZByteWriterTrait>(&self, out: T) -> Result<usize, PPMEncodeErrors> {
let found = self.data.len();
let expected = calc_expected_size(self.options);
if expected != found {
return Err(PPMEncodeErrors::TooShortInput(expected, found));
}
let mut stream = ZWriter::new(out);
stream.reserve(expected + 37)?;
self.encode_headers(&mut stream)?;
match self.options.depth().bit_type() {
BitType::U8 => stream.write_all(self.data)?,
BitType::U16 => {
for slice in self.data.chunks_exact(2) {
let byte = u16::from_ne_bytes(slice.try_into().unwrap());
stream.write_u16_be_err(byte)?;
}
}
_ => unreachable!()
}
let position = stream.bytes_written();
Ok(position)
}
}
fn version_for_colorspace(colorspace: ColorSpace) -> Option<PPMVersions> {
match colorspace {
ColorSpace::Luma => Some(PPMVersions::P5),
ColorSpace::RGB => Some(PPMVersions::P6),
ColorSpace::RGBA | ColorSpace::LumaA => Some(PPMVersions::P7),
_ => None
}
}
fn convert_tuple_type_to_pam(colorspace: ColorSpace) -> &'static str {
match colorspace {
ColorSpace::Luma => "GRAYSCALE",
ColorSpace::RGB => "RGB",
ColorSpace::LumaA => "GRAYSCALE_ALPHA",
ColorSpace::RGBA => "RGB_ALPHA",
_ => unreachable!()
}
}
const PPM_HEADER_SIZE: usize = 100;
#[inline]
pub fn max_out_size(options: &EncoderOptions) -> usize {
options
.width()
.checked_mul(options.depth().size_of())
.unwrap()
.checked_mul(options.height())
.unwrap()
.checked_mul(options.colorspace().num_components())
.unwrap()
.checked_add(PPM_HEADER_SIZE)
.unwrap()
}
fn calc_expected_size(options: EncoderOptions) -> usize {
max_out_size(&options).checked_sub(PPM_HEADER_SIZE).unwrap()
}