1use alloc::{string::String, vec::Vec};
14use core::{
15 fmt::{self, Debug, Display, Formatter},
16 num::{IntErrorKind, ParseIntError},
17 str::FromStr,
18};
19#[cfg(feature = "std")]
20use std::{
21 fs::File,
22 io::{self, BufReader, BufWriter, Read, Write},
23 path::Path,
24};
25
26use Error::*;
27use Format::*;
28
29use crate::math::color::{rgb, Color3};
30use crate::util::buf::Buf2;
31
32#[cfg(feature = "std")]
33use crate::util::buf::AsSlice2;
34
35#[derive(Copy, Clone, Debug, Eq, PartialEq)]
37struct Header {
38 format: Format,
39 width: u32,
40 height: u32,
41 #[allow(unused)]
42 max: u16,
44}
45
46#[derive(Copy, Clone, Debug, Eq, PartialEq)]
48#[allow(unused)]
49#[repr(u16)]
50enum Format {
51 TextBitmap = magic(b"P1"),
53 TextGraymap = magic(b"P2"),
55 TextPixmap = magic(b"P3"),
57 BinaryBitmap = magic(b"P4"),
59 BinaryGraymap = magic(b"P5"),
61 BinaryPixmap = magic(b"P6"),
63}
64
65const fn magic(bytes: &[u8; 2]) -> u16 {
66 u16::from_be_bytes(*bytes)
67}
68
69impl Display for Format {
70 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
71 write!(f, "P{}", *self as u8 as char)
72 }
73}
74
75impl TryFrom<[u8; 2]> for Format {
76 type Error = Error;
77 fn try_from(magic: [u8; 2]) -> Result<Self> {
78 Ok(match &magic {
79 b"P3" => TextPixmap,
80 b"P4" => BinaryBitmap,
81 b"P5" => BinaryGraymap,
82 b"P6" => BinaryPixmap,
83 other => Err(Unsupported(*other))?,
84 })
85 }
86}
87
88#[derive(Debug, Eq, PartialEq)]
90pub enum Error {
91 #[cfg(feature = "std")]
93 Io(io::ErrorKind),
94 Unsupported([u8; 2]),
96 UnexpectedEnd,
98 InvalidNumber,
100}
101
102pub type Result<T> = core::result::Result<T, Error>;
104
105#[cfg(feature = "std")]
107impl std::error::Error for Error {}
108
109impl Display for Error {
110 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
111 write!(f, "error decoding pnm image: {self:?}")
112 }
113}
114
115impl From<ParseIntError> for Error {
116 fn from(e: ParseIntError) -> Self {
117 if *e.kind() == IntErrorKind::Empty {
118 UnexpectedEnd
119 } else {
120 InvalidNumber
121 }
122 }
123}
124
125#[cfg(feature = "std")]
126impl From<io::Error> for Error {
127 fn from(e: io::Error) -> Self {
128 Io(e.kind())
129 }
130}
131
132impl Header {
133 fn parse(src: impl IntoIterator<Item = u8>) -> Result<Self> {
137 let mut it = src.into_iter();
138 let magic = [
139 it.next().ok_or(UnexpectedEnd)?,
140 it.next().ok_or(UnexpectedEnd)?,
141 ];
142 let format = magic.try_into()?;
143 let width: u32 = parse_num(&mut it)?;
144 let height: u32 = parse_num(&mut it)?;
145 let max: u16 = match &format {
146 TextBitmap | BinaryBitmap => 1,
147 _ => parse_num(&mut it)?,
148 };
149 Ok(Self { format, width, height, max })
150 }
151 #[cfg(feature = "std")]
154 fn write(&self, mut dest: impl Write) -> io::Result<()> {
155 let Self { format, width, height, max } = *self;
156 let max: &dyn Display = match format {
157 TextBitmap | BinaryBitmap => &"",
158 _ => &max,
159 };
160 writeln!(dest, "{} {} {} {}", format, width, height, max)
161 }
162}
163
164#[cfg(feature = "std")]
171pub fn load_pnm(path: impl AsRef<Path>) -> Result<Buf2<Color3>> {
172 let r = &mut BufReader::new(File::open(path)?);
173 read_pnm(r.bytes().map_while(io::Result::ok))
174}
175
176pub fn read_pnm(src: impl IntoIterator<Item = u8>) -> Result<Buf2<Color3>> {
183 let mut it = src.into_iter();
184 let h = Header::parse(&mut it)?;
185
186 let count = h.width * h.height;
187 let data: Vec<Color3> = match h.format {
188 BinaryPixmap => {
189 let mut col = [0u8; 3];
190 it.zip((0..3).cycle())
191 .flat_map(|(c, i)| {
192 col[i] = c;
193 (i == 2).then(|| col.into())
194 })
195 .take(count as usize)
196 .collect()
197 }
198 BinaryGraymap => it .map(|c| rgb(c, c, c))
200 .collect(),
201 BinaryBitmap => it
202 .flat_map(|byte| (0..8).rev().map(move |i| (byte >> i) & 1))
203 .map(|bit| {
204 let ch = (1 - bit) * 0xFF;
206 rgb(ch, ch, ch)
207 })
208 .collect(),
209 TextPixmap => {
210 let mut col = [0u8; 3];
211 (0..3)
212 .cycle()
213 .flat_map(|i| {
214 col[i] = match parse_num(&mut it) {
215 Ok(c) => c,
216 Err(e) => return Some(Err(e)),
217 };
218 (i == 2).then(|| Ok(col.into()))
219 })
220 .take(count as usize)
221 .collect::<Result<Vec<_>>>()?
222 }
223 _ => unimplemented!(),
224 };
225
226 if data.len() < (h.width * h.height) as usize {
227 Err(UnexpectedEnd)
228 } else {
229 Ok(Buf2::new_from((h.width, h.height), data))
230 }
231}
232
233#[cfg(feature = "std")]
242pub fn save_ppm(
243 path: impl AsRef<Path>,
244 data: impl AsSlice2<Color3>,
245) -> io::Result<()> {
246 let out = BufWriter::new(File::create(path)?);
247 write_ppm(out, data)
248}
249
250#[cfg(feature = "std")]
256pub fn write_ppm(
257 mut out: impl Write,
258 data: impl AsSlice2<Color3>,
259) -> io::Result<()> {
260 let slice = data.as_slice2();
261 Header {
262 format: Format::BinaryPixmap,
263 width: slice.width(),
264 height: slice.height(),
265 max: 255,
266 }
267 .write(&mut out)?;
268
269 let res = slice
270 .rows()
271 .flatten()
272 .map(|c| c.0)
273 .try_for_each(|rgb| out.write_all(&rgb[..]));
274
275 res
276}
277
278fn parse_num<T>(src: impl IntoIterator<Item = u8>) -> Result<T>
280where
281 T: FromStr,
282 Error: From<T::Err>,
283{
284 let mut in_comment = false;
286 let mut whitespace_or_comment = |b| match b {
287 b'#' => {
288 in_comment = true;
289 true
290 }
291 b'\n' => {
292 in_comment = false;
293 true
294 }
295 _ => in_comment || b.is_ascii_whitespace(),
296 };
297
298 let str = src
299 .into_iter()
300 .skip_while(|&b| whitespace_or_comment(b))
301 .take_while(|&b| !b.is_ascii_whitespace())
302 .map(char::from)
303 .collect::<String>();
304
305 Ok(str.parse()?)
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn parse_value_int() {
314 assert_eq!(parse_num(*b"123"), Ok(123));
315 assert_eq!(parse_num(*b"12345"), Ok(12345));
316 }
317
318 #[test]
319 fn parse_num_empty() {
320 assert_eq!(parse_num::<i32>(*b""), Err(UnexpectedEnd));
321 }
322
323 #[test]
324 fn parse_num_with_whitespace() {
325 assert_eq!(parse_num(*b" \n\n 42 "), Ok(42));
326 }
327
328 #[test]
329 fn parse_num_with_comment() {
330 assert_eq!(parse_num(*b"# this is a comment\n42"), Ok(42));
331 }
332
333 #[test]
334 fn parse_header_whitespace() {
335 assert_eq!(
336 Header::parse(*b"P6 123\t \n\r321 255 "),
337 Ok(Header {
338 format: BinaryPixmap,
339 width: 123,
340 height: 321,
341 max: 255,
342 })
343 );
344 }
345
346 #[test]
347 fn parse_header_comment() {
348 assert_eq!(
349 Header::parse(*b"P6 # foo 42\n 123\n#bar\n#baz\n321 255 "),
350 Ok(Header {
351 format: BinaryPixmap,
352 width: 123,
353 height: 321,
354 max: 255,
355 })
356 );
357 }
358
359 #[test]
360 fn parse_header_p4() {
361 assert_eq!(
362 Header::parse(*b"P4 123 456 "),
363 Ok(Header {
364 format: BinaryBitmap,
365 width: 123,
366 height: 456,
367 max: 1,
368 })
369 );
370 }
371
372 #[test]
373 fn parse_header_p5() {
374 assert_eq!(
375 Header::parse(*b"P5 123 456 64 "),
376 Ok(Header {
377 format: BinaryGraymap,
378 width: 123,
379 height: 456,
380 max: 64,
381 })
382 );
383 }
384
385 #[test]
386 fn parse_header_unsupported_magic() {
387 let res = Header::parse(*b"P2 1 1 1 ");
388 assert_eq!(res, Err(Unsupported(*b"P2")));
389 }
390
391 #[test]
392 fn parse_header_invalid_magic() {
393 let res = Header::parse(*b"FOO");
394 assert_eq!(res, Err(Unsupported(*b"FO")));
395 }
396
397 #[test]
398 fn parse_header_invalid_dims() {
399 assert_eq!(Header::parse(*b"P5 abc 1 1 "), Err(InvalidNumber));
400 assert_eq!(Header::parse(*b"P5 1 1 "), Err(UnexpectedEnd));
401 assert_eq!(Header::parse(*b"P6 1 -1 1 "), Err(InvalidNumber));
402 }
403
404 #[test]
405 fn parse_pnm_truncated() {
406 let data = *b"P3 2 2 256 \n 0 0 0 123 0 42 0 64 128";
407 assert_eq!(read_pnm(data).err(), Some(UnexpectedEnd));
408 }
409
410 #[cfg(feature = "std")]
411 #[test]
412 fn write_header_p1() {
413 let mut out = Vec::new();
414 let hdr = Header {
415 format: Format::TextBitmap,
416 width: 16,
417 height: 32,
418 max: 1,
419 };
420 hdr.write(&mut out).unwrap();
421 assert_eq!(&out, b"P1 16 32 \n");
422 }
423
424 #[cfg(feature = "std")]
425 #[test]
426 fn write_header_p6() {
427 let mut out = Vec::new();
428 let hdr = Header {
429 format: Format::BinaryPixmap,
430 width: 64,
431 height: 16,
432 max: 4,
433 };
434 hdr.write(&mut out).unwrap();
435 assert_eq!(&out, b"P6 64 16 4\n");
436 }
437
438 #[test]
439 fn read_pnm_p3() {
440 let data = *b"P3 2 2 256 \n 0 0 0 123 0 42 0 64 128 255 255 255";
441
442 let buf = read_pnm(data).unwrap();
443
444 assert_eq!(buf.width(), 2);
445 assert_eq!(buf.height(), 2);
446
447 assert_eq!(buf[[0, 0]], rgb(0, 0, 0));
448 assert_eq!(buf[[1, 0]], rgb(123, 0, 42));
449 assert_eq!(buf[[0, 1]], rgb(0, 64, 128));
450 assert_eq!(buf[[1, 1]], rgb(255, 255, 255));
451 }
452
453 #[test]
454 fn read_pnm_p4() {
455 let buf = read_pnm(*b"P4 4 2\n\x69").unwrap();
457
458 assert_eq!(buf.width(), 4);
459 assert_eq!(buf.height(), 2);
460
461 let b = rgb(0u8, 0, 0);
462 let w = rgb(0xFFu8, 0xFF, 0xFF);
463
464 assert_eq!(buf[0usize], [w, b, b, w]);
465 assert_eq!(buf[1usize], [b, w, w, b]);
466 }
467
468 #[test]
469 fn read_pnm_p5() {
470 let buf = read_pnm(*b"P5 2 2 255\n\x01\x23\x45\x67").unwrap();
471
472 assert_eq!(buf.width(), 2);
473 assert_eq!(buf.height(), 2);
474
475 assert_eq!(buf[0usize], [rgb(0x01, 0x01, 0x01), rgb(0x23, 0x23, 0x23)]);
476 assert_eq!(buf[1usize], [rgb(0x45, 0x45, 0x45), rgb(0x67, 0x67, 0x67)]);
477 }
478
479 #[test]
480 fn read_pnm_p6() {
481 let buf = read_pnm(
482 *b"P6 2 2 255\n\
483 \x01\x12\x23\
484 \x34\x45\x56\
485 \x67\x78\x89\
486 \x9A\xAB\xBC",
487 )
488 .unwrap();
489
490 assert_eq!(buf.width(), 2);
491 assert_eq!(buf.height(), 2);
492
493 assert_eq!(buf[0usize], [rgb(0x01, 0x12, 0x23), rgb(0x34, 0x45, 0x56)]);
494 assert_eq!(buf[1usize], [rgb(0x67, 0x78, 0x89), rgb(0x9A, 0xAB, 0xBC)]);
495 }
496
497 #[cfg(feature = "std")]
498 #[test]
499 fn write_ppm() {
500 use alloc::vec;
501 let buf = vec![
502 rgb(0xFF, 0, 0),
503 rgb(0, 0xFF, 0),
504 rgb(0, 0, 0xFF),
505 rgb(0xFF, 0xFF, 0),
506 ];
507
508 let mut out = vec![];
509 super::write_ppm(&mut out, Buf2::new_from((2, 2), buf)).unwrap();
510
511 assert_eq!(
512 &out,
513 b"P6 2 2 255\n\
514 \xFF\x00\x00\
515 \x00\xFF\x00\
516 \x00\x00\xFF\
517 \xFF\xFF\x00"
518 );
519 }
520}