lightweight_pdf_core/image/
mod.rs1mod jpeg;
15mod png;
16
17use crate::style::Common;
18use jpeg::parse_jpeg;
19use png::{parse_png, PNG_SIGNATURE};
20use std::sync::Arc;
21
22const MAX_PIXELS: u64 = 40_000_000;
27
28#[derive(Clone, Copy, PartialEq, Eq, Debug)]
29pub enum ImageFormat {
30 Jpeg,
31 Png,
32}
33
34#[derive(Clone, Copy, PartialEq, Eq, Debug)]
35pub enum ImageError {
36 UnsupportedFormat,
38 Malformed,
40 UnsupportedJpeg,
42 UnsupportedPng,
44 ImageTooLarge,
46}
47
48impl core::fmt::Display for ImageError {
49 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
50 match self {
51 ImageError::UnsupportedFormat => write!(f, "unsupported image format (need JPEG or PNG)"),
52 ImageError::Malformed => write!(f, "malformed image header"),
53 ImageError::UnsupportedJpeg => write!(f, "unsupported JPEG variant (need baseline Gray/RGB)"),
54 ImageError::UnsupportedPng => write!(f, "unsupported PNG variant (need non-interlaced 8-bit RGB/RGBA)"),
55 ImageError::ImageTooLarge => write!(f, "image pixel count exceeds the supported limit"),
56 }
57 }
58}
59
60#[derive(Clone, Debug)]
64pub struct Image {
65 pub bytes: Arc<[u8]>,
66 pub format: ImageFormat,
67 pub width_px: u32,
68 pub height_px: u32,
69 pub components: u8,
71 pub common: Common,
72}
73
74impl Image {
75 pub fn new(bytes: impl Into<Arc<[u8]>>) -> Result<Self, ImageError> {
80 let bytes: Arc<[u8]> = bytes.into();
81 let (format, width_px, height_px, components) = if bytes.starts_with(&[0xFF, 0xD8]) {
82 let (w, h, c) = parse_jpeg(&bytes)?;
83 (ImageFormat::Jpeg, w, h, c)
84 } else if bytes.starts_with(&PNG_SIGNATURE) {
85 let (w, h, c) = parse_png(&bytes)?;
86 (ImageFormat::Png, w, h, c)
87 } else {
88 return Err(ImageError::UnsupportedFormat);
89 };
90 if (width_px as u64) * (height_px as u64) > MAX_PIXELS {
91 return Err(ImageError::ImageTooLarge);
92 }
93 Ok(Image {
94 bytes,
95 format,
96 width_px,
97 height_px,
98 components,
99 common: Common::default(),
100 })
101 }
102
103 pub fn width(mut self, width: f32) -> Self {
104 self.common.width = Some(width);
105 self
106 }
107
108 pub fn height(mut self, height: f32) -> Self {
109 self.common.height = Some(height);
110 self
111 }
112
113 pub fn flex(mut self, factor: f32) -> Self {
114 self.common.flex = Some(factor);
115 self
116 }
117
118 pub fn keep_with_next(mut self) -> Self {
119 self.common.keep_with_next = true;
120 self
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 fn fixture(name: &str) -> Vec<u8> {
129 std::fs::read(format!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../test-fixtures/images/{}"), name)).expect("test fixture present")
130 }
131
132 #[test]
133 fn accepts_rgba_png_with_transparency() {
134 let img = Image::new(fixture("logo_rgba.png")).unwrap();
135 assert_eq!(img.format, ImageFormat::Png);
136 assert_eq!(img.components, 4);
137 assert_eq!((img.width_px, img.height_px), (64, 48));
138 }
139
140 #[test]
141 fn accepts_opaque_rgb_png() {
142 let img = Image::new(fixture("logo_rgb.png")).unwrap();
143 assert_eq!(img.components, 3);
144 assert_eq!((img.width_px, img.height_px), (40, 30));
145 }
146
147 #[test]
148 fn accepts_baseline_rgb_jpeg() {
149 let img = Image::new(fixture("logo_baseline.jpg")).unwrap();
150 assert_eq!(img.format, ImageFormat::Jpeg);
151 assert_eq!(img.components, 3);
152 assert_eq!((img.width_px, img.height_px), (80, 60));
153 }
154
155 #[test]
156 fn accepts_baseline_gray_jpeg() {
157 let img = Image::new(fixture("logo_gray.jpg")).unwrap();
158 assert_eq!(img.components, 1);
159 assert_eq!((img.width_px, img.height_px), (32, 32));
160 }
161
162 #[test]
163 fn rejects_progressive_jpeg() {
164 assert_eq!(Image::new(fixture("progressive.jpg")).unwrap_err(), ImageError::UnsupportedJpeg);
165 }
166
167 #[test]
168 fn rejects_cmyk_jpeg() {
169 assert_eq!(Image::new(fixture("cmyk.jpg")).unwrap_err(), ImageError::UnsupportedJpeg);
170 }
171
172 #[test]
173 fn rejects_palette_png() {
174 assert_eq!(Image::new(fixture("palette.png")).unwrap_err(), ImageError::UnsupportedPng);
175 }
176
177 #[test]
178 fn rejects_sixteen_bit_png() {
179 assert_eq!(Image::new(fixture("sixteen_bit.png")).unwrap_err(), ImageError::UnsupportedPng);
180 }
181
182 #[test]
183 fn rejects_interlaced_png() {
184 let mut bytes = fixture("logo_rgb.png");
189 assert_eq!(&bytes[12..16], b"IHDR");
190 bytes[16 + 12] = 1; assert_eq!(Image::new(bytes).unwrap_err(), ImageError::UnsupportedPng);
192 }
193
194 #[test]
195 fn rejects_garbage_bytes() {
196 assert_eq!(Image::new(vec![0u8; 16]).unwrap_err(), ImageError::UnsupportedFormat);
197 }
198
199 #[test]
200 fn rejects_oversized_declared_dimensions() {
201 let mut bytes = fixture("logo_rgb.png");
202 bytes[16..20].copy_from_slice(&10_000u32.to_be_bytes());
203 bytes[20..24].copy_from_slice(&10_000u32.to_be_bytes());
204 assert_eq!(Image::new(bytes).unwrap_err(), ImageError::ImageTooLarge);
205 }
206
207 #[test]
208 fn builder_methods_set_common_fields() {
209 let img = Image::new(fixture("logo_rgb.png"))
210 .unwrap()
211 .width(100.0)
212 .height(50.0)
213 .flex(1.0)
214 .keep_with_next();
215 assert_eq!(img.common.width, Some(100.0));
216 assert_eq!(img.common.height, Some(50.0));
217 assert_eq!(img.common.flex, Some(1.0));
218 assert!(img.common.keep_with_next);
219 }
220}