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}