typst_library/visualize/image/
raster.rs

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/// 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: Arc<DynamicImage>,
26    exif_rotation: Option<u32>,
27    icc: Option<Bytes>,
28    dpi: Option<f64>,
29}
30
31impl RasterImage {
32    /// Decode a raster image.
33    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    /// Create a raster image with optional properties set to the default.
42    pub fn plain(data: Bytes, format: impl Into<RasterFormat>) -> StrResult<Self> {
43        Self::new(data, format, Smart::Auto)
44    }
45
46    /// The internal, non-generic implementation.
47    #[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                // Apply rotation from EXIF metadata.
90                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                // Extract pixel density.
96                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    /// The raw image data.
155    pub fn data(&self) -> &Bytes {
156        &self.0.data
157    }
158
159    /// The image's format.
160    pub fn format(&self) -> RasterFormat {
161        self.0.format
162    }
163
164    /// The image's pixel width.
165    pub fn width(&self) -> u32 {
166        self.dynamic().width()
167    }
168
169    /// The image's pixel height.
170    pub fn height(&self) -> u32 {
171        self.dynamic().height()
172    }
173
174    /// The EXIF orientation value of the original image.
175    ///
176    /// The [`dynamic`](Self::dynamic) image already has this factored in. This
177    /// value is only relevant to consumers of the raw [`data`](Self::data).
178    pub fn exif_rotation(&self) -> Option<u32> {
179        self.0.exif_rotation
180    }
181
182    /// The image's pixel density in pixels per inch, if known.
183    ///
184    /// This is guaranteed to be positive.
185    pub fn dpi(&self) -> Option<f64> {
186        self.0.dpi
187    }
188
189    /// Access the underlying dynamic image.
190    pub fn dynamic(&self) -> &Arc<DynamicImage> {
191        &self.0.dynamic
192    }
193
194    /// Access the ICC profile, if any.
195    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        // The image is fully defined by data, format, and ICC profile.
203        self.data.hash(state);
204        self.format.hash(state);
205        self.icc.hash(state);
206    }
207}
208
209/// A raster graphics format.
210#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
211pub enum RasterFormat {
212    /// A format typically used in image exchange.
213    Exchange(ExchangeFormat),
214    /// A format of raw pixel data.
215    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/// A raster format typically used in image exchange, with efficient encoding.
241#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
242pub enum ExchangeFormat {
243    /// Raster format for illustrations and transparent graphics.
244    Png,
245    /// Lossy raster format suitable for photos.
246    Jpg,
247    /// Raster format that is typically used for short animated clips. Typst can
248    /// load GIFs, but they will become static.
249    Gif,
250    /// Raster format that supports both lossy and lossless compression.
251    Webp,
252}
253
254impl ExchangeFormat {
255    /// Try to detect the format of data in a buffer.
256    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/// Information that is needed to understand a pixmap buffer.
287#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
288pub struct PixelFormat {
289    /// The channel encoding.
290    encoding: PixelEncoding,
291    /// The pixel width.
292    width: u32,
293    /// The pixel height.
294    height: u32,
295}
296
297/// Determines the channel encoding of raw pixel data.
298#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
299pub enum PixelEncoding {
300    /// Three 8-bit channels: Red, green, blue.
301    Rgb8,
302    /// Four 8-bit channels: Red, green, blue, alpha.
303    Rgba8,
304    /// One 8-bit channel.
305    Luma8,
306    /// Two 8-bit channels: Luma and alpha.
307    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
334/// Try to get the rotation from the EXIF metadata.
335fn 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
341/// Apply an EXIF rotation to a dynamic image.
342fn 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
362/// Try to determine the DPI (dots per inch) of the image.
363///
364/// This is guaranteed to be a positive value, or `None` if invalid or
365/// unspecified.
366fn determine_dpi(data: &[u8], exif: Option<&exif::Exif>) -> Option<f64> {
367    // Try to extract the DPI from the EXIF metadata. If that doesn't yield
368    // anything, fall back to specialized procedures for extracting JPEG or PNG
369    // DPI metadata. GIF does not have any.
370    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
376/// Try to get the DPI from the EXIF metadata.
377fn 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
390/// Tries to extract the DPI from raw JPEG data (by inspecting the JFIF APP0
391/// section).
392fn 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,        // already inches
416        2 => dpu * 2.54, // cm -> inches
417        _ => return None,
418    })
419}
420
421/// Tries to extract the DPI from raw PNG data.
422fn 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            // Bail as soon as there is anything data-like.
429            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), // meter -> inches
443        png::Unit::Unspecified => None,
444    }
445}
446
447/// Format the user-facing raster graphic decoding error message.
448fn 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}