//! Image handling. mod raster; mod svg; pub use self::raster::{ ExchangeFormat, PixelEncoding, PixelFormat, RasterFormat, RasterImage, }; pub use self::svg::SvgImage; use std::fmt::{self, Debug, Formatter}; use std::sync::Arc; use ecow::EcoString; use typst_syntax::{Span, Spanned}; use typst_utils::LazyHash; use crate::diag::{SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Show, Smart, StyleChain, }; use crate::layout::{BlockElem, Length, Rel, Sizing}; use crate::loading::{DataSource, Load, Readable}; use crate::model::Figurable; use crate::text::LocalName; /// A raster or vector graphic. /// /// You can wrap the image in a [`figure`] to give it a number and caption. /// /// Like most elements, images are _block-level_ by default and thus do not /// integrate themselves into adjacent paragraphs. To force an image to become /// inline, put it into a [`box`]. /// /// # Example /// ```example /// #figure( /// image("molecular.jpg", width: 80%), /// caption: [ /// A step in the molecular testing /// pipeline of our lab. /// ], /// ) /// ``` #[elem(scope, Show, LocalName, Figurable)] pub struct ImageElem { /// A path to an image file or raw bytes making up an image in one of the /// supported [formats]($image.format). /// /// For more details about paths, see the [Paths section]($syntax/#paths). #[required] #[parse( let source = args.expect::>("source")?; let data = source.load(engine.world)?; Derived::new(source.v, data) )] pub source: Derived, /// The image's format. /// /// By default, the format is detected automatically. Typically, you thus /// only need to specify this when providing raw bytes as the /// [`source`]($image.source) (even then, Typst will try to figure out the /// format automatically, but that's not always possible). /// /// Supported formats are `{"png"}`, `{"jpg"}`, `{"gif"}`, `{"svg"}` as well /// as raw pixel data. Embedding PDFs as images is /// [not currently supported](https://github.com/typst/typst/issues/145). /// /// When providing raw pixel data as the `source`, you must specify a /// dictionary with the following keys as the `format`: /// - `encoding` ([str]): The encoding of the pixel data. One of: /// - `{"rgb8"}` (three 8-bit channels: red, green, blue) /// - `{"rgba8"}` (four 8-bit channels: red, green, blue, alpha) /// - `{"luma8"}` (one 8-bit channel) /// - `{"lumaa8"}` (two 8-bit channels: luma and alpha) /// - `width` ([int]): The pixel width of the image. /// - `height` ([int]): The pixel height of the image. /// /// The pixel width multiplied by the height multiplied by the channel count /// for the specified encoding must then match the `source` data. /// /// ```example /// #image( /// read( /// "tetrahedron.svg", /// encoding: none, /// ), /// format: "svg", /// width: 2cm, /// ) /// /// #image( /// bytes(range(16).map(x => x * 16)), /// format: ( /// encoding: "luma8", /// width: 4, /// height: 4, /// ), /// width: 2cm, /// ) /// ``` pub format: Smart, /// The width of the image. pub width: Smart>, /// The height of the image. pub height: Sizing, /// A text describing the image. pub alt: Option, /// How the image should adjust itself to a given area (the area is defined /// by the `width` and `height` fields). Note that `fit` doesn't visually /// change anything if the area's aspect ratio is the same as the image's /// one. /// /// ```example /// #set page(width: 300pt, height: 50pt, margin: 10pt) /// #image("tiger.jpg", width: 100%, fit: "cover") /// #image("tiger.jpg", width: 100%, fit: "contain") /// #image("tiger.jpg", width: 100%, fit: "stretch") /// ``` #[default(ImageFit::Cover)] pub fit: ImageFit, /// A hint to viewers how they should scale the image. /// /// When set to `{auto}`, the default is left up to the viewer. For PNG /// export, Typst will default to smooth scaling, like most PDF and SVG /// viewers. /// /// _Note:_ The exact look may differ across PDF viewers. pub scaling: Smart, /// An ICC profile for the image. /// /// ICC profiles define how to interpret the colors in an image. When set /// to `{auto}`, Typst will try to extract an ICC profile from the image. #[parse(match args.named::>>("icc")? { Some(Spanned { v: Smart::Custom(source), span }) => Some(Smart::Custom({ let data = Spanned::new(&source, span).load(engine.world)?; Derived::new(source, data) })), Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto), None => None, })] #[borrowed] pub icc: Smart>, } #[scope] #[allow(clippy::too_many_arguments)] impl ImageElem { /// Decode a raster or vector graphic from bytes or a string. /// /// This function is deprecated. The [`image`] function now accepts bytes /// directly. /// /// ```example /// #let original = read("diagram.svg") /// #let changed = original.replace( /// "#2B80FF", // blue /// green.to-hex(), /// ) /// /// #image.decode(original) /// #image.decode(changed) /// ``` #[func(title = "Decode Image")] #[deprecated = "`image.decode` is deprecated, directly pass bytes to `image` instead"] pub fn decode( span: Span, /// The data to decode as an image. Can be a string for SVGs. data: Readable, /// The image's format. Detected automatically by default. #[named] format: Option>, /// The width of the image. #[named] width: Option>>, /// The height of the image. #[named] height: Option, /// A text describing the image. #[named] alt: Option>, /// How the image should adjust itself to a given area. #[named] fit: Option, /// A hint to viewers how they should scale the image. #[named] scaling: Option>, ) -> StrResult { let bytes = data.into_bytes(); let source = Derived::new(DataSource::Bytes(bytes.clone()), bytes); let mut elem = ImageElem::new(source); if let Some(format) = format { elem.push_format(format); } if let Some(width) = width { elem.push_width(width); } if let Some(height) = height { elem.push_height(height); } if let Some(alt) = alt { elem.push_alt(alt); } if let Some(fit) = fit { elem.push_fit(fit); } if let Some(scaling) = scaling { elem.push_scaling(scaling); } Ok(elem.pack().spanned(span)) } } impl Show for Packed { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_image) .with_width(self.width(styles)) .with_height(self.height(styles)) .pack() .spanned(self.span())) } } impl LocalName for Packed { const KEY: &'static str = "figure"; } impl Figurable for Packed {} /// How an image should adjust itself to a given area, #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum ImageFit { /// The image should completely cover the area (preserves aspect ratio by /// cropping the image only horizontally or vertically). This is the /// default. Cover, /// The image should be fully contained in the area (preserves aspect /// ratio; doesn't crop the image; one dimension can be narrower than /// specified). Contain, /// The image should be stretched so that it exactly fills the area, even if /// this means that the image will be distorted (doesn't preserve aspect /// ratio and doesn't crop the image). Stretch, } /// A loaded raster or vector image. /// /// Values of this type are cheap to clone and hash. #[derive(Clone, Hash, Eq, PartialEq)] pub struct Image(Arc>); /// The internal representation. #[derive(Hash)] struct Repr { /// The raw, undecoded image data. kind: ImageKind, /// A text describing the image. alt: Option, /// The scaling algorithm to use. scaling: Smart, } impl Image { /// When scaling an image to it's natural size, we default to this DPI /// if the image doesn't contain DPI metadata. pub const DEFAULT_DPI: f64 = 72.0; /// Should always be the same as the default DPI used by usvg. pub const USVG_DEFAULT_DPI: f64 = 96.0; /// Create an image from a `RasterImage` or `SvgImage`. pub fn new( kind: impl Into, alt: Option, scaling: Smart, ) -> Self { Self::new_impl(kind.into(), alt, scaling) } /// Create an image with optional properties set to the default. pub fn plain(kind: impl Into) -> Self { Self::new(kind, None, Smart::Auto) } /// The internal, non-generic implementation. This is memoized to reuse /// the `Arc` and `LazyHash`. #[comemo::memoize] fn new_impl( kind: ImageKind, alt: Option, scaling: Smart, ) -> Image { Self(Arc::new(LazyHash::new(Repr { kind, alt, scaling }))) } /// The format of the image. pub fn format(&self) -> ImageFormat { match &self.0.kind { ImageKind::Raster(raster) => raster.format().into(), ImageKind::Svg(_) => VectorFormat::Svg.into(), } } /// The width of the image in pixels. pub fn width(&self) -> f64 { match &self.0.kind { ImageKind::Raster(raster) => raster.width() as f64, ImageKind::Svg(svg) => svg.width(), } } /// The height of the image in pixels. pub fn height(&self) -> f64 { match &self.0.kind { ImageKind::Raster(raster) => raster.height() as f64, ImageKind::Svg(svg) => svg.height(), } } /// The image's pixel density in pixels per inch, if known. pub fn dpi(&self) -> Option { match &self.0.kind { ImageKind::Raster(raster) => raster.dpi(), ImageKind::Svg(_) => Some(Image::USVG_DEFAULT_DPI), } } /// A text describing the image. pub fn alt(&self) -> Option<&str> { self.0.alt.as_deref() } /// The image scaling algorithm to use for this image. pub fn scaling(&self) -> Smart { self.0.scaling } /// The decoded image. pub fn kind(&self) -> &ImageKind { &self.0.kind } } impl Debug for Image { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.debug_struct("Image") .field("format", &self.format()) .field("width", &self.width()) .field("height", &self.height()) .field("alt", &self.alt()) .field("scaling", &self.scaling()) .finish() } } /// A kind of image. #[derive(Clone, Hash)] pub enum ImageKind { /// A raster image. Raster(RasterImage), /// An SVG image. Svg(SvgImage), } impl From for ImageKind { fn from(image: RasterImage) -> Self { Self::Raster(image) } } impl From for ImageKind { fn from(image: SvgImage) -> Self { Self::Svg(image) } } /// A raster or vector image format. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum ImageFormat { /// A raster graphics format. Raster(RasterFormat), /// A vector graphics format. Vector(VectorFormat), } impl ImageFormat { /// Try to detect the format of an image from data. pub fn detect(data: &[u8]) -> Option { if let Some(format) = ExchangeFormat::detect(data) { return Some(Self::Raster(RasterFormat::Exchange(format))); } // SVG or compressed SVG. if data.starts_with(b" From for ImageFormat where R: Into, { fn from(format: R) -> Self { Self::Raster(format.into()) } } impl From for ImageFormat { fn from(format: VectorFormat) -> Self { Self::Vector(format) } } cast! { ImageFormat, self => match self { Self::Raster(v) => v.into_value(), Self::Vector(v) => v.into_value(), }, v: RasterFormat => Self::Raster(v), v: VectorFormat => Self::Vector(v), } /// The image scaling algorithm a viewer should use. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum ImageScaling { /// Scale with a smoothing algorithm such as bilinear interpolation. Smooth, /// Scale with nearest neighbor or a similar algorithm to preserve the /// pixelated look of the image. Pixelated, }