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