1use std::cmp::Ordering;
2use std::hash::{Hash, Hasher};
3use std::io;
4use std::sync::Arc;
5
6use crate::diag::{StrResult, bail};
7use crate::foundations::{Bytes, Cast, Dict, Smart, Value, cast, dict};
8use ecow::{EcoString, eco_format};
9use image::codecs::gif::GifDecoder;
10use image::codecs::jpeg::JpegDecoder;
11use image::codecs::png::PngDecoder;
12use image::codecs::webp::WebPDecoder;
13use image::{
14 DynamicImage, ImageBuffer, ImageDecoder, ImageResult, Limits, Pixel, guess_format,
15};
16
17#[derive(Clone, Hash)]
19pub struct RasterImage(Arc<Repr>);
20
21struct Repr {
23 data: Bytes,
24 format: RasterFormat,
25 dynamic: Arc<DynamicImage>,
26 exif_rotation: Option<u32>,
27 icc: Option<Bytes>,
28 dpi: Option<f64>,
29}
30
31impl RasterImage {
32 pub fn new(
34 data: Bytes,
35 format: impl Into<RasterFormat>,
36 icc: Smart<Bytes>,
37 ) -> StrResult<Self> {
38 Self::new_impl(data, format.into(), icc)
39 }
40
41 pub fn plain(data: Bytes, format: impl Into<RasterFormat>) -> StrResult<Self> {
43 Self::new(data, format, Smart::Auto)
44 }
45
46 #[comemo::memoize]
48 #[typst_macros::time(name = "load raster image")]
49 fn new_impl(
50 data: Bytes,
51 format: RasterFormat,
52 icc: Smart<Bytes>,
53 ) -> StrResult<RasterImage> {
54 let mut exif_rot = None;
55
56 let (dynamic, icc, dpi) = match format {
57 RasterFormat::Exchange(format) => {
58 fn decode<T: ImageDecoder>(
59 decoder: ImageResult<T>,
60 icc: Smart<Bytes>,
61 ) -> ImageResult<(image::DynamicImage, Option<Bytes>)> {
62 let mut decoder = decoder?;
63 let icc = icc.custom().or_else(|| {
64 decoder
65 .icc_profile()
66 .ok()
67 .flatten()
68 .filter(|icc| !icc.is_empty())
69 .map(Bytes::new)
70 });
71 decoder.set_limits(Limits::default())?;
72 let dynamic = image::DynamicImage::from_decoder(decoder)?;
73 Ok((dynamic, icc))
74 }
75
76 let cursor = io::Cursor::new(&data);
77 let (mut dynamic, icc) = match format {
78 ExchangeFormat::Jpg => decode(JpegDecoder::new(cursor), icc),
79 ExchangeFormat::Png => decode(PngDecoder::new(cursor), icc),
80 ExchangeFormat::Gif => decode(GifDecoder::new(cursor), icc),
81 ExchangeFormat::Webp => decode(WebPDecoder::new(cursor), icc),
82 }
83 .map_err(format_image_error)?;
84
85 let exif = exif::Reader::new()
86 .read_from_container(&mut std::io::Cursor::new(&data))
87 .ok();
88
89 if let Some(rotation) = exif.as_ref().and_then(exif_rotation) {
91 apply_rotation(&mut dynamic, rotation);
92 exif_rot = Some(rotation);
93 }
94
95 let dpi = determine_dpi(&data, exif.as_ref());
97
98 (dynamic, icc, dpi)
99 }
100
101 RasterFormat::Pixel(format) => {
102 if format.width == 0 || format.height == 0 {
103 bail!("zero-sized images are not allowed");
104 }
105
106 let channels = match format.encoding {
107 PixelEncoding::Rgb8 => 3,
108 PixelEncoding::Rgba8 => 4,
109 PixelEncoding::Luma8 => 1,
110 PixelEncoding::Lumaa8 => 2,
111 };
112
113 let Some(expected_size) = format
114 .width
115 .checked_mul(format.height)
116 .and_then(|size| size.checked_mul(channels))
117 else {
118 bail!("pixel dimensions are too large");
119 };
120
121 if expected_size as usize != data.len() {
122 bail!("pixel dimensions and pixel data do not match");
123 }
124
125 fn to<P: Pixel<Subpixel = u8>>(
126 data: &Bytes,
127 format: PixelFormat,
128 ) -> ImageBuffer<P, Vec<u8>> {
129 ImageBuffer::from_raw(format.width, format.height, data.to_vec())
130 .unwrap()
131 }
132
133 let dynamic = match format.encoding {
134 PixelEncoding::Rgb8 => to::<image::Rgb<u8>>(&data, format).into(),
135 PixelEncoding::Rgba8 => to::<image::Rgba<u8>>(&data, format).into(),
136 PixelEncoding::Luma8 => to::<image::Luma<u8>>(&data, format).into(),
137 PixelEncoding::Lumaa8 => to::<image::LumaA<u8>>(&data, format).into(),
138 };
139
140 (dynamic, icc.custom(), None)
141 }
142 };
143
144 Ok(Self(Arc::new(Repr {
145 data,
146 format,
147 exif_rotation: exif_rot,
148 dynamic: Arc::new(dynamic),
149 icc,
150 dpi,
151 })))
152 }
153
154 pub fn data(&self) -> &Bytes {
156 &self.0.data
157 }
158
159 pub fn format(&self) -> RasterFormat {
161 self.0.format
162 }
163
164 pub fn width(&self) -> u32 {
166 self.dynamic().width()
167 }
168
169 pub fn height(&self) -> u32 {
171 self.dynamic().height()
172 }
173
174 pub fn exif_rotation(&self) -> Option<u32> {
179 self.0.exif_rotation
180 }
181
182 pub fn dpi(&self) -> Option<f64> {
186 self.0.dpi
187 }
188
189 pub fn dynamic(&self) -> &Arc<DynamicImage> {
191 &self.0.dynamic
192 }
193
194 pub fn icc(&self) -> Option<&Bytes> {
196 self.0.icc.as_ref()
197 }
198}
199
200impl Hash for Repr {
201 fn hash<H: Hasher>(&self, state: &mut H) {
202 self.data.hash(state);
204 self.format.hash(state);
205 self.icc.hash(state);
206 }
207}
208
209#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
211pub enum RasterFormat {
212 Exchange(ExchangeFormat),
214 Pixel(PixelFormat),
216}
217
218impl From<ExchangeFormat> for RasterFormat {
219 fn from(format: ExchangeFormat) -> Self {
220 Self::Exchange(format)
221 }
222}
223
224impl From<PixelFormat> for RasterFormat {
225 fn from(format: PixelFormat) -> Self {
226 Self::Pixel(format)
227 }
228}
229
230cast! {
231 RasterFormat,
232 self => match self {
233 Self::Exchange(v) => v.into_value(),
234 Self::Pixel(v) => v.into_value(),
235 },
236 v: ExchangeFormat => Self::Exchange(v),
237 v: PixelFormat => Self::Pixel(v),
238}
239
240#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
242pub enum ExchangeFormat {
243 Png,
245 Jpg,
247 Gif,
250 Webp,
252}
253
254impl ExchangeFormat {
255 pub fn detect(data: &[u8]) -> Option<Self> {
257 guess_format(data).ok().and_then(|format| format.try_into().ok())
258 }
259}
260
261impl From<ExchangeFormat> for image::ImageFormat {
262 fn from(format: ExchangeFormat) -> Self {
263 match format {
264 ExchangeFormat::Png => image::ImageFormat::Png,
265 ExchangeFormat::Jpg => image::ImageFormat::Jpeg,
266 ExchangeFormat::Gif => image::ImageFormat::Gif,
267 ExchangeFormat::Webp => image::ImageFormat::WebP,
268 }
269 }
270}
271
272impl TryFrom<image::ImageFormat> for ExchangeFormat {
273 type Error = EcoString;
274
275 fn try_from(format: image::ImageFormat) -> StrResult<Self> {
276 Ok(match format {
277 image::ImageFormat::Png => ExchangeFormat::Png,
278 image::ImageFormat::Jpeg => ExchangeFormat::Jpg,
279 image::ImageFormat::Gif => ExchangeFormat::Gif,
280 image::ImageFormat::WebP => ExchangeFormat::Webp,
281 _ => bail!("format not yet supported"),
282 })
283 }
284}
285
286#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
288pub struct PixelFormat {
289 encoding: PixelEncoding,
291 width: u32,
293 height: u32,
295}
296
297#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
299pub enum PixelEncoding {
300 Rgb8,
302 Rgba8,
304 Luma8,
306 Lumaa8,
308}
309
310cast! {
311 PixelFormat,
312 self => Value::Dict(self.into()),
313 mut dict: Dict => {
314 let format = Self {
315 encoding: dict.take("encoding")?.cast()?,
316 width: dict.take("width")?.cast()?,
317 height: dict.take("height")?.cast()?,
318 };
319 dict.finish(&["encoding", "width", "height"])?;
320 format
321 }
322}
323
324impl From<PixelFormat> for Dict {
325 fn from(format: PixelFormat) -> Self {
326 dict! {
327 "encoding" => format.encoding,
328 "width" => format.width,
329 "height" => format.height,
330 }
331 }
332}
333
334fn exif_rotation(exif: &exif::Exif) -> Option<u32> {
336 exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY)?
337 .value
338 .get_uint(0)
339}
340
341fn apply_rotation(image: &mut DynamicImage, rotation: u32) {
343 use image::imageops as ops;
344 match rotation {
345 2 => ops::flip_horizontal_in_place(image),
346 3 => ops::rotate180_in_place(image),
347 4 => ops::flip_vertical_in_place(image),
348 5 => {
349 ops::flip_horizontal_in_place(image);
350 *image = image.rotate270();
351 }
352 6 => *image = image.rotate90(),
353 7 => {
354 ops::flip_horizontal_in_place(image);
355 *image = image.rotate90();
356 }
357 8 => *image = image.rotate270(),
358 _ => {}
359 }
360}
361
362fn determine_dpi(data: &[u8], exif: Option<&exif::Exif>) -> Option<f64> {
367 exif.and_then(exif_dpi)
371 .or_else(|| jpeg_dpi(data))
372 .or_else(|| png_dpi(data))
373 .filter(|&dpi| dpi > 0.0)
374}
375
376fn exif_dpi(exif: &exif::Exif) -> Option<f64> {
378 let axis = |tag| {
379 let dpi = exif.get_field(tag, exif::In::PRIMARY)?;
380 let exif::Value::Rational(rational) = &dpi.value else { return None };
381 Some(rational.first()?.to_f64())
382 };
383
384 [axis(exif::Tag::XResolution), axis(exif::Tag::YResolution)]
385 .into_iter()
386 .flatten()
387 .max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
388}
389
390fn jpeg_dpi(data: &[u8]) -> Option<f64> {
393 let validate_at = |index: usize, expect: &[u8]| -> Option<()> {
394 data.get(index..)?.starts_with(expect).then_some(())
395 };
396 let u16_at = |index: usize| -> Option<u16> {
397 data.get(index..index + 2)?.try_into().ok().map(u16::from_be_bytes)
398 };
399
400 validate_at(0, b"\xFF\xD8\xFF\xE0\0")?;
401 validate_at(6, b"JFIF\0")?;
402 validate_at(11, b"\x01")?;
403
404 let len = u16_at(4)?;
405 if len < 16 {
406 return None;
407 }
408
409 let units = *data.get(13)?;
410 let x = u16_at(14)?;
411 let y = u16_at(16)?;
412 let dpu = x.max(y) as f64;
413
414 Some(match units {
415 1 => dpu, 2 => dpu * 2.54, _ => return None,
418 })
419}
420
421fn png_dpi(mut data: &[u8]) -> Option<f64> {
423 let mut decoder = png::StreamingDecoder::new();
424 let dims = loop {
425 let (consumed, event) = decoder.update(data, &mut Vec::new()).ok()?;
426 match event {
427 png::Decoded::PixelDimensions(dims) => break dims,
428 png::Decoded::ChunkBegin(_, png::chunk::IDAT)
430 | png::Decoded::ImageData
431 | png::Decoded::ImageEnd => return None,
432 _ => {}
433 }
434 data = data.get(consumed..)?;
435 if consumed == 0 {
436 return None;
437 }
438 };
439
440 let dpu = dims.xppu.max(dims.yppu) as f64;
441 match dims.unit {
442 png::Unit::Meter => Some(dpu * 0.0254), png::Unit::Unspecified => None,
444 }
445}
446
447fn format_image_error(error: image::ImageError) -> EcoString {
449 match error {
450 image::ImageError::Limits(_) => "file is too large".into(),
451 err => eco_format!("failed to decode image ({err})"),
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458
459 #[test]
460 fn test_image_dpi() {
461 #[track_caller]
462 fn test(path: &str, format: ExchangeFormat, dpi: f64) {
463 let data = typst_dev_assets::get(path).unwrap();
464 let bytes = Bytes::new(data);
465 let image = RasterImage::plain(bytes, format).unwrap();
466 assert_eq!(image.dpi().map(f64::round), Some(dpi));
467 }
468
469 test("images/f2t.jpg", ExchangeFormat::Jpg, 220.0);
470 test("images/tiger.jpg", ExchangeFormat::Jpg, 72.0);
471 test("images/graph.png", ExchangeFormat::Png, 144.0);
472 }
473}