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#[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 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 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 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 let pxw = image.width();
76 let pxh = image.height();
77 let px_ratio = pxw / pxh;
78
79 let region_ratio = region.size.x / region.size.y;
81
82 let wide = px_ratio > region_ratio;
84
85 let target = if region.expand.x && region.expand.y {
87 region.size
89 } else if region.expand.x {
90 Size::new(region.size.x, region.size.y.min(region.size.x / px_ratio))
92 } else if region.expand.y {
93 Size::new(region.size.x.min(region.size.y * px_ratio), region.size.y)
95 } else {
96 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 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 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 if fit == ImageFit::Cover && !target.fits(fitted) {
130 frame.clip(Curve::rect(frame.size()));
131 }
132
133 Ok(frame)
134}
135
136fn 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}