1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
//! Fast reader for JPEG and PNG comments and dimensions.
//!
//! The `pb-imgsize` crate provides a reader for JPEG and PNG images that can
//! quickly extract the image's dimensions and any comments embedded in the
//! image.
//!
//! For PNG images, the dimensions are extracted from the IHDR chunk, and the
//! comments are extracted from tEXt chunks with the keyword "comment".
//!
//! For JPEG images, the dimensions are extracted from the SOFx chunk, and the
//! comments are extracted from COM chunks.
//!
//! The reader is fast because it only reads the chunks that are necessary to
//! extract the dimensions and comments. It does not decode the image data.
//!
//! The reader does not attempt to read EXIF data.
//!
//! # Example
//!
//! ```
//! let data = include_bytes!("buttercups.jpg");
//! let metadata = pb_imgsize::read_bytes(data).unwrap();
//! assert_eq!(512, metadata.width);
//! assert_eq!(341, metadata.height);
//! assert_eq!(vec![b"Buttercups".to_vec()], metadata.comments);
//! ```

mod jpeg;
mod png;
use std::fmt::Display;
use std::io;
use std::path::Path;

pub use jpeg::JpegDecodingError;
pub use png::PngDecodingError;

/// An error that occurred while reading an image.
#[derive(Debug)]
pub enum Error {
    Io(io::Error),
    Decoding(DecodingError),
}

impl From<io::Error> for Error {
    fn from(e: io::Error) -> Self {
        Error::Io(e)
    }
}

impl From<DecodingError> for Error {
    fn from(e: DecodingError) -> Self {
        Error::Decoding(e)
    }
}

impl Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match &self {
            Error::Io(e) => write!(f, "IO error: {}", e),
            Error::Decoding(e) => write!(f, "Decoding error: {}", e),
        }
    }
}

impl std::error::Error for Error {}

/// An error that occurred while decoding an image.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DecodingError {
    // #[error("Unknown magic number in image data: 0x{0:08x}")]
    UnknownMagic(u32),

    // #[error(transparent)]
    Jpeg(jpeg::JpegDecodingError),

    // #[error(transparent)]
    Png(png::PngDecodingError),

    // #[error("Image data too short: {0} bytes")]
    TooShort(usize),
}

impl From<jpeg::JpegDecodingError> for DecodingError {
    fn from(e: jpeg::JpegDecodingError) -> Self {
        DecodingError::Jpeg(e)
    }
}

impl From<png::PngDecodingError> for DecodingError {
    fn from(e: png::PngDecodingError) -> Self {
        DecodingError::Png(e)
    }
}

impl Display for DecodingError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match &self {
            DecodingError::UnknownMagic(magic) => {
                write!(f, "Unknown magic number: 0x{:08x}", magic)
            }
            DecodingError::Jpeg(e) => write!(f, "JPEG decoding error: {}", e),
            DecodingError::Png(e) => write!(f, "PNG decoding error: {}", e),
            DecodingError::TooShort(n) => write!(f, "Image data too short: {} bytes", n),
        }
    }
}

/// An image's dimensions, along with any comments found in the data.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImageMetadata {
    pub width: u32,
    pub height: u32,
    pub comments: Vec<Vec<u8>>,
}

/// Reads the dimensions and comments of an image from a file.
///
/// This function reads the dimensions and comments of an image from a file. It
/// returns an `ImageMetadata` struct containing the width and height of the
/// image, as well as any comments found in the image.
///
/// Note: This function works by reading the entire file into memory.
///
/// # Arguments
///
/// * `path` - A path to the image file.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), pb_imgsize::Error> {
/// let metadata = pb_imgsize::read_file("src/buttercups.jpg")?;
/// assert_eq!(metadata, pb_imgsize::ImageMetadata {
///   width: 512,
///   height: 341,
///   comments: vec![b"Buttercups".to_vec()],
/// });
/// # Ok(())
/// # }
pub fn read_file(path: impl AsRef<Path>) -> Result<ImageMetadata, Error> {
    let buf = std::fs::read(path)?;
    Ok(read_bytes(&buf)?)
}

/// Reads the dimensions and comments of an image from a byte slice.
///
/// This function reads the dimensions and comments of an image from a byte
/// slice. It returns an `ImageMetadata` struct containing the width and height
/// of the image, as well as any comments found in the image.
///
/// # Arguments
///
/// * `data` - A byte slice containing the image data.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), pb_imgsize::Error> {
/// use pb_imgsize::read_bytes;
///
/// let data = include_bytes!("buttercups.jpg");
/// let metadata = read_bytes(data)?;
/// assert_eq!(metadata, pb_imgsize::ImageMetadata {
///    width: 512,
///    height: 341,
///    comments: vec![b"Buttercups".to_vec()]
/// });
/// # Ok(())
/// # }
/// ```
pub fn read_bytes(data: &[u8]) -> Result<ImageMetadata, DecodingError> {
    if data.len() < 4 {
        Err(DecodingError::TooShort(0))
    } else if data.starts_with(b"\xff\xd8") {
        Ok(jpeg::read_jpeg_data(data)?)
    } else if data.starts_with(b"\x89PNG") {
        Ok(png::read_png_data(data)?)
    } else {
        Err(DecodingError::UnknownMagic(u32::from_be_bytes([
            data[0], data[1], data[2], data[3],
        ])))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_jpeg_file() {
        let metadata = read_file(Path::new("src/buttercups.jpg")).unwrap();
        assert_eq!(512, metadata.width);
        assert_eq!(341, metadata.height);
        let comments = metadata
            .comments
            .iter()
            .map(|c| String::from_utf8_lossy(c))
            .collect::<Vec<_>>();
        assert_eq!(vec!["Buttercups".to_string()], comments);
    }

    #[test]
    fn test_png_file() {
        let metadata = read_file(Path::new("src/watercolors.png")).unwrap();
        assert_eq!(400, metadata.width);
        assert_eq!(224, metadata.height);
        let comments = metadata
            .comments
            .iter()
            .map(|c| String::from_utf8_lossy(c))
            .collect::<Vec<_>>();
        assert_eq!(vec!["Abstract watercolors".to_string()], comments);
    }
}