pnm decoding and encoding
Diffstat (limited to 'src/decode.rs')
-rw-r--r--src/decode.rs176
1 files changed, 176 insertions, 0 deletions
diff --git a/src/decode.rs b/src/decode.rs
new file mode 100644
index 0000000..183d2ec
--- /dev/null
+++ b/src/decode.rs
@@ -0,0 +1,176 @@
+//! decoding utilities
+use std::num::NonZeroU32;
+
+pub(crate) trait Read {
+ fn rd<const N: usize>(&mut self) -> Option<[u8; N]>;
+ fn by(&mut self) -> Option<u8> {
+ Some(self.rd::<1>()?[0])
+ }
+}
+impl<T: std::io::Read> Read for T {
+ fn rd<const N: usize>(&mut self) -> Option<[u8; N]> {
+ let mut buf = [0; N];
+ self.read_exact(&mut buf).ok()?;
+ Some(buf)
+ }
+}
+
+pub(crate) trait Ten {
+ fn ten() -> Self;
+}
+macro_rules! tenz {
+ ($for:ty) => {
+ impl Ten for $for {
+ fn ten() -> $for {
+ 10
+ }
+ }
+ };
+}
+tenz!(u8);
+tenz!(u16);
+tenz!(u32);
+tenz!(u64);
+tenz!(u128);
+tenz!(i8);
+tenz!(i16);
+tenz!(i32);
+tenz!(i64);
+tenz!(i128);
+
+/// Result alias with [`Error`].
+pub type Result<T> = std::result::Result<T, Error>;
+
+pub(crate) fn read_til<
+ T: Default + std::ops::Mul<T, Output = T> + std::ops::Add<T, Output = T> + From<u8> + Copy + Ten,
+>(
+ x: &mut &[u8],
+) -> Result<T> {
+ let mut n = T::default();
+ while let Some(x) = x.by() {
+ if x.is_ascii_whitespace() {
+ return Ok(n);
+ }
+ if !x.is_ascii_digit() {
+ return Err(Error::NotDigit(x as char));
+ }
+ n = n * T::ten() + T::from(x - b'0')
+ }
+ Ok(n)
+}
+
+macro_rules! dec_fn {
+ ($($f:ident)? $doc:literal) => {
+ use crate::decode::{decode_header, Error, Result};
+
+ #[doc = $doc]
+ pub fn decode(x: impl AsRef<[u8]>) -> Result<Output> {
+ let mut x = x.as_ref();
+ let magic = crate::decode::magic(&mut x).ok_or(Error::MissingMagic)?;
+ (magic == MAGIC)
+ .then_some(())
+ .ok_or(Error::WrongMagic {
+ got: magic,
+ should: MAGIC,
+ })?;
+ decode_wo_magic(x)
+ }
+
+ /// Decode without magic.
+ pub fn decode_wo_magic(mut x: &[u8]) -> Result<Output> {
+ let header = decode_header(&mut x, MAGIC)?;
+ decode_body_into(x, Uninit::new(header.width, header.height), $(header.$f.unwrap())?)
+ }
+ };
+}
+pub(crate) use dec_fn;
+
+/// Header for the older PNM formats. Not applicable to PAM.
+pub struct Header {
+ /// Magic number.
+ pub magic: u8,
+ pub width: NonZeroU32,
+ pub height: NonZeroU32,
+ /// Maximum value of each byte.
+ pub max: Option<u8>,
+}
+
+#[derive(Debug, Clone, Copy)]
+#[non_exhaustive]
+/// Errors that can occur on decoding.
+pub enum Error {
+ TooLarge,
+ NotDigit(char),
+ BadMagic(u8),
+ WrongMagic { got: u8, should: u8 },
+ MissingMagic,
+ ZeroWidth,
+ ZeroHeight,
+ MissingWidth,
+ MissingHeight,
+ MissingData,
+ MissingMax,
+ MissingDepth,
+ MissingTupltype,
+}
+
+impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::TooLarge => write!(f, "image too big"),
+ Self::NotDigit(x) => write!(f, "found {x} while decoding number"),
+ Self::BadMagic(x) => write!(f, "{x} is not a valid magic number"),
+ Self::WrongMagic { got, should } => {
+ write!(f, "expected magic number {should} found {got}")
+ }
+ Self::MissingMagic => write!(f, "no magic number (likely not a pnm image)"),
+ Self::ZeroWidth => write!(f, "zero width"),
+ Self::ZeroHeight => write!(f, "zero height"),
+ Self::MissingWidth => write!(f, "no width"),
+ Self::MissingHeight => write!(f, "no height"),
+ Self::MissingData => write!(f, "no data"),
+ Self::MissingMax => write!(f, "no max value"),
+ Self::MissingDepth => write!(f, "no depth"),
+ Self::MissingTupltype => write!(f, "no tupltype"),
+ }
+ }
+}
+
+/// Decodes the magic number.
+pub fn magic(x: &mut &[u8]) -> Option<u8> {
+ (x.by()? == b'P').then_some(())?;
+ let m = x.by().map(|x| x - b'0');
+ while x.first()?.is_ascii_whitespace() {
+ x.by();
+ }
+ m
+}
+
+/// Get the older pnm formats header. Does not decode magic.
+pub fn decode_header(x: &mut &[u8], magic: u8) -> Result<Header> {
+ while x.first() == Some(&b'#') {
+ while let Some(b) = x.by()
+ && b != b'\n'
+ {}
+ }
+ let width = NonZeroU32::new(read_til(x)?).ok_or(Error::ZeroWidth)?;
+ let height = NonZeroU32::new(read_til(x)?).ok_or(Error::ZeroHeight)?;
+ width.checked_mul(height).ok_or(Error::TooLarge)?;
+ let max = if magic != 4 && magic != 1 {
+ Some(read_til(x)?)
+ } else {
+ None
+ };
+
+ if magic != 4 {
+ while x.first().ok_or(Error::MissingData)?.is_ascii_whitespace() {
+ x.by();
+ }
+ }
+ Ok(Header {
+ magic,
+ width,
+ height,
+ max,
+ })
+}