typst_layout/
image.rs

1use std::ffi::OsStr;
2
3use typst_library::diag::{warning, At, SourceResult, StrResult};
4use typst_library::engine::Engine;
5use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain};
6use typst_library::introspection::Locator;
7use typst_library::layout::{
8    Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size,
9};
10use typst_library::loading::DataSource;
11use typst_library::text::families;
12use typst_library::visualize::{
13    Curve, ExchangeFormat, Image, ImageElem, ImageFit, ImageFormat, ImageKind,
14    RasterImage, SvgImage, VectorFormat,
15};
16
17/// Layout the image.
18#[typst_macros::time(span = elem.span())]
19pub fn layout_image(
20    elem: &Packed<ImageElem>,
21    engine: &mut Engine,
22    _: Locator,
23    styles: StyleChain,
24    region: Region,
25) -> SourceResult<Frame> {
26    let span = elem.span();
27
28    // Take the format that was explicitly defined, or parse the extension,
29    // or try to detect the format.
30    let Derived { source, derived: data } = &elem.source;
31    let format = match elem.format(styles) {
32        Smart::Custom(v) => v,
33        Smart::Auto => determine_format(source, data).at(span)?,
34    };
35
36    // Warn the user if the image contains a foreign object. Not perfect
37    // because the svg could also be encoded, but that's an edge case.
38    if format == ImageFormat::Vector(VectorFormat::Svg) {
39        let has_foreign_object =
40            data.as_str().is_ok_and(|s| s.contains("<foreignObject"));
41
42        if has_foreign_object {
43            engine.sink.warn(warning!(
44                span,
45                "image contains foreign object";
46                hint: "SVG images with foreign objects might render incorrectly in typst";
47                hint: "see https://github.com/typst/typst/issues/1421 for more information"
48            ));
49        }
50    }
51
52    // Construct the image itself.
53    let kind = match format {
54        ImageFormat::Raster(format) => ImageKind::Raster(
55            RasterImage::new(
56                data.clone(),
57                format,
58                elem.icc(styles).as_ref().map(|icc| icc.derived.clone()),
59            )
60            .at(span)?,
61        ),
62        ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg(
63            SvgImage::with_fonts(
64                data.clone(),
65                engine.world,
66                &families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
67            )
68            .at(span)?,
69        ),
70    };
71
72    let image = Image::new(kind, elem.alt(styles), elem.scaling(styles));
73
74    // Determine the image's pixel aspect ratio.
75    let pxw = image.width();
76    let pxh = image.height();
77    let px_ratio = pxw / pxh;
78
79    // Determine the region's aspect ratio.
80    let region_ratio = region.size.x / region.size.y;
81
82    // Find out whether the image is wider or taller than the region.
83    let wide = px_ratio > region_ratio;
84
85    // The space into which the image will be placed according to its fit.
86    let target = if region.expand.x && region.expand.y {
87        // If both width and height are forced, take them.
88        region.size
89    } else if region.expand.x {
90        // If just width is forced, take it.
91        Size::new(region.size.x, region.size.y.min(region.size.x / px_ratio))
92    } else if region.expand.y {
93        // If just height is forced, take it.
94        Size::new(region.size.x.min(region.size.y * px_ratio), region.size.y)
95    } else {
96        // If neither is forced, take the natural image size at the image's
97        // DPI bounded by the available space.
98        //
99        // Division by DPI is fine since it's guaranteed to be positive.
100        let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI);
101        let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi));
102        Size::new(
103            natural.x.min(region.size.x).min(region.size.y * px_ratio),
104            natural.y.min(region.size.y).min(region.size.x / px_ratio),
105        )
106    };
107
108    // Compute the actual size of the fitted image.
109    let fit = elem.fit(styles);
110    let fitted = match fit {
111        ImageFit::Cover | ImageFit::Contain => {
112            if wide == (fit == ImageFit::Contain) {
113                Size::new(target.x, target.x / px_ratio)
114            } else {
115                Size::new(target.y * px_ratio, target.y)
116            }
117        }
118        ImageFit::Stretch => target,
119    };
120
121    // First, place the image in a frame of exactly its size and then resize
122    // the frame to the target size, center aligning the image in the
123    // process.
124    let mut frame = Frame::soft(fitted);
125    frame.push(Point::zero(), FrameItem::Image(image, fitted, span));
126    frame.resize(target, Axes::splat(FixedAlignment::Center));
127
128    // Create a clipping group if only part of the image should be visible.
129    if fit == ImageFit::Cover && !target.fits(fitted) {
130        frame.clip(Curve::rect(frame.size()));
131    }
132
133    Ok(frame)
134}
135
136/// Try to determine the image format based on the data.
137fn determine_format(source: &DataSource, data: &Bytes) -> StrResult<ImageFormat> {
138    if let DataSource::Path(path) = source {
139        let ext = std::path::Path::new(path.as_str())
140            .extension()
141            .and_then(OsStr::to_str)
142            .unwrap_or_default()
143            .to_lowercase();
144
145        match ext.as_str() {
146            "png" => return Ok(ExchangeFormat::Png.into()),
147            "jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()),
148            "gif" => return Ok(ExchangeFormat::Gif.into()),
149            "svg" | "svgz" => return Ok(VectorFormat::Svg.into()),
150            _ => {}
151        }
152    }
153
154    Ok(ImageFormat::detect(data).ok_or("unknown image format")?)
155}