1use alloc::{string::String, vec::Vec};
16use core::{
17 fmt::{self, Debug, Display, Formatter},
18 num::{IntErrorKind, ParseIntError},
19 str::FromStr,
20};
21#[cfg(feature = "std")]
22use std::{
23 fs::File,
24 io::{self, BufReader, BufWriter, Read, Write},
25 path::Path,
26};
27
28use crate::math::{Color3, rgb};
29
30use super::{Dims, buf::Buf2};
31#[cfg(feature = "std")]
32use super::{
33 buf::AsSlice2,
34 pixfmt::{IntoPixel, Rgb888},
35};
36
37use Error::*;
38use Format::*;
39
40#[derive(Copy, Clone, Debug, Eq, PartialEq)]
42struct Header {
43 format: Format,
44 dims: Dims,
45 #[allow(unused)]
46 max: u16,
48}
49
50#[derive(Copy, Clone, Debug, Eq, PartialEq)]
52#[allow(unused)]
53#[repr(u16)]
54enum Format {
55 TextBitmap = magic(b"P1"),
57 TextGraymap = magic(b"P2"),
59 TextPixmap = magic(b"P3"),
61 BinaryBitmap = magic(b"P4"),
63 BinaryGraymap = magic(b"P5"),
65 BinaryPixmap = magic(b"P6"),
67}
68
69const fn magic(bytes: &[u8; 2]) -> u16 {
70 u16::from_be_bytes(*bytes)
71}
72
73impl Display for Format {
74 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
75 write!(f, "P{}", *self as u8 as char)
76 }
77}
78
79impl TryFrom<[u8; 2]> for Format {
80 type Error = Error;
81 fn try_from(magic: [u8; 2]) -> Result<Self> {
82 Ok(match &magic {
83 b"P2" => TextGraymap,
84 b"P3" => TextPixmap,
85 b"P4" => BinaryBitmap,
86 b"P5" => BinaryGraymap,
87 b"P6" => BinaryPixmap,
88 other => Err(Unsupported(*other))?,
89 })
90 }
91}
92
93#[derive(Debug, Eq, PartialEq)]
95pub enum Error {
96 #[cfg(feature = "std")]
98 Io(io::ErrorKind),
99 Unsupported([u8; 2]),
101 UnexpectedEnd,
103 InvalidNumber,
105}
106
107pub type Result<T> = core::result::Result<T, Error>;
109
110impl core::error::Error for Error {}
111
112impl Display for Error {
113 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
114 match *self {
115 #[cfg(feature = "std")]
116 Io(kind) => write!(f, "i/o error {kind}"),
117 Unsupported([c, d]) => {
118 write!(f, "unsupported magic number {}{}", c as char, d as char)
119 }
120 UnexpectedEnd => write!(f, "unexpected end of input"),
121 InvalidNumber => write!(f, "invalid numeric value"),
122 }
123 }
124}
125
126impl From<ParseIntError> for Error {
127 fn from(e: ParseIntError) -> Self {
128 if *e.kind() == IntErrorKind::Empty {
129 UnexpectedEnd
130 } else {
131 InvalidNumber
132 }
133 }
134}
135
136#[cfg(feature = "std")]
137impl From<io::Error> for Error {
138 fn from(e: io::Error) -> Self {
139 Io(e.kind())
140 }
141}
142
143impl Header {
144 fn parse(input: impl IntoIterator<Item = u8>) -> Result<Self> {
148 let mut it = input.into_iter();
149 let magic = [
150 it.next().ok_or(UnexpectedEnd)?,
151 it.next().ok_or(UnexpectedEnd)?,
152 ];
153 let format = magic.try_into()?;
154 let dims = (parse_num(&mut it)?, parse_num(&mut it)?);
155 let max: u16 = match &format {
156 TextBitmap | BinaryBitmap => 1,
157 _ => parse_num(&mut it)?,
158 };
159 Ok(Self { format, dims, max })
160 }
161 #[cfg(feature = "std")]
164 fn write(&self, mut dest: impl Write) -> io::Result<()> {
165 let Self { format, dims: (w, h), max } = *self;
166 let max: &dyn Display = match format {
167 TextBitmap | BinaryBitmap => &"",
168 _ => &max,
169 };
170 writeln!(dest, "{format} {w} {h} {max}")
171 }
172}
173
174#[cfg(feature = "std")]
182pub fn load_pnm(path: impl AsRef<Path>) -> Result<Buf2<Color3>> {
183 let r = &mut BufReader::new(File::open(path)?);
184 read_pnm(r)
185}
186
187#[cfg(feature = "std")]
195pub fn read_pnm(input: impl Read) -> Result<Buf2<Color3>> {
196 let input = BufReader::new(input);
197 parse_pnm(input.bytes().map_while(io::Result::ok))
198}
199
200pub fn parse_pnm(input: impl IntoIterator<Item = u8>) -> Result<Buf2<Color3>> {
208 let mut it = input.into_iter();
209 let h = Header::parse(&mut it)?;
210
211 let count = h.dims.0 * h.dims.1;
212 let data: Vec<Color3> = match h.format {
213 BinaryPixmap => {
214 let mut col = [0u8; 3];
215 it.zip((0..3).cycle())
216 .flat_map(|(c, i)| {
217 col[i] = c;
218 (i == 2).then(|| col.into())
219 })
220 .take(count as usize)
221 .collect()
222 }
223 BinaryGraymap => it .map(|c| rgb(c, c, c))
225 .collect(),
226 BinaryBitmap => it
227 .flat_map(|byte| (0..8).rev().map(move |i| (byte >> i) & 1))
228 .map(|bit| {
229 let ch = (1 - bit) * 0xFF;
231 rgb(ch, ch, ch)
232 })
233 .collect(),
234 TextPixmap => {
235 let mut col = [0u8; 3];
236 (0..3)
237 .cycle()
238 .flat_map(|i| {
239 col[i] = match parse_num(&mut it) {
240 Ok(c) => c,
241 Err(e) => return Some(Err(e)),
242 };
243 (i == 2).then(|| Ok(col.into()))
244 })
245 .take(count as usize)
246 .collect::<Result<Vec<_>>>()?
247 }
248 TextGraymap => (0..count)
249 .map(|_| {
250 let val = parse_num(&mut it)?;
251 Ok(rgb(val, val, val))
252 })
253 .collect::<Result<Vec<_>>>()?,
254 _ => return Err(Unsupported((h.format as u16).to_be_bytes())),
255 };
256
257 if data.len() < count as usize {
258 Err(UnexpectedEnd)
259 } else {
260 Ok(Buf2::new_from(h.dims, data))
261 }
262}
263
264#[cfg(feature = "std")]
273pub fn save_ppm<T>(
274 path: impl AsRef<Path>,
275 data: impl AsSlice2<T>,
276) -> io::Result<()>
277where
278 T: IntoPixel<[u8; 3], Rgb888> + Copy,
279{
280 let out = BufWriter::new(File::create(path)?);
281 write_ppm(out, data)
282}
283
284#[cfg(feature = "std")]
290pub fn write_ppm<T>(
291 mut out: impl Write,
292 data: impl AsSlice2<T>,
293) -> io::Result<()>
294where
295 T: IntoPixel<[u8; 3], Rgb888> + Copy,
296{
297 let slice = data.as_slice2();
298 Header {
299 format: BinaryPixmap,
300 dims: slice.dims(),
301 max: 255,
302 }
303 .write(&mut out)?;
304
305 slice
307 .rows()
308 .flatten()
309 .map(|c| c.into_pixel())
310 .try_for_each(|rgb| out.write_all(&rgb[..]))
311}
312
313fn parse_num<T>(src: impl IntoIterator<Item = u8>) -> Result<T>
315where
316 T: FromStr,
317 Error: From<T::Err>,
318{
319 let mut whitespace_or_comment = {
320 let mut in_comment = false;
321 move |b: &u8| match *b {
322 b'#' => {
323 in_comment = true;
324 true
325 }
326 b'\n' => {
327 in_comment = false;
328 true
329 }
330 _ => in_comment || b.is_ascii_whitespace(),
331 }
332 };
333 let str = src
334 .into_iter()
335 .skip_while(whitespace_or_comment)
336 .take_while(|b| !whitespace_or_comment(b))
337 .map(char::from)
338 .collect::<String>();
339 Ok(str.parse()?)
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 #[test]
347 fn parse_value_int() {
348 assert_eq!(parse_num(*b"123"), Ok(123));
349 assert_eq!(parse_num(*b"12345"), Ok(12345));
350 }
351
352 #[test]
353 fn parse_num_empty() {
354 assert_eq!(parse_num::<i32>(*b""), Err(UnexpectedEnd));
355 }
356
357 #[test]
358 fn parse_num_with_whitespace() {
359 assert_eq!(parse_num(*b" \n\n 42 "), Ok(42));
360 }
361
362 #[test]
363 fn parse_num_with_comment_before() {
364 assert_eq!(parse_num(*b"# this is a comment\n42"), Ok(42));
365 }
366
367 #[test]
368 fn parse_num_with_comment_after() {
369 assert_eq!(parse_num(*b"42#this is a comment"), Ok(42));
370 }
371
372 #[test]
373 fn parse_header_whitespace() {
374 assert_eq!(
375 Header::parse(*b"P6 123\t \n\r321 255 "),
376 Ok(Header {
377 format: BinaryPixmap,
378 dims: (123, 321),
379 max: 255,
380 })
381 );
382 }
383
384 #[test]
385 fn parse_header_comment() {
386 assert_eq!(
387 Header::parse(*b"P6 # foo 42\n 123\n#bar\n#baz\n321 255 "),
388 Ok(Header {
389 format: BinaryPixmap,
390 dims: (123, 321),
391 max: 255,
392 })
393 );
394 }
395
396 #[test]
397 fn parse_header_p2() {
398 assert_eq!(
399 Header::parse(*b"P2 123 456 789"),
400 Ok(Header {
401 format: TextGraymap,
402 dims: (123, 456),
403 max: 789,
404 })
405 );
406 }
407
408 #[test]
409 fn parse_header_p3() {
410 assert_eq!(
411 Header::parse(*b"P3 123 456 789"),
412 Ok(Header {
413 format: TextPixmap,
414 dims: (123, 456),
415 max: 789,
416 })
417 );
418 }
419
420 #[test]
421 fn parse_header_p4() {
422 assert_eq!(
423 Header::parse(*b"P4 123 456 "),
424 Ok(Header {
425 format: BinaryBitmap,
426 dims: (123, 456),
427 max: 1,
428 })
429 );
430 }
431
432 #[test]
433 fn parse_header_p5() {
434 assert_eq!(
435 Header::parse(*b"P5 123 456 789 "),
436 Ok(Header {
437 format: BinaryGraymap,
438 dims: (123, 456),
439 max: 789,
440 })
441 );
442 }
443
444 #[test]
445 fn parse_header_p6() {
446 assert_eq!(
447 Header::parse(*b"P6 123 456 789 "),
448 Ok(Header {
449 format: BinaryPixmap,
450 dims: (123, 456),
451 max: 789,
452 })
453 );
454 }
455
456 #[test]
457 fn parse_header_unsupported_magic() {
458 let res = Header::parse(*b"P7 1 1 1 ");
459 assert_eq!(res, Err(Unsupported(*b"P7")));
460 }
461
462 #[test]
463 fn parse_header_invalid_magic() {
464 let res = Header::parse(*b"FOO");
465 assert_eq!(res, Err(Unsupported(*b"FO")));
466 }
467
468 #[test]
469 fn parse_header_invalid_dims() {
470 assert_eq!(Header::parse(*b"P5 abc 1 1 "), Err(InvalidNumber));
471 assert_eq!(Header::parse(*b"P5 1 1 "), Err(UnexpectedEnd));
472 assert_eq!(Header::parse(*b"P6 1 -1 1 "), Err(InvalidNumber));
473 }
474
475 #[test]
476 fn parse_pnm_truncated() {
477 let data = *b"P3 2 2 256 \n 0 0 0 123 0 42 0 64 128";
478 assert_eq!(parse_pnm(data).err(), Some(UnexpectedEnd));
479 }
480
481 #[cfg(feature = "std")]
482 #[test]
483 fn write_header_p1() {
484 let mut out = Vec::new();
485 let hdr = Header {
486 format: TextBitmap,
487 dims: (123, 456),
488 max: 1,
489 };
490 hdr.write(&mut out).unwrap();
491 assert_eq!(&out, b"P1 123 456 \n");
492 }
493
494 #[cfg(feature = "std")]
495 #[test]
496 fn write_header_p6() {
497 let mut out = Vec::new();
498 let hdr = Header {
499 format: BinaryPixmap,
500 dims: (123, 456),
501 max: 789,
502 };
503 hdr.write(&mut out).unwrap();
504 assert_eq!(&out, b"P6 123 456 789\n");
505 }
506
507 #[test]
508 fn read_pnm_p2() {
509 let data = *b"P2 2 2 128 \n 12 34 56 78";
510
511 let buf = parse_pnm(data).unwrap();
512
513 assert_eq!(buf.width(), 2);
514 assert_eq!(buf.height(), 2);
515
516 assert_eq!(buf[[0, 0]], rgb(12, 12, 12));
517 assert_eq!(buf[[1, 0]], rgb(34, 34, 34));
518 assert_eq!(buf[[0, 1]], rgb(56, 56, 56));
519 assert_eq!(buf[[1, 1]], rgb(78, 78, 78));
520 }
521
522 #[test]
523 fn read_pnm_p3() {
524 let data = *b"P3 2 2 256 \n 0 0 0 123 0 42 0 64 128 255 255 255";
525
526 let buf = parse_pnm(data).unwrap();
527
528 assert_eq!(buf.dims(), (2, 2));
529
530 assert_eq!(buf[[0, 0]], rgb(0, 0, 0));
531 assert_eq!(buf[[1, 0]], rgb(123, 0, 42));
532 assert_eq!(buf[[0, 1]], rgb(0, 64, 128));
533 assert_eq!(buf[[1, 1]], rgb(255, 255, 255));
534 }
535
536 #[test]
537 fn read_pnm_p4() {
538 let buf = parse_pnm(*b"P4 4 2\n\x69").unwrap();
540
541 assert_eq!(buf.dims(), (4, 2));
542
543 let b = rgb(0u8, 0, 0);
544 let w = rgb(0xFFu8, 0xFF, 0xFF);
545
546 assert_eq!(buf[0usize], [w, b, b, w]);
547 assert_eq!(buf[1usize], [b, w, w, b]);
548 }
549
550 #[test]
551 fn read_pnm_p5() {
552 let buf = parse_pnm(*b"P5 2 2 255\n\x01\x23\x45\x67").unwrap();
553
554 assert_eq!(buf.dims(), (2, 2));
555
556 assert_eq!(buf[0usize], [rgb(0x01, 0x01, 0x01), rgb(0x23, 0x23, 0x23)]);
557 assert_eq!(buf[1usize], [rgb(0x45, 0x45, 0x45), rgb(0x67, 0x67, 0x67)]);
558 }
559
560 #[test]
561 fn read_pnm_p6() {
562 let buf = parse_pnm(
563 *b"P6 2 2 255\n\
564 \x01\x12\x23\
565 \x34\x45\x56\
566 \x67\x78\x89\
567 \x9A\xAB\xBC",
568 )
569 .unwrap();
570
571 assert_eq!(buf.dims(), (2, 2));
572
573 assert_eq!(buf[0usize], [rgb(0x01, 0x12, 0x23), rgb(0x34, 0x45, 0x56)]);
574 assert_eq!(buf[1usize], [rgb(0x67, 0x78, 0x89), rgb(0x9A, 0xAB, 0xBC)]);
575 }
576
577 #[cfg(feature = "std")]
578 #[test]
579 fn write_ppm() {
580 use alloc::vec;
581 let buf = vec![
582 rgb(0xFF, 0, 0),
583 rgb(0, 0xFF, 0),
584 rgb(0, 0, 0xFF),
585 rgb(0xFF, 0xFF, 0),
586 ];
587
588 let mut out = vec![];
589 super::write_ppm(&mut out, Buf2::new_from((2, 2), buf)).unwrap();
590
591 assert_eq!(
592 &out,
593 b"P6 2 2 255\n\
594 \xFF\x00\x00\
595 \x00\xFF\x00\
596 \x00\x00\xFF\
597 \xFF\xFF\x00"
598 );
599 }
600}