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)]
70pub struct Image {
71 pub bytes: Arc<[u8]>,
72 pub format: ImageFormat,
73 pub width_px: u32,
74 pub height_px: u32,
75 pub components: u8,
77 pub common: Common,
78 pub alt: Option<String>,
89}
90
91#[cfg(feature = "serde")]
92impl serde::Serialize for Image {
93 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
94 use base64::Engine;
95 use serde::ser::SerializeStruct;
96 let mut state = serializer.serialize_struct("Image", 3)?;
97 state.serialize_field("bytes_base64", &base64::engine::general_purpose::STANDARD.encode(&self.bytes))?;
98 state.serialize_field("common", &self.common)?;
99 state.serialize_field("alt", &self.alt)?;
100 state.end()
101 }
102}
103
104#[cfg(feature = "serde")]
105impl<'de> serde::Deserialize<'de> for Image {
106 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
107 where
108 D: serde::Deserializer<'de>,
109 {
110 use base64::Engine;
111
112 #[derive(serde::Deserialize)]
113 #[serde(deny_unknown_fields)]
114 struct Raw {
115 bytes_base64: String,
116 #[serde(default)]
117 common: Common,
118 #[serde(default)]
119 alt: Option<String>,
120 }
121
122 let raw = Raw::deserialize(deserializer)?;
123 let bytes = base64::engine::general_purpose::STANDARD
124 .decode(raw.bytes_base64.as_bytes())
125 .map_err(serde::de::Error::custom)?;
126 let mut image = Image::new(bytes).map_err(serde::de::Error::custom)?;
127 image.common = raw.common;
128 image.alt = raw.alt;
129 Ok(image)
130 }
131}
132
133#[cfg(feature = "schemars")]
135impl schemars::JsonSchema for Image {
136 fn schema_name() -> std::borrow::Cow<'static, str> {
137 "Image".into()
138 }
139
140 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
141 schemars::json_schema!({
142 "type": "object",
143 "properties": {
144 "bytes_base64": { "type": "string" },
145 "common": generator.subschema_for::<Common>(),
146 "alt": { "type": ["string", "null"] }
147 },
148 "required": ["bytes_base64"],
149 "additionalProperties": false
150 })
151 }
152}
153
154impl Image {
155 pub fn new(bytes: impl Into<Arc<[u8]>>) -> Result<Self, ImageError> {
160 let bytes: Arc<[u8]> = bytes.into();
161 let (format, width_px, height_px, components) = if bytes.starts_with(&[0xFF, 0xD8]) {
162 let (w, h, c) = parse_jpeg(&bytes)?;
163 (ImageFormat::Jpeg, w, h, c)
164 } else if bytes.starts_with(&PNG_SIGNATURE) {
165 let (w, h, c) = parse_png(&bytes)?;
166 (ImageFormat::Png, w, h, c)
167 } else {
168 return Err(ImageError::UnsupportedFormat);
169 };
170 if (width_px as u64) * (height_px as u64) > MAX_PIXELS {
171 return Err(ImageError::ImageTooLarge);
172 }
173 Ok(Image {
174 bytes,
175 format,
176 width_px,
177 height_px,
178 components,
179 common: Common::default(),
180 alt: None,
181 })
182 }
183
184 pub fn alt(mut self, alt: impl Into<String>) -> Self {
187 self.alt = Some(alt.into());
188 self
189 }
190
191 pub fn width(mut self, width: f32) -> Self {
192 self.common.width = Some(width);
193 self
194 }
195
196 pub fn height(mut self, height: f32) -> Self {
197 self.common.height = Some(height);
198 self
199 }
200
201 pub fn flex(mut self, factor: f32) -> Self {
202 self.common.flex = Some(factor);
203 self
204 }
205
206 pub fn keep_with_next(mut self) -> Self {
207 self.common.keep_with_next = true;
208 self
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 fn fixture(name: &str) -> Vec<u8> {
217 std::fs::read(format!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../test-fixtures/images/{}"), name)).expect("test fixture present")
218 }
219
220 #[test]
221 fn accepts_rgba_png_with_transparency() {
222 let img = Image::new(fixture("logo_rgba.png")).unwrap();
223 assert_eq!(img.format, ImageFormat::Png);
224 assert_eq!(img.components, 4);
225 assert_eq!((img.width_px, img.height_px), (64, 48));
226 }
227
228 #[test]
229 fn accepts_opaque_rgb_png() {
230 let img = Image::new(fixture("logo_rgb.png")).unwrap();
231 assert_eq!(img.components, 3);
232 assert_eq!((img.width_px, img.height_px), (40, 30));
233 }
234
235 #[test]
236 fn accepts_baseline_rgb_jpeg() {
237 let img = Image::new(fixture("logo_baseline.jpg")).unwrap();
238 assert_eq!(img.format, ImageFormat::Jpeg);
239 assert_eq!(img.components, 3);
240 assert_eq!((img.width_px, img.height_px), (80, 60));
241 }
242
243 #[test]
244 fn accepts_baseline_gray_jpeg() {
245 let img = Image::new(fixture("logo_gray.jpg")).unwrap();
246 assert_eq!(img.components, 1);
247 assert_eq!((img.width_px, img.height_px), (32, 32));
248 }
249
250 #[test]
251 fn rejects_progressive_jpeg() {
252 assert_eq!(Image::new(fixture("progressive.jpg")).unwrap_err(), ImageError::UnsupportedJpeg);
253 }
254
255 #[test]
256 fn rejects_cmyk_jpeg() {
257 assert_eq!(Image::new(fixture("cmyk.jpg")).unwrap_err(), ImageError::UnsupportedJpeg);
258 }
259
260 #[test]
261 fn rejects_palette_png() {
262 assert_eq!(Image::new(fixture("palette.png")).unwrap_err(), ImageError::UnsupportedPng);
263 }
264
265 #[test]
266 fn rejects_sixteen_bit_png() {
267 assert_eq!(Image::new(fixture("sixteen_bit.png")).unwrap_err(), ImageError::UnsupportedPng);
268 }
269
270 #[test]
271 fn rejects_interlaced_png() {
272 let mut bytes = fixture("logo_rgb.png");
277 assert_eq!(&bytes[12..16], b"IHDR");
278 bytes[16 + 12] = 1; assert_eq!(Image::new(bytes).unwrap_err(), ImageError::UnsupportedPng);
280 }
281
282 #[test]
283 fn rejects_garbage_bytes() {
284 assert_eq!(Image::new(vec![0u8; 16]).unwrap_err(), ImageError::UnsupportedFormat);
285 }
286
287 #[test]
288 fn rejects_oversized_declared_dimensions() {
289 let mut bytes = fixture("logo_rgb.png");
290 bytes[16..20].copy_from_slice(&10_000u32.to_be_bytes());
291 bytes[20..24].copy_from_slice(&10_000u32.to_be_bytes());
292 assert_eq!(Image::new(bytes).unwrap_err(), ImageError::ImageTooLarge);
293 }
294
295 #[test]
296 fn builder_methods_set_common_fields() {
297 let img = Image::new(fixture("logo_rgb.png"))
298 .unwrap()
299 .width(100.0)
300 .height(50.0)
301 .flex(1.0)
302 .keep_with_next();
303 assert_eq!(img.common.width, Some(100.0));
304 assert_eq!(img.common.height, Some(50.0));
305 assert_eq!(img.common.flex, Some(1.0));
306 assert!(img.common.keep_with_next);
307 }
308}