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}