pnm decoding and encoding
init
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Cargo.toml | 18 | ||||
| -rw-r--r-- | LICENSE | 21 | ||||
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | examples/viewer.rs | 8 | ||||
| -rw-r--r-- | src/decode.rs | 176 | ||||
| -rw-r--r-- | src/decode_body_into.md | 3 | ||||
| -rw-r--r-- | src/decode_into.rs | 1 | ||||
| -rw-r--r-- | src/encode.rs | 73 | ||||
| -rw-r--r-- | src/encode_into.md | 6 | ||||
| -rw-r--r-- | src/est.md | 1 | ||||
| -rw-r--r-- | src/lib.rs | 90 | ||||
| -rw-r--r-- | src/pam.rs | 360 | ||||
| -rw-r--r-- | src/pbm.rs | 250 | ||||
| -rw-r--r-- | src/pgm.rs | 180 | ||||
| -rw-r--r-- | src/ppm.rs | 180 | ||||
| -rw-r--r-- | tdata/fimg-gray.imgbuf | bin | 0 -> 300 bytes | |||
| -rw-r--r-- | tdata/fimg-gray.pam | bin | 0 -> 367 bytes | |||
| -rw-r--r-- | tdata/fimg-gray.png | bin | 0 -> 201 bytes | |||
| -rw-r--r-- | tdata/fimg-rainbow-transparent.imgbuf | bin | 0 -> 1200 bytes | |||
| -rw-r--r-- | tdata/fimg-rainbow-transparent.pam | bin | 0 -> 1267 bytes | |||
| -rw-r--r-- | tdata/fimg-rainbow-transparent.png | bin | 0 -> 165 bytes | |||
| -rw-r--r-- | tdata/fimg-rainbow.imgbuf | bin | 0 -> 900 bytes | |||
| -rw-r--r-- | tdata/fimg-rainbow.pam | bin | 0 -> 961 bytes | |||
| -rw-r--r-- | tdata/fimg-rainbow.png | bin | 0 -> 152 bytes | |||
| -rw-r--r-- | tdata/fimg-rgb.pam | bin | 0 -> 961 bytes | |||
| -rw-r--r-- | tdata/fimg-transparent.imgbuf | bin | 0 -> 600 bytes | |||
| -rw-r--r-- | tdata/fimg-transparent.pam | bin | 0 -> 673 bytes | |||
| -rw-r--r-- | tdata/fimg-transparent.png | bin | 0 -> 119 bytes | |||
| -rw-r--r-- | tdata/fimg.imgbuf | bin | 0 -> 300 bytes | |||
| -rw-r--r-- | tdata/fimg.pam | bin | 0 -> 371 bytes | |||
| -rw-r--r-- | tdata/fimg.png | bin | 0 -> 107 bytes | |||
| -rw-r--r-- | tdata/fimgA.pbm | 16 | ||||
| -rw-r--r-- | tdata/fimgA.pgm | 16 | ||||
| -rw-r--r-- | tdata/fimgA.ppm | 16 | ||||
| -rw-r--r-- | tdata/fimgR.pbm | 2 | ||||
| -rw-r--r-- | tdata/fimgR.pgm | bin | 0 -> 313 bytes | |||
| -rw-r--r-- | tdata/fimgR.ppm | bin | 0 -> 913 bytes |
38 files changed, 1421 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ffa3bbd --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +Cargo.lock
\ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..cce3d30 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "pnm" +version = "0.1.0" +edition = "2021" +description = "portable anymap format encoding and decoding" +authors = ["bend-n <[email protected]>"] +license = "MIT" +repository = "https://github.com/bend-n/pnm" +exclude = ["tdata", ".gitignore"] +keywords = ["image", "format", "encoding", "decoding"] +categories = ["multimedia::images", "graphics", "encoding"] + +[dependencies] +atools = "0.1.1" +fimg = { version = "0.4.41", default-features = false } + +[dev-dependencies] +fimg = { version = "0.4.41", features = ["save"], default-features = false } @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 bendn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d4d7a5 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# PNM + +provides encoders and decoders for the portable anymap formats.
\ No newline at end of file diff --git a/examples/viewer.rs b/examples/viewer.rs new file mode 100644 index 0000000..f4b0f14 --- /dev/null +++ b/examples/viewer.rs @@ -0,0 +1,8 @@ +fn main() { + for elem in std::env::args().skip(1) { + pnm::decode(&std::fs::read(elem).unwrap()) + .unwrap() + .rgba() + .show(); + } +} 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, + }) +} diff --git a/src/decode_body_into.md b/src/decode_body_into.md new file mode 100644 index 0000000..97fb7f5 --- /dev/null +++ b/src/decode_body_into.md @@ -0,0 +1,3 @@ +Decodes the body of this image, placing it in the [`Uninit`] image. + +If you wish to use this yourself, use [`decode_header`] to create an [`Uninit`] image, before passing it to [`decode_body_into`].
\ No newline at end of file diff --git a/src/decode_into.rs b/src/decode_into.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/decode_into.rs @@ -0,0 +1 @@ + diff --git a/src/encode.rs b/src/encode.rs new file mode 100644 index 0000000..c2a8c09 --- /dev/null +++ b/src/encode.rs @@ -0,0 +1,73 @@ +pub trait P<T: Copy> { + unsafe fn put<const N: usize>(&mut self, x: [T; N]); + #[cfg_attr(debug_assertions, track_caller)] + unsafe fn push(&mut self, x: T) { + self.put([x]); + } +} + +impl<T: Copy> P<T> for *mut T { + #[cfg_attr(debug_assertions, track_caller)] + unsafe fn put<const N: usize>(&mut self, x: [T; N]) { + self.copy_from(x.as_ptr(), N); + *self = self.add(N); + } +} + +pub const fn encode_bool(x: bool) -> u8 { + (x as u8) + b'0' +} + +const MAGIC: [u32; 256] = [ + 538976304, 538976305, 538976306, 538976307, 538976308, 538976309, 538976310, 538976311, + 538976312, 538976313, 538980401, 538980657, 538980913, 538981169, 538981425, 538981681, + 538981937, 538982193, 538982449, 538982705, 538980402, 538980658, 538980914, 538981170, + 538981426, 538981682, 538981938, 538982194, 538982450, 538982706, 538980403, 538980659, + 538980915, 538981171, 538981427, 538981683, 538981939, 538982195, 538982451, 538982707, + 538980404, 538980660, 538980916, 538981172, 538981428, 538981684, 538981940, 538982196, + 538982452, 538982708, 538980405, 538980661, 538980917, 538981173, 538981429, 538981685, + 538981941, 538982197, 538982453, 538982709, 538980406, 538980662, 538980918, 538981174, + 538981430, 538981686, 538981942, 538982198, 538982454, 538982710, 538980407, 538980663, + 538980919, 538981175, 538981431, 538981687, 538981943, 538982199, 538982455, 538982711, + 538980408, 538980664, 538980920, 538981176, 538981432, 538981688, 538981944, 538982200, + 538982456, 538982712, 538980409, 538980665, 538980921, 538981177, 538981433, 538981689, + 538981945, 538982201, 538982457, 538982713, 540028977, 540094513, 540160049, 540225585, + 540291121, 540356657, 540422193, 540487729, 540553265, 540618801, 540029233, 540094769, + 540160305, 540225841, 540291377, 540356913, 540422449, 540487985, 540553521, 540619057, + 0x20303231, 540095025, 540160561, 540226097, 540291633, 540357169, 540422705, 540488241, + 540553777, 540619313, 540029745, 540095281, 540160817, 540226353, 540291889, 540357425, + 540422961, 540488497, 540554033, 540619569, 540030001, 540095537, 540161073, 540226609, + 540292145, 540357681, 540423217, 540488753, 540554289, 540619825, 540030257, 540095793, + 540161329, 540226865, 540292401, 540357937, 540423473, 540489009, 540554545, 540620081, + 540030513, 540096049, 540161585, 540227121, 540292657, 540358193, 540423729, 540489265, + 540554801, 540620337, 540030769, 540096305, 540161841, 540227377, 540292913, 540358449, + 540423985, 540489521, 540555057, 540620593, 540031025, 540096561, 540162097, 540227633, + 540293169, 540358705, 540424241, 540489777, 540555313, 540620849, 540031281, 540096817, + 540162353, 540227889, 540293425, 540358961, 540424497, 540490033, 540555569, 540621105, + 540028978, 540094514, 540160050, 540225586, 540291122, 540356658, 540422194, 540487730, + 540553266, 540618802, 540029234, 540094770, 540160306, 540225842, 540291378, 540356914, + 540422450, 540487986, 540553522, 540619058, 540029490, 540095026, 540160562, 540226098, + 540291634, 540357170, 540422706, 540488242, 540553778, 540619314, 540029746, 540095282, + 540160818, 540226354, 540291890, 540357426, 540422962, 540488498, 540554034, 540619570, + 540030002, 540095538, 540161074, 540226610, 540292146, 540357682, 540423218, 540488754, + 540554290, 540619826, 540030258, 540095794, 540161330, 540226866, 540292402, 540357938, +]; + +/// Has a space. (_) +pub const fn encode_(x: u8) -> [u8; 4] { + MAGIC[x as usize].to_le_bytes() +} + +pub unsafe fn encodeu32(mut x: u32, buf: &mut *mut u8) { + let mut tmp = [0; 10]; + let mut tp = tmp.as_mut_ptr(); + while x >= 10 { + tp.write((x % 10) as u8 + b'0'); + tp = tp.add(1); + x /= 10; + } + tp.write(x as u8 + b'0'); + for b in tmp.into_iter().rev().skip_while(|&b| b == 0) { + buf.push(b); + } +} diff --git a/src/encode_into.md b/src/encode_into.md new file mode 100644 index 0000000..3dc02f2 --- /dev/null +++ b/src/encode_into.md @@ -0,0 +1,6 @@ +Encodes directly into a buffer. Returns number of bytes written. +Buffer should have at least [`size`] bytes. + +# Safety + +Undefined Behaviour when the buffer's length is less than [`size`]`(bytes)`.
\ No newline at end of file diff --git a/src/est.md b/src/est.md new file mode 100644 index 0000000..7ebd79c --- /dev/null +++ b/src/est.md @@ -0,0 +1 @@ +Inaccurate estimate. Will always be greater than the real number of bytes.
\ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f3cdf62 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,90 @@ +//! crate for decoding/encoding the portable anymap format. +#![allow(incomplete_features)] +#![feature( + test, + generic_const_exprs, + ptr_sub_ptr, + array_chunks, + let_chains, + iter_array_chunks +)] +#![warn( + clippy::missing_const_for_fn, + clippy::suboptimal_flops, + clippy::dbg_macro, + clippy::use_self +)] + +use fimg::{uninit, DynImage, Image}; +pub mod decode; +pub(crate) mod encode; +pub mod pam; +pub mod pbm; +pub mod pgm; +pub mod ppm; +pub use pam::PAM; + +/// Decode any [`pgm`], [`ppm`], [`pbm`], [`pam`] image. +pub fn decode(x: &impl AsRef<[u8]>) -> decode::Result<DynImage<Vec<u8>>> { + let mut x = x.as_ref(); + let magic = decode::magic(&mut x).ok_or(decode::Error::MissingMagic)?; + match magic { + pbm::raw::MAGIC => { + let header = decode::decode_header(&mut x, pbm::raw::MAGIC)?; + Ok(DynImage::Y(pbm::raw::decode_body_into_u8( + x, + uninit::Image::new(header.width, header.height), + )?)) + } + pbm::plain::MAGIC => { + let header = decode::decode_header(&mut x, pbm::plain::MAGIC)?; + Ok(DynImage::Y(pbm::plain::decode_body_into_u8( + x, + uninit::Image::new(header.width, header.height), + )?)) + } + pgm::raw::MAGIC => Ok(DynImage::Y(pgm::raw::decode_wo_magic(x)?)), + pgm::plain::MAGIC => Ok(DynImage::Y(pgm::plain::decode_wo_magic(x)?)), + ppm::raw::MAGIC => Ok(DynImage::Rgb(ppm::raw::decode_wo_magic(x)?)), + ppm::plain::MAGIC => Ok(DynImage::Rgb(ppm::plain::decode_wo_magic(x)?)), + pam::MAGIC => pam::decode_wo_magic(x), + _ => Err(decode::Error::BadMagic(magic)), + } +} + +/// Encodes an image to one of the [`pgm`] or [`ppm`] portable anymap formats. +/// +/// Please note that this will not produce a [`pam`], use [`PAM`] for that. +pub trait Encode { + /// Encodes an image to one of the [`pgm`] or [`ppm`] portable anymap formats. + fn encode(self) -> Vec<u8>; + /// ASCII EDITION. + fn encode_plain(self) -> String; +} + +macro_rules! x { + ($mod:ident) => { + impl<T: AsRef<[u8]>> Encode for Image<T, { $mod::CHANNELS }> { + fn encode(self) -> Vec<u8> { + $mod::raw::encode(self) + } + fn encode_plain(self) -> String { + $mod::plain::encode(self) + } + } + }; + (t $mod:ident, $n:literal) => { + impl<T: AsRef<[u8]>> Encode for Image<T, $n> { + fn encode(self) -> Vec<u8> { + $mod::raw::encode(<Image<Box<[u8]>, { $mod::CHANNELS }>>::from(self.as_ref())) + } + fn encode_plain(self) -> String { + $mod::plain::encode(<Image<Box<[u8]>, { $mod::CHANNELS }>>::from(self.as_ref())) + } + } + }; +} +x![pgm]; +x![t pgm, 2]; +x![ppm]; +x![t ppm, 4]; diff --git a/src/pam.rs b/src/pam.rs new file mode 100644 index 0000000..f55b4b8 --- /dev/null +++ b/src/pam.rs @@ -0,0 +1,360 @@ +//! [Portable Arbitrary Format](https://en.wikipedia.org/wiki/Netpbm#PAM_graphics_format) RGB (no alpha) image encoding and decoding. +pub type Input<'a> = Image<&'a [u8], 3>; +pub type Uninit = fimg::uninit::Image<u8, 3>; +use std::num::NonZeroU32; + +use crate::decode::{read_til, Error, Read, Result}; +use crate::encode::{encodeu32, P}; +use atools::Join; +use fimg::{DynImage, Image}; + +pub const MAGIC: u8 = 7; + +/// Encode an <code>[Image]<[u8], N></code> to [PAM](https://en.wikipedia.org/wiki/Netpbm#PAM_graphics_format) Raw (binary) Image. +/// And decode. +pub trait PAM { + /// Encode this image to pam. + fn encode(self) -> Vec<u8>; + #[doc = include_str!("encode_into.md")] + unsafe fn encode_into(x: Self, out: *mut u8) -> usize; +} + +/// Bool images. +/// Unstable api. +pub trait PAMBit { + /// Encode this bit image to pam. + fn encode_bitmap(self) -> Vec<u8>; + #[doc = include_str!("encode_into.md")] + unsafe fn encode_into(x: Self, out: *mut u8) -> usize; +} + +impl<T: AsRef<[u8]>> PAM for Image<T, 1> { + fn encode(self) -> Vec<u8> { + let mut y = Vec::with_capacity(size(self.bytes())); + let n = unsafe { PAM::encode_into(self.as_ref(), y.as_mut_ptr()) }; + unsafe { y.set_len(n) }; + y + } + + unsafe fn encode_into(x: Self, out: *mut u8) -> usize { + encode_into((x.bytes(), (x.width(), x.height())), out, b"GRAYSCALE", 1) + } +} + +impl<T: AsRef<[bool]>> PAMBit for Image<T, 1> { + fn encode_bitmap(self) -> Vec<u8> { + let mut y = Vec::with_capacity(size(self.as_ref().buffer())); + let n = unsafe { PAMBit::encode_into(self.as_ref(), y.as_mut_ptr()) }; + unsafe { y.set_len(n) }; + y + } + + unsafe fn encode_into(x: Self, out: *mut u8) -> usize { + let b = x.buffer().as_ref(); + let b = std::slice::from_raw_parts(b.as_ptr() as *mut u8, b.len()); + encode_into((b, (x.width(), x.height())), out, b"BLACKANDWHITE", 1) + } +} + +impl<T: AsRef<[u8]>> PAM for Image<T, 2> { + fn encode(self) -> Vec<u8> { + let mut y = Vec::with_capacity(size(self.bytes())); + let n = unsafe { PAM::encode_into(self.as_ref(), y.as_mut_ptr()) }; + unsafe { y.set_len(n) }; + y + } + + unsafe fn encode_into(x: Self, out: *mut u8) -> usize { + encode_into( + (x.bytes(), (x.width(), x.height())), + out, + b"GRAYSCALE_ALPHA", + 2, + ) + } +} + +impl<T: AsRef<[u8]>> PAM for Image<T, 3> { + fn encode(self) -> Vec<u8> { + let mut y = Vec::with_capacity(size(self.bytes())); + let n = unsafe { PAM::encode_into(self.as_ref(), y.as_mut_ptr()) }; + unsafe { y.set_len(n) }; + y + } + + unsafe fn encode_into(x: Self, out: *mut u8) -> usize { + encode_into((x.bytes(), (x.width(), x.height())), out, b"RGB", 3) + } +} + +impl<T: AsRef<[u8]>> PAM for Image<T, 4> { + fn encode(self) -> Vec<u8> { + let mut y = Vec::with_capacity(size(self.bytes())); + let n = unsafe { PAM::encode_into(self.as_ref(), y.as_mut_ptr()) }; + unsafe { y.set_len(n) }; + y + } + + unsafe fn encode_into(x: Self, out: *mut u8) -> usize { + encode_into((x.bytes(), (x.width(), x.height())), out, b"RGB_ALPHA", 2) + } +} + +#[inline] +unsafe fn encode_into<const N: usize>( + (buf, (w, h)): (&[u8], (u32, u32)), + out: *mut u8, + tupltype: &[u8; N], + depth: u8, +) -> usize { + let mut o = out; + o.put(b'P'.join(MAGIC + b'0')); + o.put(*b"\nWIDTH "); + encodeu32(w, &mut o); + o.put(*b"\nHEIGHT "); + encodeu32(h, &mut o); + o.put(*b"\nDEPTH "); + o.push(depth + b'0'); + o.put(*b"\nMAXVAL 255\n"); + o.put(*b"TUPLTYPE "); + o.put(*tupltype); + o.put(*b"\nENDHDR\n"); + for &x in buf { + if &tupltype[..] == b"BLACKANDWHITE" { + o.push(x ^ 1) + } else { + o.push(x) + } + } + + o.sub_ptr(out) +} + +#[derive(Copy, Clone, Debug)] +/// Header for PAM images. +pub struct PAMHeader { + pub width: NonZeroU32, + pub height: NonZeroU32, + /// Channel count + pub depth: u8, + /// Max value + pub max: u8, + /// Data type + pub tupltype: Type, +} + +/// Tupltype. See [pam wikipedia page](https://en.wikipedia.org/wiki/Netpbm#PAM_graphics_format) for more informaiton. +#[derive(Copy, Clone, Debug)] +pub enum Type { + /// Black and white bitmap type, corresponding to `BLACKANDWHITE` + Bit, + /// Grayscale type, corresponds to `GRAYSCALE` + Y, + RGB, + /// Black and white with alpha, `BLACKANDWHITE_ALPHA` + BitA, + /// Gray with alpha. `GRAYSCALE_ALPHA` + YA, + RGBA, +} + +impl Type { + const fn bytes(self) -> u8 { + use Type::*; + match self { + Bit | Y => 1, + BitA | YA => 2, + RGB => 3, + RGBA => 4, + } + } +} + +/// Decode a PAM image into a [`DynImage`]. +pub fn decode(mut x: &[u8]) -> Result<DynImage<Vec<u8>>> { + crate::decode::magic(&mut x); + decode_wo_magic(x) +} + +/// Decode a magicless PAM image. +pub fn decode_wo_magic(mut x: &[u8]) -> Result<DynImage<Vec<u8>>> { + let header = decode_pam_header(&mut x)?; + let mut alloc = Vec::with_capacity( + header.tupltype.bytes() as usize + * header.width.get() as usize + * header.height.get() as usize, + ); + let n = unsafe { decode_inner(x, alloc.as_mut_ptr(), header)? }; + unsafe { alloc.set_len(n) }; + Ok(match header.tupltype { + Type::Bit => unsafe { DynImage::Y(Image::new(header.width, header.height, alloc)) }, + Type::Y => unsafe { DynImage::Y(Image::new(header.width, header.height, alloc)) }, + Type::BitA => unsafe { DynImage::Ya(Image::new(header.width, header.height, alloc)) }, + Type::YA => unsafe { DynImage::Ya(Image::new(header.width, header.height, alloc)) }, + Type::RGB => unsafe { DynImage::Rgb(Image::new(header.width, header.height, alloc)) }, + Type::RGBA => unsafe { DynImage::Rgba(Image::new(header.width, header.height, alloc)) }, + }) +} + +/// Decodes this pam image's body, placing it in the raw pointer. +/// # Safety +/// +/// buffer must have [`size`] bytes of space. +pub unsafe fn decode_inner(x: &[u8], mut into: *mut u8, header: PAMHeader) -> Result<usize> { + let n = header.tupltype.bytes() as usize + * header.width.get() as usize + * header.height.get() as usize; + match header.tupltype { + Type::Bit => x + .iter() + .map(|&x| x * 0xff) + .take(n) + .for_each(|x| into.push(x)), + Type::BitA => x + .iter() + .array_chunks::<2>() + .take(n) + .map(|[&x, &a]| [x * 0xff, a]) + .for_each(|x| into.put(x)), + Type::Y | Type::YA | Type::RGB | Type::RGBA => { + into.copy_from(x.as_ptr(), n); + } + } + Ok(n) +} + +/// expects no magic +pub fn decode_pam_header(x: &mut &[u8]) -> Result<PAMHeader> { + macro_rules! test { + ($for:literal else $e:ident) => { + if x.rd().ok_or(Error::$e)? != *$for { + return Err(Error::$e); + }; + }; + } + test![b"WIDTH " else MissingWidth]; + let width = NonZeroU32::new(read_til(x)?).ok_or(Error::ZeroWidth)?; + test![b"HEIGHT " else MissingHeight]; + let height = NonZeroU32::new(read_til(x)?).ok_or(Error::ZeroHeight)?; + width.checked_mul(height).ok_or(Error::TooLarge)?; + test![b"DEPTH " else MissingDepth]; + let depth = read_til::<u8>(x)?; + test![b"MAXVAL " else MissingMax]; + let max = read_til::<u8>(x)?; + test![b"TUPLTYPE " else MissingTupltype]; + let end = x + .iter() + .position(|&x| x == b'\n') + .ok_or(Error::MissingTupltype)?; + let tupltype = match &x[..end] { + b"BLACKANDWHITE" => Type::Bit, + b"BLACKANDWHITE_ALPHA" => Type::BitA, + b"GRAYSCALE" => Type::Y, + b"GRAYSCALE_ALPHA" => Type::YA, + b"RGB" => Type::RGB, + b"RGB_ALPHA" => Type::RGBA, + _ => return Err(Error::MissingTupltype), + }; + *x = &x[end..]; + test![b"\nENDHDR\n" else MissingData]; + Ok(PAMHeader { + width, + height, + depth, + max, + tupltype, + }) +} + +#[doc = include_str!("est.md")] +pub const fn size<T>(x: &[T]) -> usize { + 92 + x.len() +} + +#[test] +fn test_bit() { + assert_eq!( + PAMBit::encode_bitmap( + Image::<_, 1>::build(20, 15).buf( + include_bytes!("../tdata/fimg.imgbuf") + .iter() + .map(|&x| x <= 128) + .collect::<Vec<_>>(), + ), + ), + include_bytes!("../tdata/fimg.pam") + ); + + assert_eq!( + &**decode(include_bytes!("../tdata/fimg.pam")) + .unwrap() + .buffer(), + include_bytes!("../tdata/fimg.imgbuf") + ); +} + +#[test] +fn test_y() { + assert_eq!( + PAM::encode( + Image::<_, 1>::build(20, 15).buf(&include_bytes!("../tdata/fimg-gray.imgbuf")[..]) + ), + include_bytes!("../tdata/fimg-gray.pam") + ); + assert_eq!( + &**decode(include_bytes!("../tdata/fimg-gray.pam")) + .unwrap() + .buffer(), + include_bytes!("../tdata/fimg-gray.imgbuf") + ); +} + +#[test] +fn test_ya() { + assert_eq!( + PAM::encode( + Image::<_, 2>::build(20, 15) + .buf(&include_bytes!("../tdata/fimg-transparent.imgbuf")[..]) + ), + include_bytes!("../tdata/fimg-transparent.pam") + ); + assert_eq!( + &**decode(include_bytes!("../tdata/fimg-transparent.pam")) + .unwrap() + .buffer(), + include_bytes!("../tdata/fimg-transparent.imgbuf") + ); +} + +#[test] +fn test_rgb() { + assert_eq!( + PAM::encode( + Image::<_, 3>::build(20, 15).buf(&include_bytes!("../tdata/fimg-rainbow.imgbuf")[..]) + ), + include_bytes!("../tdata/fimg-rainbow.pam") + ); + assert_eq!( + &**decode(include_bytes!("../tdata/fimg-rainbow.pam")) + .unwrap() + .buffer(), + include_bytes!("../tdata/fimg-rainbow.imgbuf") + ); +} + +#[test] +fn test_rgba() { + assert_eq!( + PAM::encode( + Image::<_, 4>::build(20, 15) + .buf(&include_bytes!("../tdata/fimg-rainbow-transparent.imgbuf")[..]) + ), + include_bytes!("../tdata/fimg-rainbow-transparent.pam") + ); + assert_eq!( + &**decode(include_bytes!("../tdata/fimg-rainbow-transparent.pam")) + .unwrap() + .buffer(), + include_bytes!("../tdata/fimg-rainbow-transparent.imgbuf") + ); +} diff --git a/src/pbm.rs b/src/pbm.rs new file mode 100644 index 0000000..40b3bf3 --- /dev/null +++ b/src/pbm.rs @@ -0,0 +1,250 @@ +//! [Portable BitMap Format](https://en.wikipedia.org/wiki/Netpbm#PBM_example) black and white image encoding and decoding. +//! +//! Unstable api. +pub type Input<'a> = Image<&'a [bool], 1>; +pub type Output = Image<Vec<bool>, 1>; +pub type Uninit = fimg::uninit::Image<bool, 1>; +use crate::encode::{encodeu32, P}; +use atools::prelude::*; +use fimg::Image; + +#[cfg(test)] +fn tdata() -> Vec<bool> { + include_bytes!("../tdata/fimg.imgbuf") + .iter() + .map(|&x| x <= 128) + .collect::<Vec<_>>() +} + +/// Module for handling plain ascii (human readable) [PBM](https://en.wikipedia.org/wiki/Netpbm#PBM_example) (black and white) images. +pub mod plain { + use crate::encode::encode_bool; + + use super::*; + pub const MAGIC: u8 = 1; + + /// Encode an <code>[Image]<[bool], 1></code> into a [PBM](https://en.wikipedia.org/wiki/Netpbm#PBM_example) ASCII Image. + pub fn encode<T: AsRef<[bool]>>(x: Image<T, 1>) -> String { + let mut y = Vec::with_capacity(size(x.as_ref())); + let n = unsafe { encode_into(x.as_ref(), y.as_mut_ptr()) }; + unsafe { y.set_len(n) }; + unsafe { String::from_utf8_unchecked(y) } + } + + crate::decode::dec_fn! { + "Decode an ASCII [PBM](https://en.wikipedia.org/wiki/Netpbm#PBM_example) image into an <code>[Image]<[Box]<[bool]>, 1></code>" + } + + #[doc = include_str!("decode_body_into.md")] + pub fn decode_body_into(x: &[u8], mut into: Uninit) -> Result<Output> { + let mut out = into.buf().as_mut_ptr() as *mut bool; + let pixels = into.width() * into.height(); + for &b in x + .iter() + .filter(|&&x| matches!(x, b'0' | b'1')) + .take(pixels as usize) + { + // SAFETY: iterator over `pixels` elements. + unsafe { out.push(b == b'1') }; + } + if unsafe { out.sub_ptr(into.buf().as_mut_ptr().cast()) < pixels as usize } { + return Err(Error::MissingData); + } + // SAFETY: checked that the pixels have been initialized. + Ok(unsafe { into.assume_init() }) + } + + /// Converts 0 to 255 and 1 to 0, for your u8 image experience. + pub fn decode_body_into_u8( + x: &[u8], + mut into: fimg::uninit::Image<u8, 1>, + ) -> Result<Image<Vec<u8>, 1>> { + let mut out = into.buf().as_mut_ptr() as *mut u8; + let pixels = into.width() * into.height(); + for &b in x + .iter() + .filter(|&&x| matches!(x, b'0' | b'1')) + .take(pixels as usize) + { + // SAFETY: iterator over `pixels` elements. + unsafe { out.push((b == b'0') as u8 * 0xff) }; + } + if unsafe { out.sub_ptr(into.buf().as_mut_ptr().cast()) < pixels as usize } { + return Err(Error::MissingData); + } + // SAFETY: checked that the pixels have been initialized. + Ok(unsafe { into.assume_init() }) + } + + #[doc = include_str!("encode_into.md")] + pub unsafe fn encode_into(x: Input, out: *mut u8) -> usize { + let mut o = out; + o.put(b'P'.join(MAGIC + b'0')); + o.push(b' '); + encodeu32(x.width(), &mut o); + o.push(b' '); + encodeu32(x.height(), &mut o); + o.push(b'\n'); + for row in x.buffer().chunks_exact(x.width() as _) { + for &on in row { + o.push(encode_bool(on)); + // cosmetic + o.push(b' '); + } + // cosmetic + o.push(b'\n'); + } + o.sub_ptr(out) + } + + #[doc = include_str!("est.md")] + pub fn size(x: Input) -> usize { + 2 // P1 + + 23 // \n4294967295 4294967295\n + + x.height() as usize // \n + + x.len() * 2 // ' 1' + } + + #[test] + fn test_encode() { + assert_eq!( + encode(Image::build(20, 15).buf(tdata())), + include_str!("../tdata/fimgA.pbm") + ); + } + + #[test] + fn test_decode() { + assert_eq!( + &**decode(include_bytes!("../tdata/fimgA.pbm")) + .unwrap() + .buffer(), + tdata() + ) + } +} + +/// Module for handling raw (packed binary) [PBM](https://en.wikipedia.org/wiki/Netpbm#PBM_example) (black and white) images. +pub mod raw { + use super::*; + pub const MAGIC: u8 = 4; + /// Encode an <code>[Image]<[bool], 1></code> [PBM](https://en.wikipedia.org/wiki/Netpbm#PBM_example) Raw (packed binary) Image. + pub fn encode<T: AsRef<[bool]>>(x: Image<T, 1>) -> Vec<u8> { + let mut y = Vec::with_capacity(size(x.as_ref())); + let n = unsafe { encode_into(x.as_ref(), y.as_mut_ptr()) }; + unsafe { y.set_len(n) }; + y + } + + crate::decode::dec_fn! { + "Decode a raw binary [PBM](https://en.wikipedia.org/wiki/Netpbm#PBM_example) image into an <code>[Image]<[Box]<[bool]>, 1></code>" + } + + #[doc = include_str!("encode_into.md")] + pub unsafe fn encode_into(x: Input, out: *mut u8) -> usize { + let mut o = out; + o.put(b'P'.join(MAGIC + b'0')); + o.push(b' '); + encodeu32(x.width(), &mut o); + o.push(b' '); + encodeu32(x.height(), &mut o); + o.push(b'\n'); + x.buffer() + .chunks_exact(x.width() as _) + .flat_map(|x| x.chunks(8)) + .map(|chunk| { + chunk + .iter() + .copied() + .chain(std::iter::repeat(false).take(8 - chunk.len())) + .zip(0u8..) + .fold(0, |acc, (x, i)| acc | (x as u8) << 7 - i) + }) + .for_each(|x| o.push(x)); + + o.sub_ptr(out) + } + + #[doc = include_str!("decode_body_into.md")] + pub fn decode_body_into(x: &[u8], mut into: Uninit) -> Result<Output> { + let mut out = into.buf().as_mut_ptr() as *mut bool; + let pixels = into.width() * into.height(); + let padding = into.width() % 8; + for &x in x + .iter() + .copied() + // expand the bits + .flat_map(|b| atools::range::<8>().rev().map(|x| b & (1 << x) != 0)) + // TODO skip? + .collect::<Vec<_>>() + .chunks_exact((into.width() + padding) as _) + .map(|x| &x[..into.width() as _]) + .take(pixels as _) + .flatten() + { + // SAFETY: took `pixels` pixels. + unsafe { out.push(x) }; + } + if unsafe { out.sub_ptr(into.buf().as_mut_ptr().cast()) < pixels as usize } { + return Err(Error::MissingData); + } + // SAFETY: checked that the pixels have been initialized. + Ok(unsafe { into.assume_init() }) + } + + #[doc = include_str!("decode_body_into.md")] + pub fn decode_body_into_u8( + x: &[u8], + mut into: fimg::uninit::Image<u8, 1>, + ) -> Result<Image<Vec<u8>, 1>> { + let mut out = into.buf().as_mut_ptr() as *mut u8; + let pixels = into.width() * into.height(); + let padding = into.width() % 8; + for x in x + .iter() + .copied() + // expand the bits + .flat_map(|b| atools::range::<8>().rev().map(|x| b & (1 << x) == 0)) + // TODO skip? + .collect::<Vec<_>>() + .chunks_exact((into.width() + padding) as _) + .map(|x| &x[..into.width() as _]) + .take(pixels as _) + .flatten() + .map(|&x| x as u8 * 0xff) + { + // SAFETY: took `pixels` pixels. + unsafe { out.push(x) }; + } + if unsafe { out.sub_ptr(into.buf().as_mut_ptr().cast()) < pixels as usize } { + return Err(Error::MissingData); + } + // SAFETY: checked that the pixels have been initialized. + Ok(unsafe { into.assume_init() }) + } + #[doc = include_str!("est.md")] + pub fn size(x: Input) -> usize { + 2 // magic + + 23 // w h + + (x.len() / 8) // packed pixels + + ((x.width() as usize % 8 != 0) as usize * x.height() as usize) // padding + } + + #[test] + fn test_decode() { + assert_eq!( + &**decode(include_bytes!("../tdata/fimgR.pbm")) + .unwrap() + .buffer(), + tdata() + ) + } + + #[test] + fn test_encode() { + assert_eq!( + encode(Image::build(20, 15).buf(tdata())), + include_bytes!("../tdata/fimgR.pbm") + ); + } +} diff --git a/src/pgm.rs b/src/pgm.rs new file mode 100644 index 0000000..0b844d8 --- /dev/null +++ b/src/pgm.rs @@ -0,0 +1,180 @@ +//! [Portable GreyMap Format](https://en.wikipedia.org/wiki/Netpbm#PGM_example) grey image encoding and decoding. +pub(crate) const CHANNELS: usize = 1; +pub type Input<'a> = Image<&'a [u8], 1>; +pub type Output = Image<Vec<u8>, 1>; +pub type Uninit = fimg::uninit::Image<u8, 1>; +use crate::encode::{encodeu32, P}; +use atools::prelude::*; +use fimg::Image; + +#[cfg(test)] +fn tdata() -> &'static [u8] { + include_bytes!("../tdata/fimg-gray.imgbuf") +} + +/// Module for handling plain ascii (human readable) [PGM](https://en.wikipedia.org/wiki/Netpbm#PGM_example) (Y) images. +pub mod plain { + + use crate::encode::encode_; + + use super::*; + pub const MAGIC: u8 = 2; + + /// Encode an <code>[Image]<[u8], 1></code> into a [PBM](https://en.wikipedia.org/wiki/Netpbm#PBM_example) ASCII Image. + pub fn encode<T: AsRef<[u8]>>(x: Image<T, 1>) -> String { + let mut y = Vec::with_capacity(size(x.as_ref())); + let n = unsafe { encode_into(x.as_ref(), y.as_mut_ptr()) }; + unsafe { y.set_len(n) }; + unsafe { String::from_utf8_unchecked(y) } + } + + crate::decode::dec_fn! { + max "Decode an ASCII [PGM](https://en.wikipedia.org/wiki/Netpbm#PGM_example) image into an <code>[Image]<[Box]<[u8]>, 1></code>" + } + + #[doc = include_str!("decode_body_into.md")] + pub fn decode_body_into(x: &[u8], mut into: Uninit, max: u8) -> Result<Output> { + let mut out = into.buf().as_mut_ptr() as *mut u8; + let pixels = into.width() * into.height(); + for b in x + .split(u8::is_ascii_whitespace) + .filter(|x| !x.is_empty() && x.len() <= 3) + .filter(|x| x.iter().all(u8::is_ascii_digit)) + .map(|x| x.iter().fold(0, |acc, &x| acc * 10 + (x - b'0'))) + .map(|x| { + if max == 255 { + x + } else { + ((x as f32 / max as f32) * 255.) as u8 + } + }) + .take(pixels as usize) + { + // SAFETY: iterator over `pixels` elements. + unsafe { out.push(b) }; + } + if unsafe { out.sub_ptr(into.buf().as_mut_ptr().cast()) < pixels as usize } { + return Err(Error::MissingData); + } + // SAFETY: checked that the pixels have been initialized. + Ok(unsafe { into.assume_init() }) + } + + #[doc = include_str!("encode_into.md")] + pub unsafe fn encode_into(x: Input, out: *mut u8) -> usize { + let mut o = out; + o.put(b'P'.join(MAGIC + b'0')); + o.push(b' '); + encodeu32(x.width(), &mut o); + o.push(b' '); + encodeu32(x.height(), &mut o); + o.put(*b" 255\n"); + for row in x.buffer().chunks_exact(x.width() as _) { + for &on in row { + o.put(encode_(on)); + } + // cosmetic + o.push(b'\n'); + } + o.sub_ptr(out) + } + + #[doc = include_str!("est.md")] + pub fn size(x: Input) -> usize { + 2 // P1 + + 23 // \n4294967295 4294967295\n + + x.height() as usize // \n + + x.len() * 4 // '255 ' + } + + #[test] + fn test_encode() { + assert_eq!( + encode(Image::build(20, 15).buf(tdata())), + include_str!("../tdata/fimgA.pgm") + ); + } + + #[test] + fn test_decode() { + assert_eq!( + &**decode(include_bytes!("../tdata/fimgA.pgm")) + .unwrap() + .buffer(), + tdata() + ) + } +} + +/// Module for handling raw (binary) [PGM](https://en.wikipedia.org/wiki/Netpbm#PGM_example) (gray) images. +pub mod raw { + use super::*; + pub const MAGIC: u8 = 5; + /// Encode an <code>[Image]<[u8], 1></code> [PBM](https://en.wikipedia.org/wiki/Netpbm#PGM_example) Raw (binary) Image. + pub fn encode<T: AsRef<[u8]>>(x: Image<T, 1>) -> Vec<u8> { + let mut y = Vec::with_capacity(size(x.as_ref())); + let n = unsafe { encode_into(x.as_ref(), y.as_mut_ptr()) }; + unsafe { y.set_len(n) }; + y + } + + crate::decode::dec_fn! { + "Decode a raw binary [PGM](https://en.wikipedia.org/wiki/Netpbm#PGM_example) image into an <code>[Image]<[Box]<[u8]>, 1></code>" + } + + #[doc = include_str!("encode_into.md")] + pub unsafe fn encode_into(x: Input, out: *mut u8) -> usize { + let mut o = out; + o.put(b'P'.join(MAGIC + b'0')); + o.push(b' '); + encodeu32(x.width(), &mut o); + o.push(b' '); + encodeu32(x.height(), &mut o); + o.put(*b" 255\n"); + for &x in *x.buffer() { + o.push(x) + } + + o.sub_ptr(out) + } + + #[doc = include_str!("decode_body_into.md")] + pub fn decode_body_into(x: &[u8], mut into: Uninit) -> Result<Output> { + let mut out = into.buf().as_mut_ptr() as *mut u8; + let pixels = into.width() * into.height(); + for b in x.iter().copied().take(pixels as _) { + // SAFETY: took `pixels` pixels. + unsafe { out.push(b) }; + } + if unsafe { out.sub_ptr(into.buf().as_mut_ptr().cast()) < pixels as usize } { + return Err(Error::MissingData); + } + // SAFETY: checked that the pixels have been initialized. + Ok(unsafe { into.assume_init() }) + } + + #[doc = include_str!("est.md")] + pub fn size(x: Input) -> usize { + 2 // magic + + 23 // w h + + x.len() // data + } + + #[test] + fn test_decode() { + assert_eq!( + &**decode(include_bytes!("../tdata/fimgR.pgm")) + .unwrap() + .buffer(), + tdata() + ) + } + + #[test] + fn test_encode() { + assert_eq!( + encode(Image::build(20, 15).buf(tdata())), + include_bytes!("../tdata/fimgR.pgm") + ); + } +} diff --git a/src/ppm.rs b/src/ppm.rs new file mode 100644 index 0000000..5c62062 --- /dev/null +++ b/src/ppm.rs @@ -0,0 +1,180 @@ +//! [Portable PixMap Format](https://en.wikipedia.org/wiki/Netpbm#PPM_example) RGB (no alpha) image encoding and decoding. +pub(crate) const CHANNELS: usize = 3; +pub type Input<'a> = Image<&'a [u8], 3>; +pub type Output = Image<Vec<u8>, 3>; +pub type Uninit = fimg::uninit::Image<u8, 3>; +use crate::encode::{encodeu32, P}; +use atools::prelude::*; +use fimg::Image; + +#[cfg(test)] +fn tdata() -> &'static [u8] { + include_bytes!("../tdata/fimg-rainbow.imgbuf") +} + +/// Module for handling plain ascii (human readable) [PPM](https://en.wikipedia.org/wiki/Netpbm#PPM_example) (Y) images. +pub mod plain { + use crate::encode::encode_; + + use super::*; + pub const MAGIC: u8 = 3; + + /// Encode an <code>[Image]<[u8], 3></code> into a [PPM](https://en.wikipedia.org/wiki/Netpbm#PPM_example) ASCII Image. + pub fn encode<T: AsRef<[u8]>>(x: Image<T, 3>) -> String { + let mut y = Vec::with_capacity(size(x.as_ref())); + let n = unsafe { encode_into(x.as_ref(), y.as_mut_ptr()) }; + unsafe { y.set_len(n) }; + unsafe { String::from_utf8_unchecked(y) } + } + + crate::decode::dec_fn! { + max "Decode an ASCII [PPM](https://en.wikipedia.org/wiki/Netpbm#PPM_example) image into an <code>[Image]<[Box]<[u8]>, 3></code>" + } + + #[doc = include_str!("decode_body_into.md")] + pub fn decode_body_into(x: &[u8], mut into: Uninit, max: u8) -> Result<Output> { + let mut out = into.buf().as_mut_ptr() as *mut u8; + let pixels = into.width() * into.height(); + for b in x + .split(u8::is_ascii_whitespace) + .filter(|x| !x.is_empty() && x.len() <= 3) + .filter(|x| x.iter().all(u8::is_ascii_digit)) + .map(|x| x.iter().fold(0, |acc, &x| acc * 10 + (x - b'0'))) + .map(|x| { + if max == 255 { + x + } else { + ((x as f32 / max as f32) * 255.) as u8 + } + }) + .array_chunks::<3>() + .take(pixels as usize) + { + // SAFETY: iterator over `pixels` elements. + unsafe { out.put(b) }; + } + if unsafe { out.sub_ptr(into.buf().as_mut_ptr().cast()) < (pixels * 3) as usize } { + return Err(Error::MissingData); + } + // SAFETY: checked that the pixels have been initialized. + Ok(unsafe { into.assume_init() }) + } + + #[doc = include_str!("encode_into.md")] + pub unsafe fn encode_into(x: Input, out: *mut u8) -> usize { + let mut o = out; + o.put(b'P'.join(MAGIC + b'0')); + o.push(b' '); + encodeu32(x.width(), &mut o); + o.push(b' '); + encodeu32(x.height(), &mut o); + o.put(*b" 255\n"); + for row in x.flatten().chunks_exact(x.width() as _) { + for &on in row.iter().flatten() { + o.put(encode_(on)); + } + // cosmetic + o.push(b'\n'); + } + o.sub_ptr(out) + } + + #[doc = include_str!("est.md")] + pub fn size(x: Input) -> usize { + 2 // P1 + + 23 // \n4294967295 4294967295\n + + x.height() as usize // \n + + x.len() * 4 // '255 ' + } + + #[test] + fn test_encode() { + assert_eq!( + encode(Image::build(20, 15).buf(tdata())), + include_str!("../tdata/fimgA.ppm") + ); + } + + #[test] + fn test_decode() { + assert_eq!( + &**decode(include_bytes!("../tdata/fimgA.ppm")) + .unwrap() + .buffer(), + tdata() + ) + } +} + +/// Module for handling raw (binary) [PPM](https://en.wikipedia.org/wiki/Netpbm#PPM_example) (rgb) images. +pub mod raw { + use super::*; + pub const MAGIC: u8 = 6; + /// Encode an <code>[Image]<[u8], 3></code> [PPM](https://en.wikipedia.org/wiki/Netpbm#PPM_example) Raw (binary) Image. + pub fn encode<T: AsRef<[u8]>>(x: Image<T, 3>) -> Vec<u8> { + let mut y = Vec::with_capacity(size(x.as_ref())); + let n = unsafe { encode_into(x.as_ref(), y.as_mut_ptr()) }; + unsafe { y.set_len(n) }; + y + } + + crate::decode::dec_fn! { + "Decode a raw binary [PPM](https://en.wikipedia.org/wiki/Netpbm#PPM_example) image into an <code>[Image]<[Box]<[u8]>, 3></code>" + } + + #[doc = include_str!("encode_into.md")] + pub unsafe fn encode_into(x: Input, out: *mut u8) -> usize { + let mut o = out; + o.put(b'P'.join(MAGIC + b'0')); + o.push(b' '); + encodeu32(x.width(), &mut o); + o.push(b' '); + encodeu32(x.height(), &mut o); + o.put(*b" 255\n"); + for &x in *x.buffer() { + o.push(x) + } + + o.sub_ptr(out) + } + + #[doc = include_str!("decode_body_into.md")] + pub fn decode_body_into(x: &[u8], mut into: Uninit) -> Result<Output> { + let mut out = into.buf().as_mut_ptr() as *mut u8; + let pixels = into.width() * into.height(); + for b in x.iter().copied().array_chunks::<3>().take(pixels as _) { + // SAFETY: took `pixels` pixels. + unsafe { out.put(b) }; + } + if unsafe { out.sub_ptr(into.buf().as_mut_ptr().cast()) < (pixels * 3) as usize } { + return Err(Error::MissingData); + } + // SAFETY: checked that the pixels have been initialized. + Ok(unsafe { into.assume_init() }) + } + + #[doc = include_str!("est.md")] + pub fn size(x: Input) -> usize { + 2 // magic + + 23 // w h + + x.len() // data + } + + #[test] + fn test_decode() { + assert_eq!( + &**decode(include_bytes!("../tdata/fimgR.ppm")) + .unwrap() + .buffer(), + tdata() + ) + } + + #[test] + fn test_encode() { + assert_eq!( + encode(Image::build(20, 15).buf(tdata())), + include_bytes!("../tdata/fimgR.ppm") + ); + } +} diff --git a/tdata/fimg-gray.imgbuf b/tdata/fimg-gray.imgbuf Binary files differnew file mode 100644 index 0000000..2aede56 --- /dev/null +++ b/tdata/fimg-gray.imgbuf diff --git a/tdata/fimg-gray.pam b/tdata/fimg-gray.pam Binary files differnew file mode 100644 index 0000000..c32beb8 --- /dev/null +++ b/tdata/fimg-gray.pam diff --git a/tdata/fimg-gray.png b/tdata/fimg-gray.png Binary files differnew file mode 100644 index 0000000..59e360f --- /dev/null +++ b/tdata/fimg-gray.png diff --git a/tdata/fimg-rainbow-transparent.imgbuf b/tdata/fimg-rainbow-transparent.imgbuf Binary files differnew file mode 100644 index 0000000..9a1c7a3 --- /dev/null +++ b/tdata/fimg-rainbow-transparent.imgbuf diff --git a/tdata/fimg-rainbow-transparent.pam b/tdata/fimg-rainbow-transparent.pam Binary files differnew file mode 100644 index 0000000..3dfacd6 --- /dev/null +++ b/tdata/fimg-rainbow-transparent.pam diff --git a/tdata/fimg-rainbow-transparent.png b/tdata/fimg-rainbow-transparent.png Binary files differnew file mode 100644 index 0000000..5df2eaf --- /dev/null +++ b/tdata/fimg-rainbow-transparent.png diff --git a/tdata/fimg-rainbow.imgbuf b/tdata/fimg-rainbow.imgbuf Binary files differnew file mode 100644 index 0000000..1a71b3f --- /dev/null +++ b/tdata/fimg-rainbow.imgbuf diff --git a/tdata/fimg-rainbow.pam b/tdata/fimg-rainbow.pam Binary files differnew file mode 100644 index 0000000..c6fdcd0 --- /dev/null +++ b/tdata/fimg-rainbow.pam diff --git a/tdata/fimg-rainbow.png b/tdata/fimg-rainbow.png Binary files differnew file mode 100644 index 0000000..c27fa30 --- /dev/null +++ b/tdata/fimg-rainbow.png diff --git a/tdata/fimg-rgb.pam b/tdata/fimg-rgb.pam Binary files differnew file mode 100644 index 0000000..c6fdcd0 --- /dev/null +++ b/tdata/fimg-rgb.pam diff --git a/tdata/fimg-transparent.imgbuf b/tdata/fimg-transparent.imgbuf Binary files differnew file mode 100644 index 0000000..9d730fe --- /dev/null +++ b/tdata/fimg-transparent.imgbuf diff --git a/tdata/fimg-transparent.pam b/tdata/fimg-transparent.pam Binary files differnew file mode 100644 index 0000000..837c323 --- /dev/null +++ b/tdata/fimg-transparent.pam diff --git a/tdata/fimg-transparent.png b/tdata/fimg-transparent.png Binary files differnew file mode 100644 index 0000000..8c5f5a1 --- /dev/null +++ b/tdata/fimg-transparent.png diff --git a/tdata/fimg.imgbuf b/tdata/fimg.imgbuf Binary files differnew file mode 100644 index 0000000..668a3e5 --- /dev/null +++ b/tdata/fimg.imgbuf diff --git a/tdata/fimg.pam b/tdata/fimg.pam Binary files differnew file mode 100644 index 0000000..03b0a4f --- /dev/null +++ b/tdata/fimg.pam diff --git a/tdata/fimg.png b/tdata/fimg.png Binary files differnew file mode 100644 index 0000000..63d5670 --- /dev/null +++ b/tdata/fimg.png diff --git a/tdata/fimgA.pbm b/tdata/fimgA.pbm new file mode 100644 index 0000000..177c826 --- /dev/null +++ b/tdata/fimgA.pbm @@ -0,0 +1,16 @@ +P1 20 15 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 +1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 +1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 +1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 +1 1 0 0 0 1 0 1 0 0 0 0 0 1 0 0 0 0 1 1 +1 1 1 0 1 1 1 1 0 1 0 1 0 1 0 1 1 0 1 1 +1 1 1 0 1 1 0 1 0 1 0 1 0 1 0 1 1 0 1 1 +1 1 1 0 1 1 0 1 0 1 0 1 0 1 0 1 1 0 1 1 +1 1 1 0 1 1 0 1 0 1 0 1 0 1 0 0 0 0 1 1 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 diff --git a/tdata/fimgA.pgm b/tdata/fimgA.pgm new file mode 100644 index 0000000..8db43f3 --- /dev/null +++ b/tdata/fimgA.pgm @@ -0,0 +1,16 @@ +P2 20 15 255 +162 152 143 133 124 114 105 95 86 77 67 58 48 39 29 20 11 1 0 0 +161 152 142 133 123 114 105 95 86 76 67 57 48 38 29 20 10 1 0 0 +161 151 142 255 255 255 255 95 85 76 66 57 47 38 29 19 10 0 0 0 +160 151 142 255 123 113 104 94 85 75 66 57 47 38 28 19 9 0 0 0 +160 151 141 255 122 113 103 94 84 75 66 56 47 37 28 18 9 0 0 0 +160 150 255 255 255 112 255 94 255 255 255 255 255 37 255 255 255 255 0 0 +159 150 140 255 121 112 103 93 255 74 255 55 255 36 255 0 0 255 0 0 +159 149 140 255 121 112 255 93 255 74 255 55 255 36 255 0 0 255 0 0 +158 149 140 255 121 111 255 92 255 73 255 55 255 36 255 0 0 255 0 0 +158 149 139 255 120 111 255 92 255 73 255 54 255 35 255 255 255 255 0 0 +158 148 139 129 120 110 101 92 82 73 63 54 44 35 25 16 7 255 0 0 +157 148 138 129 119 110 101 91 82 72 63 53 44 35 25 16 6 255 0 0 +157 147 138 129 119 110 100 91 81 72 62 53 44 34 255 255 255 255 0 0 +156 147 138 128 119 109 100 90 81 71 62 53 43 34 24 15 5 0 0 0 +156 147 137 128 118 109 99 90 81 71 62 52 43 33 24 14 5 0 0 0 diff --git a/tdata/fimgA.ppm b/tdata/fimgA.ppm new file mode 100644 index 0000000..fe0ae58 --- /dev/null +++ b/tdata/fimgA.ppm @@ -0,0 +1,16 @@ +P3 20 15 255 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 0 14 255 115 0 255 115 0 255 115 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 0 14 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 0 14 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 255 0 14 255 0 14 255 115 0 0 0 0 255 115 0 0 0 0 250 210 32 250 210 32 19 143 62 19 143 62 19 143 62 0 0 0 53 88 160 53 88 160 136 0 130 136 0 130 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 0 14 0 0 0 0 0 0 0 0 0 0 0 0 250 210 32 0 0 0 19 143 62 0 0 0 19 143 62 0 0 0 53 88 160 0 0 0 0 0 0 136 0 130 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 0 14 0 0 0 0 0 0 255 115 0 0 0 0 250 210 32 0 0 0 19 143 62 0 0 0 19 143 62 0 0 0 53 88 160 0 0 0 0 0 0 136 0 130 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 0 14 0 0 0 0 0 0 255 115 0 0 0 0 250 210 32 0 0 0 19 143 62 0 0 0 19 143 62 0 0 0 53 88 160 0 0 0 0 0 0 136 0 130 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 0 14 0 0 0 0 0 0 255 115 0 0 0 0 250 210 32 0 0 0 19 143 62 0 0 0 19 143 62 0 0 0 53 88 160 53 88 160 136 0 130 136 0 130 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 136 0 130 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 136 0 130 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 53 88 160 53 88 160 136 0 130 136 0 130 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/tdata/fimgR.pbm b/tdata/fimgR.pbm new file mode 100644 index 0000000..bec7383 --- /dev/null +++ b/tdata/fimgR.pbm @@ -0,0 +1,2 @@ +P4 20 15 +����������������0�U��U��U��T0��������0������
\ No newline at end of file diff --git a/tdata/fimgR.pgm b/tdata/fimgR.pgm Binary files differnew file mode 100644 index 0000000..02ca1c1 --- /dev/null +++ b/tdata/fimgR.pgm diff --git a/tdata/fimgR.ppm b/tdata/fimgR.ppm Binary files differnew file mode 100644 index 0000000..2bf5d88 --- /dev/null +++ b/tdata/fimgR.ppm |