typst_library/visualize/image/
raster.rs

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/// A decoded raster image.
18#[derive(Clone, Hash)]
19pub struct RasterImage(Arc<Repr>);
20
21/// The internal representation.
22struct Repr {
23    data: Bytes,
24    format: RasterFormat,
25    dynamic: image::DynamicImage,
26    icc: Option<Bytes>,
27    dpi: Option<f64>,
28}
29
30impl RasterImage {
31    /// Decode a raster image.
32    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    /// Create a raster image with optional properties set to the default.
41    pub fn plain(data: Bytes, format: impl Into<RasterFormat>) -> StrResult<Self> {
42        Self::new(data, format, Smart::Auto)
43    }
44
45    /// The internal, non-generic implementation.
46    #[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                // Apply rotation from EXIF metadata.
86                if let Some(rotation) = exif.as_ref().and_then(exif_rotation) {
87                    apply_rotation(&mut dynamic, rotation);
88                }
89
90                // Extract pixel density.
91                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    /// The raw image data.
143    pub fn data(&self) -> &Bytes {
144        &self.0.data
145    }
146
147    /// The image's format.
148    pub fn format(&self) -> RasterFormat {
149        self.0.format
150    }
151
152    /// The image's pixel width.
153    pub fn width(&self) -> u32 {
154        self.dynamic().width()
155    }
156
157    /// The image's pixel height.
158    pub fn height(&self) -> u32 {
159        self.dynamic().height()
160    }
161
162    /// The image's pixel density in pixels per inch, if known.
163    ///
164    /// This is guaranteed to be positive.
165    pub fn dpi(&self) -> Option<f64> {
166        self.0.dpi
167    }
168
169    /// Access the underlying dynamic image.
170    pub fn dynamic(&self) -> &image::DynamicImage {
171        &self.0.dynamic
172    }
173
174    /// Access the ICC profile, if any.
175    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        // The image is fully defined by data, format, and ICC profile.
183        self.data.hash(state);
184        self.format.hash(state);
185        self.icc.hash(state);
186    }
187}
188
189/// A raster graphics format.
190#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
191pub enum RasterFormat {
192    /// A format typically used in image exchange.
193    Exchange(ExchangeFormat),
194    /// A format of raw pixel data.
195    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/// A raster format typically used in image exchange, with efficient encoding.
221#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
222pub enum ExchangeFormat {
223    /// Raster format for illustrations and transparent graphics.
224    Png,
225    /// Lossy raster format suitable for photos.
226    Jpg,
227    /// Raster format that is typically used for short animated clips. Typst can
228    /// load GIFs, but they will become static.
229    Gif,
230}
231
232impl ExchangeFormat {
233    /// Try to detect the format of data in a buffer.
234    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/// Information that is needed to understand a pixmap buffer.
263#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
264pub struct PixelFormat {
265    /// The channel encoding.
266    encoding: PixelEncoding,
267    /// The pixel width.
268    width: u32,
269    /// The pixel height.
270    height: u32,
271}
272
273/// Determines the channel encoding of raw pixel data.
274#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
275pub enum PixelEncoding {
276    /// Three 8-bit channels: Red, green, blue.
277    Rgb8,
278    /// Four 8-bit channels: Red, green, blue, alpha.
279    Rgba8,
280    /// One 8-bit channel.
281    Luma8,
282    /// Two 8-bit channels: Luma and alpha.
283    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
310/// Try to get the rotation from the EXIF metadata.
311fn 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
317/// Apply an EXIF rotation to a dynamic image.
318fn 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
338/// Try to determine the DPI (dots per inch) of the image.
339///
340/// This is guaranteed to be a positive value, or `None` if invalid or
341/// unspecified.
342fn determine_dpi(data: &[u8], exif: Option<&exif::Exif>) -> Option<f64> {
343    // Try to extract the DPI from the EXIF metadata. If that doesn't yield
344    // anything, fall back to specialized procedures for extracting JPEG or PNG
345    // DPI metadata. GIF does not have any.
346    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
352/// Try to get the DPI from the EXIF metadata.
353fn 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
366/// Tries to extract the DPI from raw JPEG data (by inspecting the JFIF APP0
367/// section).
368fn 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,        // already inches
392        2 => dpu * 2.54, // cm -> inches
393        _ => return None,
394    })
395}
396
397/// Tries to extract the DPI from raw PNG data.
398fn 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            // Bail as soon as there is anything data-like.
405            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), // meter -> inches
419        png::Unit::Unspecified => None,
420    }
421}
422
423/// Format the user-facing raster graphic decoding error message.
424fn 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}