Skip to content

Commit

Permalink
Merge pull request #29 from Roughsketch/pnm
Browse files Browse the repository at this point in the history
Add support for PNM
  • Loading branch information
Roughsketch authored May 11, 2023
2 parents edc4824 + d6d1e8e commit 1751e64
Show file tree
Hide file tree
Showing 13 changed files with 4,942 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ imagesize = "0.12"
* JPEG XL
* KTX2
* PNG
* PNM (PBM, PGM, PPM)
* PSD / PSB
* QOI
* TGA
Expand Down
5 changes: 5 additions & 0 deletions src/formats/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod jpeg;
pub mod jxl;
pub mod ktx2;
pub mod png;
pub mod pnm;
pub mod psd;
pub mod qoi;
pub mod tga;
Expand Down Expand Up @@ -107,6 +108,10 @@ pub fn image_type<R: BufRead + Seek>(reader: &mut R) -> ImageResult<ImageType> {
return Ok(ImageType::Farbfeld);
}

if pnm::matches(&header) {
return Ok(ImageType::Pnm);
}

if vtf::matches(&header) {
return Ok(ImageType::Vtf);
}
Expand Down
64 changes: 64 additions & 0 deletions src/formats/pnm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use crate::util::*;
use crate::{ImageResult, ImageSize};

use std::io::{self, BufRead, Seek, SeekFrom};

pub fn size<R: BufRead + Seek>(reader: &mut R) -> ImageResult<ImageSize> {
reader.seek(SeekFrom::Start(2))?;

// We try to loop until we find a line that does not start with a comment
// or is empty. After that, we should expect width and height back to back
// separated by an arbitrary amount of whitespace.
loop {
// Lines can be arbitrarily long, but 1k is a good enough cap I think.
// Anything higher and I blame whoever made the file.
let line = read_until_whitespace(reader, 1024)?;
let trimmed_line = line.trim();

// If it's a comment, skip until newline
if trimmed_line.starts_with('#') {
read_until_capped(reader, b'\n', 1024);
continue
}

// If it's just empty skip
if trimmed_line.is_empty() {
continue;
}

// The first thing we read that isn't empty or a comment should be the width
let raw_width = line;

// Read in the next non-whitespace section as the height
let line = read_until_whitespace(reader, 1024)?;
let raw_height = line.trim();

// Try to parse the width and height
let width_parsed = raw_width.parse::<usize>().ok();
let height_parsed = raw_height.parse::<usize>().ok();

// If successful return it
if let (Some(width), Some(height)) = (width_parsed, height_parsed) {
return Ok(ImageSize { width, height });
}

// If no successful then assume that it cannot be read
// If this happens we need to gather test files for those cases
break;
}

Err(io::Error::new(io::ErrorKind::InvalidData, "PNM dimensions not found").into())
}

pub fn matches(header: &[u8]) -> bool {
if header[0] != b'P' {
return false;
}

// We only support P1 to P6. Currently ignoring P7, PF, PFM
if header[1] < b'1' && header[1] > b'6' {
return false;
}

true
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ pub enum ImageType {
Ktx2,
/// Standard PNG
Png,
/// Portable Any Map
Pnm,
/// Photoshop Document
Psd,
/// Quite OK Image Format
Expand Down Expand Up @@ -259,6 +261,7 @@ fn dispatch_header<R: BufRead + Seek>(reader: &mut R) -> ImageResult<ImageSize>
ImageType::Jxl => jxl::size(reader),
ImageType::Ktx2 => ktx2::size(reader),
ImageType::Png => png::size(reader),
ImageType::Pnm => pnm::size(reader),
ImageType::Psd => psd::size(reader),
ImageType::Qoi => qoi::size(reader),
ImageType::Tga => tga::size(reader),
Expand Down
35 changes: 35 additions & 0 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,41 @@ pub fn read_until_capped<R: BufRead>(reader: &mut R, delimiter: u8, max_size: us
Ok(bytes)
}

/// Skips all starting whitespace characters and then reads a string until the next whitespace character
/// Example:
/// " test string" => "test"
pub fn read_until_whitespace<R: BufRead>(reader: &mut R, max_size: usize) -> io::Result<String> {
let mut bytes = Vec::new();
let mut amount_read = 0;
let mut seen_non_whitespace = false;

while amount_read < max_size {
amount_read += 1;

let mut byte = [0; 1];
reader.read_exact(&mut byte)?;

if byte[0].is_ascii_whitespace() {
// If we've seen a non-whitespace character before then exit
if seen_non_whitespace {
break;
}

// Skip whitespace until we found first non-whitespace character
continue;
}

bytes.push(byte[0]);
seen_non_whitespace = true;
}

if amount_read >= max_size {
return Err(io::Error::new(io::ErrorKind::InvalidData, format!("Delimiter not found within {} bytes", max_size)));
}

String::from_utf8(bytes).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}

pub fn read_line_capped<R: BufRead>(reader: &mut R, max_size: usize) -> io::Result<String> {
let bytes = read_until_capped(reader, b'\n', max_size)?;
String::from_utf8(bytes).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
Expand Down
10 changes: 10 additions & 0 deletions tests/images/pnm/P1/feep.ascii.pbm
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
P1
# feep.pbm
24 7
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 1 1 1 1 0 0 1 1 1 1 0 0 1 1 1 1 0 0 1 1 1 1 0
0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 1 0
0 1 1 1 0 0 0 1 1 1 0 0 0 1 1 1 0 0 0 1 1 1 1 0
0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0
0 1 0 0 0 0 0 1 1 1 1 0 0 1 1 1 1 0 0 1 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Loading

0 comments on commit 1751e64

Please sign in to comment.