Skip to main content

zune_ppm/
encoder.rs

1/*
2 * Copyright (c) 2023.
3 *
4 * This software is free software;
5 *
6 * You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
7 */
8
9use alloc::format;
10use core::fmt::{Debug, Display, Formatter};
11
12use zune_core::bit_depth::BitType;
13use zune_core::bytestream::{ZByteIoError, ZByteWriterTrait, ZWriter};
14use zune_core::colorspace::ColorSpace;
15use zune_core::options::EncoderOptions;
16
17/// Errors occurring during encoding
18pub enum PPMEncodeErrors {
19    Static(&'static str),
20    TooShortInput(usize, usize),
21    UnsupportedColorspace(ColorSpace),
22    IoError(ZByteIoError)
23}
24
25impl Debug for PPMEncodeErrors {
26    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
27        match self {
28            PPMEncodeErrors::Static(ref errors) => {
29                writeln!(f, "{errors}")
30            }
31            PPMEncodeErrors::TooShortInput(expected, found) => {
32                writeln!(f, "Expected input of length {expected} but found {found}")
33            }
34            PPMEncodeErrors::UnsupportedColorspace(colorspace) => {
35                writeln!(f, "Unsupported colorspace {colorspace:?} for ppm")
36            }
37            PPMEncodeErrors::IoError(err) => {
38                writeln!(f, "I/O error: {:?}", err)
39            }
40        }
41    }
42}
43
44impl From<ZByteIoError> for PPMEncodeErrors {
45    fn from(value: ZByteIoError) -> Self {
46        Self::IoError(value)
47    }
48}
49
50enum PPMVersions {
51    P5,
52    P6,
53    P7
54}
55
56impl Display for PPMVersions {
57    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
58        match self {
59            Self::P6 => write!(f, "P6"),
60            Self::P5 => write!(f, "P5"),
61            Self::P7 => write!(f, "P7")
62        }
63    }
64}
65
66/// A PPM encoder
67///
68///
69///# Encoding 16 bit data
70/// To encode a 16-bit image, each element needs to be
71/// re-interpreted as 2 u8’s in native endian, the library will do the
72/// appropriate conversions when needed
73///
74/// # Example
75/// - Encoding 8 bit grayscale data
76///```
77/// use zune_core::bit_depth::BitDepth;
78/// use zune_core::colorspace::ColorSpace;
79/// use zune_core::options::EncoderOptions;
80/// use zune_ppm::PPMEncoder;
81/// use zune_ppm::PPMEncodeErrors;
82///
83/// fn main()-> Result<(),PPMEncodeErrors> {
84///    const W:usize = 100;
85///    const H:usize = 100;
86///    let data:[u8;{W * H}] = std::array::from_fn(|x| (x % 256) as u8);
87///    let encoder = PPMEncoder::new(&data,EncoderOptions::new(W,H,ColorSpace::Luma,BitDepth::Eight));
88///    let mut write_to =vec![];
89///    encoder.encode(&mut write_to)?;
90///    Ok(())
91/// }
92/// ```
93pub struct PPMEncoder<'a> {
94    data:    &'a [u8],
95    options: EncoderOptions
96}
97
98impl<'a> PPMEncoder<'a> {
99    /// Create a new encoder which will encode the specified
100    /// data whose format is contained in the options.
101    ///
102    /// # Note
103    /// To encode 16 bit data,it still must be provided as u8 bytes
104    /// in native endian.
105    ///
106    /// One can use [`u16::to_ne_bytes`] for this if data is in a u16 slice
107    ///
108    /// [`u16::to_ne_bytes`]:u16::to_ne_bytes
109    pub fn new(data: &'a [u8], options: EncoderOptions) -> PPMEncoder<'a> {
110        PPMEncoder { data, options }
111    }
112
113    fn encode_headers<T: ZByteWriterTrait>(
114        &self, stream: &mut ZWriter<T>
115    ) -> Result<(), PPMEncodeErrors> {
116        let version = version_for_colorspace(self.options.colorspace()).ok_or(
117            PPMEncodeErrors::UnsupportedColorspace(self.options.colorspace())
118        )?;
119
120        let width = self.options.width();
121        let height = self.options.height();
122        let components = self.options.colorspace().num_components();
123        let max_val = self.options.depth().max_value();
124        let colorspace = self.options.colorspace();
125
126        let header = match version {
127            PPMVersions::P5 | PPMVersions::P6 => {
128                format!("{version}\n{width}\n{height}\n{max_val}\n")
129            }
130            PPMVersions::P7 => {
131                let tuple_type = convert_tuple_type_to_pam(colorspace);
132
133                format!(
134                    "P7\nWIDTH {width}\nHEIGHT {height}\nDEPTH {components}\nMAXVAL {max_val}\nTUPLTYPE {tuple_type}\n ENDHDR\n",
135                )
136            }
137        };
138
139        stream.write_all(header.as_bytes()).unwrap();
140
141        Ok(())
142    }
143    /// Encode into a user provided buffer
144    ///
145    /// # Arguments
146    /// - out: The output buffer to write bytes into
147    ///     It is recommended that the buffer be at least [`max_out_size`](crate::encoder::max_out_size) in order
148    ///     to encode successfully. In case size is not big enough , the library will bail and return an error
149    ///
150    /// # Returns
151    /// - Ok(size): The actual number of bytes written
152    /// - Err: An error in case something bad happened, contents of `out` are to be treated as invalid
153    pub fn encode<T: ZByteWriterTrait>(&self, out: T) -> Result<usize, PPMEncodeErrors> {
154        let found = self.data.len();
155        let expected = calc_expected_size(self.options);
156
157        if expected != found {
158            return Err(PPMEncodeErrors::TooShortInput(expected, found));
159        }
160        let mut stream = ZWriter::new(out);
161        stream.reserve(expected + 37)?; // 37 arbitrary number, chosen by divinity, guaranteed to work
162
163        self.encode_headers(&mut stream)?;
164
165        match self.options.depth().bit_type() {
166            BitType::U8 => stream.write_all(self.data)?,
167            BitType::U16 => {
168                // chunk in two and write to stream
169                for slice in self.data.chunks_exact(2) {
170                    let byte = u16::from_ne_bytes(slice.try_into().unwrap());
171                    stream.write_u16_be_err(byte)?;
172                }
173            }
174            _ => unreachable!()
175        }
176        let position = stream.bytes_written();
177        Ok(position)
178    }
179}
180
181fn version_for_colorspace(colorspace: ColorSpace) -> Option<PPMVersions> {
182    match colorspace {
183        ColorSpace::Luma => Some(PPMVersions::P5),
184        ColorSpace::RGB => Some(PPMVersions::P6),
185        ColorSpace::RGBA | ColorSpace::LumaA => Some(PPMVersions::P7),
186        _ => None
187    }
188}
189
190fn convert_tuple_type_to_pam(colorspace: ColorSpace) -> &'static str {
191    match colorspace {
192        ColorSpace::Luma => "GRAYSCALE",
193        ColorSpace::RGB => "RGB",
194        ColorSpace::LumaA => "GRAYSCALE_ALPHA",
195        ColorSpace::RGBA => "RGB_ALPHA",
196        _ => unreachable!()
197    }
198}
199
200const PPM_HEADER_SIZE: usize = 100;
201
202/// Gives expected minimum size for which the encoder wil guarantee
203/// that the given raw bytes will fit
204///
205#[inline]
206pub fn max_out_size(options: &EncoderOptions) -> usize {
207    options
208        .width()
209        .checked_mul(options.depth().size_of())
210        .unwrap()
211        .checked_mul(options.height())
212        .unwrap()
213        .checked_mul(options.colorspace().num_components())
214        .unwrap()
215        .checked_add(PPM_HEADER_SIZE)
216        .unwrap()
217}
218
219fn calc_expected_size(options: EncoderOptions) -> usize {
220    max_out_size(&options).checked_sub(PPM_HEADER_SIZE).unwrap()
221}