diff --git a/crates/typst-library/src/compute/data.rs b/crates/typst-library/src/compute/data.rs index 4a7c53cc1..48e358468 100644 --- a/crates/typst-library/src/compute/data.rs +++ b/crates/typst-library/src/compute/data.rs @@ -57,7 +57,7 @@ pub enum Encoding { Utf8, } -/// A value that can be read from a value. +/// A value that can be read from a file. pub enum Readable { /// A decoded string. Str(Str), @@ -75,6 +75,15 @@ cast! { v: Bytes => Self::Bytes(v), } +impl From for Bytes { + fn from(value: Readable) -> Self { + match value { + Readable::Bytes(v) => v, + Readable::Str(v) => v.as_bytes().into(), + } + } +} + /// Reads structured data from a CSV file. /// /// The CSV file will be read and parsed into a 2-dimensional array of strings: diff --git a/crates/typst-library/src/visualize/image.rs b/crates/typst-library/src/visualize/image.rs index 514861e5d..7c42ef3ce 100644 --- a/crates/typst-library/src/visualize/image.rs +++ b/crates/typst-library/src/visualize/image.rs @@ -1,9 +1,10 @@ use std::ffi::OsStr; use std::path::Path; -use typst::eval::Bytes; +use typst::geom::Smart; use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat}; +use crate::compute::Readable; use crate::meta::{Figurable, LocalName}; use crate::prelude::*; use crate::text::families; @@ -32,6 +33,10 @@ use crate::text::families; /// 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] @@ -47,8 +52,11 @@ pub struct ImageElem { /// The raw file data. #[internal] #[required] - #[parse(data)] - pub data: Bytes, + #[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>, @@ -64,6 +72,61 @@ pub struct ImageElem { 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( @@ -72,22 +135,36 @@ impl Layout for ImageElem { styles: StyleChain, regions: Regions, ) -> SourceResult { - let ext = Path::new(self.path().as_str()) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default() - .to_lowercase(); + // 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(); - let format = 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), - _ => bail!(self.span(), "unknown image format"), + 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( - self.data(), + data.into(), format, vt.world, families(styles).next().as_ref().map(|f| f.as_str()), diff --git a/crates/typst/src/image.rs b/crates/typst/src/image.rs index 1b62a5ace..05672e2a8 100644 --- a/crates/typst/src/image.rs +++ b/crates/typst/src/image.rs @@ -12,10 +12,11 @@ use image::codecs::gif::GifDecoder; use image::codecs::jpeg::JpegDecoder; use image::codecs::png::PngDecoder; use image::io::Limits; -use image::{ImageDecoder, ImageResult}; +use image::{guess_format, ImageDecoder, ImageResult}; +use typst_macros::{cast, Cast}; use usvg::{TreeParsing, TreeTextToPath}; -use crate::diag::{format_xml_like_error, StrResult}; +use crate::diag::{bail, format_xml_like_error, StrResult}; use crate::eval::Bytes; use crate::font::Font; use crate::geom::Axes; @@ -156,8 +157,18 @@ pub enum ImageFormat { Vector(VectorFormat), } +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), +} + /// A raster graphics format. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum RasterFormat { /// Raster format for illustrations and transparent graphics. Png, @@ -168,12 +179,19 @@ pub enum RasterFormat { } /// A vector graphics format. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum VectorFormat { /// The vector graphics format of the web. Svg, } +impl RasterFormat { + /// Try to detect the format of data in a buffer. + pub fn detect(data: &[u8]) -> Option { + guess_format(data).ok().and_then(|format| format.try_into().ok()) + } +} + impl From for image::ImageFormat { fn from(format: RasterFormat) -> Self { match format { @@ -184,6 +202,19 @@ impl From for image::ImageFormat { } } +impl TryFrom for RasterFormat { + type Error = EcoString; + + fn try_from(format: image::ImageFormat) -> StrResult { + Ok(match format { + image::ImageFormat::Png => RasterFormat::Png, + image::ImageFormat::Jpeg => RasterFormat::Jpg, + image::ImageFormat::Gif => RasterFormat::Gif, + _ => bail!("Format not yet supported."), + }) + } +} + impl From for RasterFormat { fn from(format: ttf_parser::RasterImageFormat) -> Self { match format { diff --git a/tests/ref/visualize/image.png b/tests/ref/visualize/image.png index 90aa9b486..ec53fa980 100644 Binary files a/tests/ref/visualize/image.png and b/tests/ref/visualize/image.png differ diff --git a/tests/typ/visualize/image.typ b/tests/typ/visualize/image.typ index e3bcc64fb..60ce4f683 100644 --- a/tests/typ/visualize/image.typ +++ b/tests/typ/visualize/image.typ @@ -60,3 +60,23 @@ A #box(image("/files/tiger.jpg", height: 1cm, width: 80%)) B --- // Error: 2-25 failed to parse svg: found closing tag 'g' instead of 'style' in line 4 #image("/files/bad.svg") + +--- +// Test parsing from svg data +#image.decode(``.text, format: "svg") + +--- +// Error: 2-168 failed to parse svg: missing root node +#image.decode(``.text, format: "svg") + +--- +// Test format auto detect +#image.decode(read("/files/tiger.jpg", encoding: none), width: 80%) + +--- +// Test format manual +#image.decode(read("/files/tiger.jpg", encoding: none), format: "jpg", width: 80%) + +--- +// Error: 2-83 failed to decode image +#image.decode(read("/files/tiger.jpg", encoding: none), format: "png", width: 80%)