pnm decoding and encoding
bendn 2024-03-15
commit 66b1c09
-rw-r--r--.gitignore1
-rw-r--r--Cargo.toml18
-rw-r--r--LICENSE21
-rw-r--r--README.md3
-rw-r--r--examples/viewer.rs8
-rw-r--r--src/decode.rs176
-rw-r--r--src/decode_body_into.md3
-rw-r--r--src/decode_into.rs1
-rw-r--r--src/encode.rs73
-rw-r--r--src/encode_into.md6
-rw-r--r--src/est.md1
-rw-r--r--src/lib.rs90
-rw-r--r--src/pam.rs360
-rw-r--r--src/pbm.rs250
-rw-r--r--src/pgm.rs180
-rw-r--r--src/ppm.rs180
-rw-r--r--tdata/fimg-gray.imgbufbin0 -> 300 bytes
-rw-r--r--tdata/fimg-gray.pambin0 -> 367 bytes
-rw-r--r--tdata/fimg-gray.pngbin0 -> 201 bytes
-rw-r--r--tdata/fimg-rainbow-transparent.imgbufbin0 -> 1200 bytes
-rw-r--r--tdata/fimg-rainbow-transparent.pambin0 -> 1267 bytes
-rw-r--r--tdata/fimg-rainbow-transparent.pngbin0 -> 165 bytes
-rw-r--r--tdata/fimg-rainbow.imgbufbin0 -> 900 bytes
-rw-r--r--tdata/fimg-rainbow.pambin0 -> 961 bytes
-rw-r--r--tdata/fimg-rainbow.pngbin0 -> 152 bytes
-rw-r--r--tdata/fimg-rgb.pambin0 -> 961 bytes
-rw-r--r--tdata/fimg-transparent.imgbufbin0 -> 600 bytes
-rw-r--r--tdata/fimg-transparent.pambin0 -> 673 bytes
-rw-r--r--tdata/fimg-transparent.pngbin0 -> 119 bytes
-rw-r--r--tdata/fimg.imgbufbin0 -> 300 bytes
-rw-r--r--tdata/fimg.pambin0 -> 371 bytes
-rw-r--r--tdata/fimg.pngbin0 -> 107 bytes
-rw-r--r--tdata/fimgA.pbm16
-rw-r--r--tdata/fimgA.pgm16
-rw-r--r--tdata/fimgA.ppm16
-rw-r--r--tdata/fimgR.pbm2
-rw-r--r--tdata/fimgR.pgmbin0 -> 313 bytes
-rw-r--r--tdata/fimgR.ppmbin0 -> 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 }
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..2f002a4
--- /dev/null
+++ b/LICENSE
@@ -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
new file mode 100644
index 0000000..2aede56
--- /dev/null
+++ b/tdata/fimg-gray.imgbuf
Binary files differ
diff --git a/tdata/fimg-gray.pam b/tdata/fimg-gray.pam
new file mode 100644
index 0000000..c32beb8
--- /dev/null
+++ b/tdata/fimg-gray.pam
Binary files differ
diff --git a/tdata/fimg-gray.png b/tdata/fimg-gray.png
new file mode 100644
index 0000000..59e360f
--- /dev/null
+++ b/tdata/fimg-gray.png
Binary files differ
diff --git a/tdata/fimg-rainbow-transparent.imgbuf b/tdata/fimg-rainbow-transparent.imgbuf
new file mode 100644
index 0000000..9a1c7a3
--- /dev/null
+++ b/tdata/fimg-rainbow-transparent.imgbuf
Binary files differ
diff --git a/tdata/fimg-rainbow-transparent.pam b/tdata/fimg-rainbow-transparent.pam
new file mode 100644
index 0000000..3dfacd6
--- /dev/null
+++ b/tdata/fimg-rainbow-transparent.pam
Binary files differ
diff --git a/tdata/fimg-rainbow-transparent.png b/tdata/fimg-rainbow-transparent.png
new file mode 100644
index 0000000..5df2eaf
--- /dev/null
+++ b/tdata/fimg-rainbow-transparent.png
Binary files differ
diff --git a/tdata/fimg-rainbow.imgbuf b/tdata/fimg-rainbow.imgbuf
new file mode 100644
index 0000000..1a71b3f
--- /dev/null
+++ b/tdata/fimg-rainbow.imgbuf
Binary files differ
diff --git a/tdata/fimg-rainbow.pam b/tdata/fimg-rainbow.pam
new file mode 100644
index 0000000..c6fdcd0
--- /dev/null
+++ b/tdata/fimg-rainbow.pam
Binary files differ
diff --git a/tdata/fimg-rainbow.png b/tdata/fimg-rainbow.png
new file mode 100644
index 0000000..c27fa30
--- /dev/null
+++ b/tdata/fimg-rainbow.png
Binary files differ
diff --git a/tdata/fimg-rgb.pam b/tdata/fimg-rgb.pam
new file mode 100644
index 0000000..c6fdcd0
--- /dev/null
+++ b/tdata/fimg-rgb.pam
Binary files differ
diff --git a/tdata/fimg-transparent.imgbuf b/tdata/fimg-transparent.imgbuf
new file mode 100644
index 0000000..9d730fe
--- /dev/null
+++ b/tdata/fimg-transparent.imgbuf
Binary files differ
diff --git a/tdata/fimg-transparent.pam b/tdata/fimg-transparent.pam
new file mode 100644
index 0000000..837c323
--- /dev/null
+++ b/tdata/fimg-transparent.pam
Binary files differ
diff --git a/tdata/fimg-transparent.png b/tdata/fimg-transparent.png
new file mode 100644
index 0000000..8c5f5a1
--- /dev/null
+++ b/tdata/fimg-transparent.png
Binary files differ
diff --git a/tdata/fimg.imgbuf b/tdata/fimg.imgbuf
new file mode 100644
index 0000000..668a3e5
--- /dev/null
+++ b/tdata/fimg.imgbuf
Binary files differ
diff --git a/tdata/fimg.pam b/tdata/fimg.pam
new file mode 100644
index 0000000..03b0a4f
--- /dev/null
+++ b/tdata/fimg.pam
Binary files differ
diff --git a/tdata/fimg.png b/tdata/fimg.png
new file mode 100644
index 0000000..63d5670
--- /dev/null
+++ b/tdata/fimg.png
Binary files differ
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
new file mode 100644
index 0000000..02ca1c1
--- /dev/null
+++ b/tdata/fimgR.pgm
Binary files differ
diff --git a/tdata/fimgR.ppm b/tdata/fimgR.ppm
new file mode 100644
index 0000000..2bf5d88
--- /dev/null
+++ b/tdata/fimgR.ppm
Binary files differ