typst_library/visualize/image/
mod.rs

1//! Image handling.
2
3mod raster;
4mod svg;
5
6pub use self::raster::{
7    ExchangeFormat, PixelEncoding, PixelFormat, RasterFormat, RasterImage,
8};
9pub use self::svg::SvgImage;
10
11use std::fmt::{self, Debug, Formatter};
12use std::sync::Arc;
13
14use ecow::EcoString;
15use typst_syntax::{Span, Spanned};
16use typst_utils::LazyHash;
17
18use crate::diag::{SourceResult, StrResult};
19use crate::engine::Engine;
20use crate::foundations::{
21    cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Show,
22    Smart, StyleChain,
23};
24use crate::layout::{BlockElem, Length, Rel, Sizing};
25use crate::loading::{DataSource, Load, Readable};
26use crate::model::Figurable;
27use crate::text::LocalName;
28
29/// A raster or vector graphic.
30///
31/// You can wrap the image in a [`figure`] to give it a number and caption.
32///
33/// Like most elements, images are _block-level_ by default and thus do not
34/// integrate themselves into adjacent paragraphs. To force an image to become
35/// inline, put it into a [`box`].
36///
37/// # Example
38/// ```example
39/// #figure(
40///   image("molecular.jpg", width: 80%),
41///   caption: [
42///     A step in the molecular testing
43///     pipeline of our lab.
44///   ],
45/// )
46/// ```
47#[elem(scope, Show, LocalName, Figurable)]
48pub struct ImageElem {
49    /// A [path]($syntax/#paths) to an image file or raw bytes making up an
50    /// image in one of the supported [formats]($image.format).
51    ///
52    /// Bytes can be used to specify raw pixel data in a row-major,
53    /// left-to-right, top-to-bottom format.
54    ///
55    /// ```example
56    /// #let original = read("diagram.svg")
57    /// #let changed = original.replace(
58    ///   "#2B80FF", // blue
59    ///   green.to-hex(),
60    /// )
61    ///
62    /// #image(bytes(original))
63    /// #image(bytes(changed))
64    /// ```
65    #[required]
66    #[parse(
67        let source = args.expect::<Spanned<DataSource>>("source")?;
68        let data = source.load(engine.world)?;
69        Derived::new(source.v, data)
70    )]
71    pub source: Derived<DataSource, Bytes>,
72
73    /// The image's format.
74    ///
75    /// By default, the format is detected automatically. Typically, you thus
76    /// only need to specify this when providing raw bytes as the
77    /// [`source`]($image.source) (even then, Typst will try to figure out the
78    /// format automatically, but that's not always possible).
79    ///
80    /// Supported formats are `{"png"}`, `{"jpg"}`, `{"gif"}`, `{"svg"}` as well
81    /// as raw pixel data. Embedding PDFs as images is
82    /// [not currently supported](https://github.com/typst/typst/issues/145).
83    ///
84    /// When providing raw pixel data as the `source`, you must specify a
85    /// dictionary with the following keys as the `format`:
86    /// - `encoding` ([str]): The encoding of the pixel data. One of:
87    ///   - `{"rgb8"}` (three 8-bit channels: red, green, blue)
88    ///   - `{"rgba8"}` (four 8-bit channels: red, green, blue, alpha)
89    ///   - `{"luma8"}` (one 8-bit channel)
90    ///   - `{"lumaa8"}` (two 8-bit channels: luma and alpha)
91    /// - `width` ([int]): The pixel width of the image.
92    /// - `height` ([int]): The pixel height of the image.
93    ///
94    /// The pixel width multiplied by the height multiplied by the channel count
95    /// for the specified encoding must then match the `source` data.
96    ///
97    /// ```example
98    /// #image(
99    ///   read(
100    ///     "tetrahedron.svg",
101    ///     encoding: none,
102    ///   ),
103    ///   format: "svg",
104    ///   width: 2cm,
105    /// )
106    ///
107    /// #image(
108    ///   bytes(range(16).map(x => x * 16)),
109    ///   format: (
110    ///     encoding: "luma8",
111    ///     width: 4,
112    ///     height: 4,
113    ///   ),
114    ///   width: 2cm,
115    /// )
116    /// ```
117    pub format: Smart<ImageFormat>,
118
119    /// The width of the image.
120    pub width: Smart<Rel<Length>>,
121
122    /// The height of the image.
123    pub height: Sizing,
124
125    /// A text describing the image.
126    pub alt: Option<EcoString>,
127
128    /// How the image should adjust itself to a given area (the area is defined
129    /// by the `width` and `height` fields). Note that `fit` doesn't visually
130    /// change anything if the area's aspect ratio is the same as the image's
131    /// one.
132    ///
133    /// ```example
134    /// #set page(width: 300pt, height: 50pt, margin: 10pt)
135    /// #image("tiger.jpg", width: 100%, fit: "cover")
136    /// #image("tiger.jpg", width: 100%, fit: "contain")
137    /// #image("tiger.jpg", width: 100%, fit: "stretch")
138    /// ```
139    #[default(ImageFit::Cover)]
140    pub fit: ImageFit,
141
142    /// A hint to viewers how they should scale the image.
143    ///
144    /// When set to `{auto}`, the default is left up to the viewer. For PNG
145    /// export, Typst will default to smooth scaling, like most PDF and SVG
146    /// viewers.
147    ///
148    /// _Note:_ The exact look may differ across PDF viewers.
149    pub scaling: Smart<ImageScaling>,
150
151    /// An ICC profile for the image.
152    ///
153    /// ICC profiles define how to interpret the colors in an image. When set
154    /// to `{auto}`, Typst will try to extract an ICC profile from the image.
155    #[parse(match args.named::<Spanned<Smart<DataSource>>>("icc")? {
156        Some(Spanned { v: Smart::Custom(source), span }) => Some(Smart::Custom({
157            let data = Spanned::new(&source, span).load(engine.world)?;
158            Derived::new(source, data)
159        })),
160        Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto),
161        None => None,
162    })]
163    #[borrowed]
164    pub icc: Smart<Derived<DataSource, Bytes>>,
165}
166
167#[scope]
168#[allow(clippy::too_many_arguments)]
169impl ImageElem {
170    /// Decode a raster or vector graphic from bytes or a string.
171    #[func(title = "Decode Image")]
172    #[deprecated = "`image.decode` is deprecated, directly pass bytes to `image` instead"]
173    pub fn decode(
174        span: Span,
175        /// The data to decode as an image. Can be a string for SVGs.
176        data: Readable,
177        /// The image's format. Detected automatically by default.
178        #[named]
179        format: Option<Smart<ImageFormat>>,
180        /// The width of the image.
181        #[named]
182        width: Option<Smart<Rel<Length>>>,
183        /// The height of the image.
184        #[named]
185        height: Option<Sizing>,
186        /// A text describing the image.
187        #[named]
188        alt: Option<Option<EcoString>>,
189        /// How the image should adjust itself to a given area.
190        #[named]
191        fit: Option<ImageFit>,
192        /// A hint to viewers how they should scale the image.
193        #[named]
194        scaling: Option<Smart<ImageScaling>>,
195    ) -> StrResult<Content> {
196        let bytes = data.into_bytes();
197        let source = Derived::new(DataSource::Bytes(bytes.clone()), bytes);
198        let mut elem = ImageElem::new(source);
199        if let Some(format) = format {
200            elem.push_format(format);
201        }
202        if let Some(width) = width {
203            elem.push_width(width);
204        }
205        if let Some(height) = height {
206            elem.push_height(height);
207        }
208        if let Some(alt) = alt {
209            elem.push_alt(alt);
210        }
211        if let Some(fit) = fit {
212            elem.push_fit(fit);
213        }
214        if let Some(scaling) = scaling {
215            elem.push_scaling(scaling);
216        }
217        Ok(elem.pack().spanned(span))
218    }
219}
220
221impl Show for Packed<ImageElem> {
222    fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
223        Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_image)
224            .with_width(self.width(styles))
225            .with_height(self.height(styles))
226            .pack()
227            .spanned(self.span()))
228    }
229}
230
231impl LocalName for Packed<ImageElem> {
232    const KEY: &'static str = "figure";
233}
234
235impl Figurable for Packed<ImageElem> {}
236
237/// How an image should adjust itself to a given area,
238#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
239pub enum ImageFit {
240    /// The image should completely cover the area (preserves aspect ratio by
241    /// cropping the image only horizontally or vertically). This is the
242    /// default.
243    Cover,
244    /// The image should be fully contained in the area (preserves aspect
245    /// ratio; doesn't crop the image; one dimension can be narrower than
246    /// specified).
247    Contain,
248    /// The image should be stretched so that it exactly fills the area, even if
249    /// this means that the image will be distorted (doesn't preserve aspect
250    /// ratio and doesn't crop the image).
251    Stretch,
252}
253
254/// A loaded raster or vector image.
255///
256/// Values of this type are cheap to clone and hash.
257#[derive(Clone, Hash, Eq, PartialEq)]
258pub struct Image(Arc<LazyHash<Repr>>);
259
260/// The internal representation.
261#[derive(Hash)]
262struct Repr {
263    /// The raw, undecoded image data.
264    kind: ImageKind,
265    /// A text describing the image.
266    alt: Option<EcoString>,
267    /// The scaling algorithm to use.
268    scaling: Smart<ImageScaling>,
269}
270
271impl Image {
272    /// When scaling an image to it's natural size, we default to this DPI
273    /// if the image doesn't contain DPI metadata.
274    pub const DEFAULT_DPI: f64 = 72.0;
275
276    /// Should always be the same as the default DPI used by usvg.
277    pub const USVG_DEFAULT_DPI: f64 = 96.0;
278
279    /// Create an image from a `RasterImage` or `SvgImage`.
280    pub fn new(
281        kind: impl Into<ImageKind>,
282        alt: Option<EcoString>,
283        scaling: Smart<ImageScaling>,
284    ) -> Self {
285        Self::new_impl(kind.into(), alt, scaling)
286    }
287
288    /// Create an image with optional properties set to the default.
289    pub fn plain(kind: impl Into<ImageKind>) -> Self {
290        Self::new(kind, None, Smart::Auto)
291    }
292
293    /// The internal, non-generic implementation. This is memoized to reuse
294    /// the `Arc` and `LazyHash`.
295    #[comemo::memoize]
296    fn new_impl(
297        kind: ImageKind,
298        alt: Option<EcoString>,
299        scaling: Smart<ImageScaling>,
300    ) -> Image {
301        Self(Arc::new(LazyHash::new(Repr { kind, alt, scaling })))
302    }
303
304    /// The format of the image.
305    pub fn format(&self) -> ImageFormat {
306        match &self.0.kind {
307            ImageKind::Raster(raster) => raster.format().into(),
308            ImageKind::Svg(_) => VectorFormat::Svg.into(),
309        }
310    }
311
312    /// The width of the image in pixels.
313    pub fn width(&self) -> f64 {
314        match &self.0.kind {
315            ImageKind::Raster(raster) => raster.width() as f64,
316            ImageKind::Svg(svg) => svg.width(),
317        }
318    }
319
320    /// The height of the image in pixels.
321    pub fn height(&self) -> f64 {
322        match &self.0.kind {
323            ImageKind::Raster(raster) => raster.height() as f64,
324            ImageKind::Svg(svg) => svg.height(),
325        }
326    }
327
328    /// The image's pixel density in pixels per inch, if known.
329    pub fn dpi(&self) -> Option<f64> {
330        match &self.0.kind {
331            ImageKind::Raster(raster) => raster.dpi(),
332            ImageKind::Svg(_) => Some(Image::USVG_DEFAULT_DPI),
333        }
334    }
335
336    /// A text describing the image.
337    pub fn alt(&self) -> Option<&str> {
338        self.0.alt.as_deref()
339    }
340
341    /// The image scaling algorithm to use for this image.
342    pub fn scaling(&self) -> Smart<ImageScaling> {
343        self.0.scaling
344    }
345
346    /// The decoded image.
347    pub fn kind(&self) -> &ImageKind {
348        &self.0.kind
349    }
350}
351
352impl Debug for Image {
353    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
354        f.debug_struct("Image")
355            .field("format", &self.format())
356            .field("width", &self.width())
357            .field("height", &self.height())
358            .field("alt", &self.alt())
359            .field("scaling", &self.scaling())
360            .finish()
361    }
362}
363
364/// A kind of image.
365#[derive(Clone, Hash)]
366pub enum ImageKind {
367    /// A raster image.
368    Raster(RasterImage),
369    /// An SVG image.
370    Svg(SvgImage),
371}
372
373impl From<RasterImage> for ImageKind {
374    fn from(image: RasterImage) -> Self {
375        Self::Raster(image)
376    }
377}
378
379impl From<SvgImage> for ImageKind {
380    fn from(image: SvgImage) -> Self {
381        Self::Svg(image)
382    }
383}
384
385/// A raster or vector image format.
386#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
387pub enum ImageFormat {
388    /// A raster graphics format.
389    Raster(RasterFormat),
390    /// A vector graphics format.
391    Vector(VectorFormat),
392}
393
394impl ImageFormat {
395    /// Try to detect the format of an image from data.
396    pub fn detect(data: &[u8]) -> Option<Self> {
397        if let Some(format) = ExchangeFormat::detect(data) {
398            return Some(Self::Raster(RasterFormat::Exchange(format)));
399        }
400
401        if is_svg(data) {
402            return Some(Self::Vector(VectorFormat::Svg));
403        }
404
405        None
406    }
407}
408
409/// Checks whether the data looks like an SVG or a compressed SVG.
410fn is_svg(data: &[u8]) -> bool {
411    // Check for the gzip magic bytes. This check is perhaps a bit too
412    // permissive as other formats than SVGZ could use gzip.
413    if data.starts_with(&[0x1f, 0x8b]) {
414        return true;
415    }
416
417    // If the first 2048 bytes contain the SVG namespace declaration, we assume
418    // that it's an SVG. Note that, if the SVG does not contain a namespace
419    // declaration, usvg will reject it.
420    let head = &data[..data.len().min(2048)];
421    memchr::memmem::find(head, b"http://www.w3.org/2000/svg").is_some()
422}
423
424/// A vector graphics format.
425#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
426pub enum VectorFormat {
427    /// The vector graphics format of the web.
428    Svg,
429}
430
431impl<R> From<R> for ImageFormat
432where
433    R: Into<RasterFormat>,
434{
435    fn from(format: R) -> Self {
436        Self::Raster(format.into())
437    }
438}
439
440impl From<VectorFormat> for ImageFormat {
441    fn from(format: VectorFormat) -> Self {
442        Self::Vector(format)
443    }
444}
445
446cast! {
447    ImageFormat,
448    self => match self {
449        Self::Raster(v) => v.into_value(),
450        Self::Vector(v) => v.into_value(),
451    },
452    v: RasterFormat => Self::Raster(v),
453    v: VectorFormat => Self::Vector(v),
454}
455
456/// The image scaling algorithm a viewer should use.
457#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
458pub enum ImageScaling {
459    /// Scale with a smoothing algorithm such as bilinear interpolation.
460    Smooth,
461    /// Scale with nearest neighbor or a similar algorithm to preserve the
462    /// pixelated look of the image.
463    Pixelated,
464}