pb_imgsize/
lib.rs

1//! Fast reader for JPEG and PNG comments and dimensions.
2//!
3//! The `pb-imgsize` crate provides a reader for JPEG and PNG images that can
4//! quickly extract the image's dimensions and any comments embedded in the
5//! image.
6//!
7//! For PNG images, the dimensions are extracted from the IHDR chunk, and the
8//! comments are extracted from tEXt chunks with the keyword "comment".
9//!
10//! For JPEG images, the dimensions are extracted from the SOFx chunk, and the
11//! comments are extracted from COM chunks.
12//!
13//! The reader is fast because it only reads the chunks that are necessary to
14//! extract the dimensions and comments. It does not decode the image data.
15//!
16//! The reader does not attempt to read EXIF data.
17//!
18//! # Example
19//!
20//! ```
21//! let data = include_bytes!("buttercups.jpg");
22//! let metadata = pb_imgsize::read_bytes(data).unwrap();
23//! assert_eq!(512, metadata.width);
24//! assert_eq!(341, metadata.height);
25//! assert_eq!(vec![b"Buttercups".to_vec()], metadata.comments);
26//! ```
27
28mod jpeg;
29mod png;
30use std::fmt::Display;
31use std::io;
32use std::path::Path;
33
34pub use jpeg::JpegDecodingError;
35pub use png::PngDecodingError;
36
37/// An error that occurred while reading an image.
38#[derive(Debug)]
39pub enum Error {
40    Io(io::Error),
41    Decoding(DecodingError),
42}
43
44impl From<io::Error> for Error {
45    fn from(e: io::Error) -> Self {
46        Error::Io(e)
47    }
48}
49
50impl From<DecodingError> for Error {
51    fn from(e: DecodingError) -> Self {
52        Error::Decoding(e)
53    }
54}
55
56impl Display for Error {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        match &self {
59            Error::Io(e) => write!(f, "IO error: {}", e),
60            Error::Decoding(e) => write!(f, "Decoding error: {}", e),
61        }
62    }
63}
64
65impl std::error::Error for Error {}
66
67/// An error that occurred while decoding an image.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum DecodingError {
70    // #[error("Unknown magic number in image data: 0x{0:08x}")]
71    UnknownMagic(u32),
72
73    // #[error(transparent)]
74    Jpeg(jpeg::JpegDecodingError),
75
76    // #[error(transparent)]
77    Png(png::PngDecodingError),
78
79    // #[error("Image data too short: {0} bytes")]
80    TooShort(usize),
81}
82
83impl From<jpeg::JpegDecodingError> for DecodingError {
84    fn from(e: jpeg::JpegDecodingError) -> Self {
85        DecodingError::Jpeg(e)
86    }
87}
88
89impl From<png::PngDecodingError> for DecodingError {
90    fn from(e: png::PngDecodingError) -> Self {
91        DecodingError::Png(e)
92    }
93}
94
95impl Display for DecodingError {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        match &self {
98            DecodingError::UnknownMagic(magic) => {
99                write!(f, "Unknown magic number: 0x{:08x}", magic)
100            }
101            DecodingError::Jpeg(e) => write!(f, "JPEG decoding error: {}", e),
102            DecodingError::Png(e) => write!(f, "PNG decoding error: {}", e),
103            DecodingError::TooShort(n) => write!(f, "Image data too short: {} bytes", n),
104        }
105    }
106}
107
108/// An image's dimensions, along with any comments found in the data.
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct ImageMetadata {
111    pub width: u32,
112    pub height: u32,
113    pub comments: Vec<Vec<u8>>,
114}
115
116/// Reads the dimensions and comments of an image from a file.
117///
118/// This function reads the dimensions and comments of an image from a file. It
119/// returns an `ImageMetadata` struct containing the width and height of the
120/// image, as well as any comments found in the image.
121///
122/// Note: This function works by reading the entire file into memory.
123///
124/// # Arguments
125///
126/// * `path` - A path to the image file.
127///
128/// # Examples
129///
130/// ```
131/// # fn main() -> Result<(), pb_imgsize::Error> {
132/// let metadata = pb_imgsize::read_file("src/buttercups.jpg")?;
133/// assert_eq!(metadata, pb_imgsize::ImageMetadata {
134///   width: 512,
135///   height: 341,
136///   comments: vec![b"Buttercups".to_vec()],
137/// });
138/// # Ok(())
139/// # }
140pub fn read_file(path: impl AsRef<Path>) -> Result<ImageMetadata, Error> {
141    let buf = std::fs::read(path)?;
142    Ok(read_bytes(&buf)?)
143}
144
145/// Reads the dimensions and comments of an image from a byte slice.
146///
147/// This function reads the dimensions and comments of an image from a byte
148/// slice. It returns an `ImageMetadata` struct containing the width and height
149/// of the image, as well as any comments found in the image.
150///
151/// # Arguments
152///
153/// * `data` - A byte slice containing the image data.
154///
155/// # Examples
156///
157/// ```
158/// # fn main() -> Result<(), pb_imgsize::Error> {
159/// use pb_imgsize::read_bytes;
160///
161/// let data = include_bytes!("buttercups.jpg");
162/// let metadata = read_bytes(data)?;
163/// assert_eq!(metadata, pb_imgsize::ImageMetadata {
164///    width: 512,
165///    height: 341,
166///    comments: vec![b"Buttercups".to_vec()]
167/// });
168/// # Ok(())
169/// # }
170/// ```
171pub fn read_bytes(data: &[u8]) -> Result<ImageMetadata, DecodingError> {
172    if data.len() < 4 {
173        Err(DecodingError::TooShort(0))
174    } else if data.starts_with(b"\xff\xd8") {
175        Ok(jpeg::read_jpeg_data(data)?)
176    } else if data.starts_with(b"\x89PNG") {
177        Ok(png::read_png_data(data)?)
178    } else {
179        Err(DecodingError::UnknownMagic(u32::from_be_bytes([
180            data[0], data[1], data[2], data[3],
181        ])))
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_jpeg_file() {
191        let metadata = read_file(Path::new("src/buttercups.jpg")).unwrap();
192        assert_eq!(512, metadata.width);
193        assert_eq!(341, metadata.height);
194        let comments = metadata
195            .comments
196            .iter()
197            .map(|c| String::from_utf8_lossy(c))
198            .collect::<Vec<_>>();
199        assert_eq!(vec!["Buttercups".to_string()], comments);
200    }
201
202    #[test]
203    fn test_png_file() {
204        let metadata = read_file(Path::new("src/watercolors.png")).unwrap();
205        assert_eq!(400, metadata.width);
206        assert_eq!(224, metadata.height);
207        let comments = metadata
208            .comments
209            .iter()
210            .map(|c| String::from_utf8_lossy(c))
211            .collect::<Vec<_>>();
212        assert_eq!(vec!["Abstract watercolors".to_string()], comments);
213    }
214}