use std::ffi::OsStr; use std::path::Path; use typst::geom::Smart; use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat}; use typst::util::option_eq; use crate::compute::Readable; use crate::meta::{Figurable, LocalName}; use crate::prelude::*; use crate::text::families; /// A raster or vector graphic. /// /// Supported formats are PNG, JPEG, GIF and SVG. /// /// _Note:_ Work on SVG export is ongoing and there might be visual inaccuracies /// in the resulting PDF. Make sure to double-check embedded SVG images. If you /// have an issue, also feel free to report it on [GitHub][gh-svg]. /// /// ## Example { #example } /// ```example /// #figure( /// image("molecular.jpg", width: 80%), /// caption: [ /// A step in the molecular testing /// pipeline of our lab. /// ], /// ) /// ``` /// /// [gh-svg]: https://github.com/typst/typst/issues?q=is%3Aopen+is%3Aissue+label%3Asvg /// /// Display: Image /// Category: visualize #[element(Layout, LocalName, Figurable)] #[scope( scope.define("decode", image_decode_func()); scope )] pub struct ImageElem { /// Path to an image file. #[required] #[parse( let Spanned { v: path, span } = args.expect::>("path to image file")?; let id = vm.resolve_path(&path).at(span)?; let data = vm.world().file(id).at(span)?; path )] pub path: EcoString, /// The raw file data. #[internal] #[required] #[parse(Readable::Bytes(data))] pub data: Readable, /// The image's format. Detected automatically by default. pub format: Smart, /// The width of the image. pub width: Smart>, /// The height of the image. pub height: Smart>, /// A text describing the image. pub alt: Option, /// How the image should adjust itself to a given area. #[default(ImageFit::Cover)] pub fit: ImageFit, } /// Decode a raster of vector graphic from bytes or a string. /// /// ## Example { #example } /// ```example /// #let original = read("diagram.svg") /// #let changed = original.replace( /// "#2B80FF", // blue /// green.hex(), /// ) /// /// #image.decode(original) /// #image.decode(changed) /// ``` /// /// Display: Decode Image /// Category: visualize #[func] pub fn image_decode( /// 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, ) -> StrResult { let mut elem = ImageElem::new(EcoString::new(), data); 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); } Ok(elem.pack()) } impl Layout for ImageElem { #[tracing::instrument(name = "ImageElem::layout", skip_all)] fn layout( &self, vt: &mut Vt, styles: StyleChain, regions: Regions, ) -> SourceResult { // Take the format that was explicitly defined, or parse the extention, // or try to detect the format. let data = self.data(); let format = match self.format(styles) { Smart::Custom(v) => v, Smart::Auto => { let ext = Path::new(self.path().as_str()) .extension() .and_then(OsStr::to_str) .unwrap_or_default() .to_lowercase(); match ext.as_str() { "png" => ImageFormat::Raster(RasterFormat::Png), "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg), "gif" => ImageFormat::Raster(RasterFormat::Gif), "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg), _ => match &data { Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg), Readable::Bytes(bytes) => match RasterFormat::detect(bytes) { Some(f) => ImageFormat::Raster(f), None => bail!(self.span(), "unknown image format"), }, }, } } }; let image = Image::with_fonts( data.into(), format, vt.world, families(styles).next().as_ref().map(|f| f.as_str()), self.alt(styles), ) .at(self.span())?; let sizing = Axes::new(self.width(styles), self.height(styles)); let region = sizing .zip(regions.base()) .map(|(s, r)| s.map(|v| v.resolve(styles).relative_to(r))) .unwrap_or(regions.base()); let expand = sizing.as_ref().map(Smart::is_custom) | regions.expand; let region_ratio = region.x / region.y; // Find out whether the image is wider or taller than the target size. let pxw = image.width() as f64; let pxh = image.height() as f64; let px_ratio = pxw / pxh; let wide = px_ratio > region_ratio; // The space into which the image will be placed according to its fit. let target = if expand.x && expand.y { region } else if expand.x || (!expand.y && wide && region.x.is_finite()) { Size::new(region.x, region.y.min(region.x.safe_div(px_ratio))) } else if region.y.is_finite() { Size::new(region.x.min(region.y * px_ratio), region.y) } else { Size::new(Abs::pt(pxw), Abs::pt(pxh)) }; // Compute the actual size of the fitted image. let fit = self.fit(styles); let fitted = match fit { ImageFit::Cover | ImageFit::Contain => { if wide == (fit == ImageFit::Contain) { Size::new(target.x, target.x / px_ratio) } else { Size::new(target.y * px_ratio, target.y) } } ImageFit::Stretch => target, }; // First, place the image in a frame of exactly its size and then resize // the frame to the target size, center aligning the image in the // process. let mut frame = Frame::new(fitted); frame.push(Point::zero(), FrameItem::Image(image, fitted, self.span())); frame.resize(target, Align::CENTER_HORIZON); // Create a clipping group if only part of the image should be visible. if fit == ImageFit::Cover && !target.fits(fitted) { frame.clip(); } // Apply metadata. frame.meta(styles, false); Ok(Fragment::frame(frame)) } } impl LocalName for ImageElem { fn local_name(&self, lang: Lang, region: Option) -> &'static str { match lang { Lang::ALBANIAN => "Figurë", Lang::ARABIC => "شكل", Lang::BOKMÅL => "Figur", Lang::CHINESE if option_eq(region, "TW") => "圖", Lang::CHINESE => "图", Lang::CZECH => "Obrázek", Lang::DANISH => "Figur", Lang::DUTCH => "Figuur", Lang::FILIPINO => "Pigura", Lang::FINNISH => "Kuva", Lang::FRENCH => "Figure", Lang::GERMAN => "Abbildung", Lang::ITALIAN => "Figura", Lang::NYNORSK => "Figur", Lang::POLISH => "Rysunek", Lang::PORTUGUESE => "Figura", Lang::RUSSIAN => "Рисунок", Lang::SLOVENIAN => "Slika", Lang::SPANISH => "Figura", Lang::SWEDISH => "Figur", Lang::TURKISH => "Şekil", Lang::UKRAINIAN => "Рисунок", Lang::VIETNAMESE => "Hình", Lang::JAPANESE => "図", Lang::ENGLISH | _ => "Figure", } } } impl Figurable for ImageElem {} /// 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. This is the default. Cover, /// The image should be fully contained in the area. Contain, /// The image should be stretched so that it exactly fills the area, even if /// this means that the image will be distorted. Stretch, }