From 560e49b67ca738e2b2768d7eda3bccdb47570dcc Mon Sep 17 00:00:00 2001 From: Toon Verstraelen Date: Tue, 2 Sep 2025 16:17:48 +0200 Subject: [PATCH] Load linked bitmap images in SVG images (#6794) Co-authored-by: Laurenz --- Cargo.lock | 2 +- Cargo.toml | 2 +- crates/typst-library/src/diag.rs | 2 +- .../typst-library/src/visualize/image/mod.rs | 63 +++++--- .../typst-library/src/visualize/image/svg.rs | 152 +++++++++++++++++- tests/ref/image-svg-linked-jpg1.png | Bin 0 -> 650 bytes tests/ref/image-svg-linked-jpg2.png | Bin 0 -> 646 bytes tests/ref/image-svg-linked-many-formats.png | Bin 0 -> 4006 bytes tests/suite/visualize/image.typ | 87 ++++++++++ 9 files changed, 275 insertions(+), 33 deletions(-) create mode 100644 tests/ref/image-svg-linked-jpg1.png create mode 100644 tests/ref/image-svg-linked-jpg2.png create mode 100644 tests/ref/image-svg-linked-many-formats.png diff --git a/Cargo.lock b/Cargo.lock index 0930c4f5..99253a7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2975,7 +2975,7 @@ dependencies = [ [[package]] name = "typst-dev-assets" version = "0.13.1" -source = "git+https://github.com/typst/typst-dev-assets?rev=cb896b4#cb896b4ce786b8b33af15791da894327572fc488" +source = "git+https://github.com/typst/typst-dev-assets?rev=ed6b8b0#ed6b8b0e035097486c44a328bf331fad0bee96f6" [[package]] name = "typst-docs" diff --git a/Cargo.toml b/Cargo.toml index 1ab48f54..0214e0bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" } typst-timing = { path = "crates/typst-timing", version = "0.13.1" } typst-utils = { path = "crates/typst-utils", version = "0.13.1" } typst-assets = { git = "https://github.com/typst/typst-assets", rev = "fbf00f9" } -typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "cb896b4" } +typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "ed6b8b0" } arrayvec = "0.7.4" az = "1.2" base64 = "0.22" diff --git a/crates/typst-library/src/diag.rs b/crates/typst-library/src/diag.rs index 018f1616..e401c11e 100644 --- a/crates/typst-library/src/diag.rs +++ b/crates/typst-library/src/diag.rs @@ -593,7 +593,7 @@ pub type LoadResult = Result; /// [`FileId`]: typst_syntax::FileId #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct LoadError { - /// The position in the file at which the error occured. + /// The position in the file at which the error occurred. pos: ReportPos, /// Must contain a message formatted like this: `"failed to do thing (cause)"`. message: EcoString, diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index ceaca1bb..7f4c4192 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -265,14 +265,22 @@ impl Packed { ) .at(span)?, ), - ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg( - SvgImage::with_fonts( - loaded.data.clone(), - engine.world, - &families(styles).map(|f| f.as_str()).collect::>(), + ImageFormat::Vector(VectorFormat::Svg) => { + // Identify the SVG file in case contained hrefs need to be resolved. + let svg_file = match self.source.source { + DataSource::Path(ref path) => span.resolve_path(path).ok(), + DataSource::Bytes(_) => span.id(), + }; + ImageKind::Svg( + SvgImage::with_fonts_images( + loaded.data.clone(), + engine.world, + &families(styles).map(|f| f.as_str()).collect::>(), + svg_file, + ) + .within(loaded)?, ) - .within(loaded)?, - ), + } ImageFormat::Vector(VectorFormat::Pdf) => { let document = match PdfDocument::new(loaded.data.clone()) { Ok(doc) => doc, @@ -325,28 +333,37 @@ impl Packed { }; let Derived { source, derived: loaded } = &self.source; - if let DataSource::Path(path) = source { - let ext = std::path::Path::new(path.as_str()) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default() - .to_lowercase(); - - match ext.as_str() { - "png" => return Ok(ExchangeFormat::Png.into()), - "jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()), - "gif" => return Ok(ExchangeFormat::Gif.into()), - "svg" | "svgz" => return Ok(VectorFormat::Svg.into()), - "pdf" => return Ok(VectorFormat::Pdf.into()), - "webp" => return Ok(ExchangeFormat::Webp.into()), - _ => {} - } + if let DataSource::Path(path) = source + && let Some(format) = determine_format_from_path(path.as_str()) + { + return Ok(format); } Ok(ImageFormat::detect(&loaded.data).ok_or("unknown image format")?) } } +/// Derive the image format from the file extension of a path. +fn determine_format_from_path(path: &str) -> Option { + let ext = std::path::Path::new(path) + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default() + .to_lowercase(); + + match ext.as_str() { + // Raster formats + "png" => Some(ExchangeFormat::Png.into()), + "jpg" | "jpeg" => Some(ExchangeFormat::Jpg.into()), + "gif" => Some(ExchangeFormat::Gif.into()), + "webp" => Some(ExchangeFormat::Webp.into()), + // Vector formats + "svg" | "svgz" => Some(VectorFormat::Svg.into()), + "pdf" => Some(VectorFormat::Pdf.into()), + _ => None, + } +} + impl LocalName for Packed { const KEY: &'static str = "figure"; } diff --git a/crates/typst-library/src/visualize/image/svg.rs b/crates/typst-library/src/visualize/image/svg.rs index dab6eaa0..56e25ad4 100644 --- a/crates/typst-library/src/visualize/image/svg.rs +++ b/crates/typst-library/src/visualize/image/svg.rs @@ -2,13 +2,19 @@ use std::hash::{Hash, Hasher}; use std::sync::{Arc, Mutex}; use comemo::Tracked; +use ecow::{EcoString, eco_format}; use rustc_hash::FxHashMap; use siphasher::sip128::{Hasher128, SipHasher13}; +use typst_syntax::FileId; use crate::World; -use crate::diag::{LoadError, LoadResult, ReportPos, format_xml_like_error}; +use crate::diag::{FileError, LoadError, LoadResult, ReportPos, format_xml_like_error}; use crate::foundations::Bytes; use crate::layout::Axes; +use crate::visualize::VectorFormat; +use crate::visualize::image::raster::{ExchangeFormat, RasterFormat}; +use crate::visualize::image::{ImageFormat, determine_format_from_path}; + use crate::text::{ Font, FontBook, FontFlags, FontStretch, FontStyle, FontVariant, FontWeight, }; @@ -35,32 +41,47 @@ impl SvgImage { Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash: 0, tree }))) } - /// Decode an SVG image with access to fonts. + /// Decode an SVG image with access to fonts and linked images. #[comemo::memoize] #[typst_macros::time(name = "load svg")] - pub fn with_fonts( + pub fn with_fonts_images( data: Bytes, world: Tracked, families: &[&str], + svg_file: Option, ) -> LoadResult { let book = world.book(); - let resolver = Mutex::new(FontResolver::new(world, book, families)); + let font_resolver = Mutex::new(FontResolver::new(world, book, families)); + let image_resolver = Mutex::new(ImageResolver::new(world, svg_file)); let tree = usvg::Tree::from_data( &data, &usvg::Options { font_resolver: usvg::FontResolver { select_font: Box::new(|font, db| { - resolver.lock().unwrap().select_font(font, db) + font_resolver.lock().unwrap().select_font(font, db) }), select_fallback: Box::new(|c, exclude_fonts, db| { - resolver.lock().unwrap().select_fallback(c, exclude_fonts, db) + font_resolver.lock().unwrap().select_fallback( + c, + exclude_fonts, + db, + ) + }), + }, + image_href_resolver: usvg::ImageHrefResolver { + resolve_data: usvg::ImageHrefResolver::default_data_resolver(), + resolve_string: Box::new(|href, _opts| { + image_resolver.lock().unwrap().load(href) }), }, ..base_options() }, ) .map_err(format_usvg_error)?; - let font_hash = resolver.into_inner().unwrap().finish(); + if let Some(err) = image_resolver.into_inner().unwrap().error { + return Err(err); + } + let font_hash = font_resolver.into_inner().unwrap().finish(); Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash, tree }))) } @@ -287,3 +308,120 @@ impl FontResolver<'_> { Some(id) } } + +/// Resolves linked images in an SVG. +/// (Linked SVG images from an SVG are not supported yet.) +struct ImageResolver<'a> { + /// The world used to load linked images. + world: Tracked<'a, dyn World + 'a>, + /// Parent folder of the SVG file, used to resolve hrefs to linked images, if any. + svg_file: Option, + /// The first error that occurred when loading a linked image, if any. + error: Option, +} + +impl<'a> ImageResolver<'a> { + fn new(world: Tracked<'a, dyn World + 'a>, svg_file: Option) -> Self { + Self { world, svg_file, error: None } + } + + /// Load a linked image or return None if a previous image caused an error, + /// or if the linked image failed to load. + /// Only the first error message is retained. + fn load(&mut self, href: &str) -> Option { + if self.error.is_some() { + return None; + } + match self.load_or_error(href) { + Ok(image) => Some(image), + Err(err) => { + self.error = Some(LoadError::new( + ReportPos::None, + eco_format!("failed to load linked image {} in SVG", href), + err, + )); + None + } + } + } + + /// Load a linked image or return an error message string. + fn load_or_error(&mut self, href: &str) -> Result { + // If the href starts with "file://", strip this prefix to construct an ordinary path. + let href = href.strip_prefix("file://").unwrap_or(href); + + // Do not accept absolute hrefs. They would be parsed in typst in a way + // that is not compatible with their interpretation in the SVG standard. + if href.starts_with("/") { + return Err("absolute paths are not allowed".into()); + } + + // Exit early if the href is an URL. + if let Some(pos) = href.find("://") { + let scheme = &href[..pos]; + if scheme + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.') + { + return Err("URLs are not allowed".into()); + } + } + + // Resolve the path to the linked image. + if self.svg_file.is_none() { + return Err("cannot access file system from here".into()); + } + // Replace the file name in svg_file by href. + let href_file = self.svg_file.unwrap().join(href); + + // Load image if file can be accessed. + match self.world.file(href_file) { + Ok(bytes) => { + let arc_data = Arc::new(bytes.to_vec()); + let format = match determine_format_from_path(href) { + Some(format) => Some(format), + None => ImageFormat::detect(&arc_data), + }; + match format { + Some(ImageFormat::Vector(vector_format)) => match vector_format { + VectorFormat::Svg => { + Err("SVG images are not supported yet".into()) + } + VectorFormat::Pdf => { + Err("PDF documents are not supported".into()) + } + }, + Some(ImageFormat::Raster(raster_format)) => match raster_format { + RasterFormat::Exchange(exchange_format) => { + match exchange_format { + ExchangeFormat::Gif => Ok(usvg::ImageKind::GIF(arc_data)), + ExchangeFormat::Jpg => { + Ok(usvg::ImageKind::JPEG(arc_data)) + } + ExchangeFormat::Png => Ok(usvg::ImageKind::PNG(arc_data)), + ExchangeFormat::Webp => { + Ok(usvg::ImageKind::WEBP(arc_data)) + } + } + } + RasterFormat::Pixel(_) => { + Err("pixel formats are not supported".into()) + } + }, + None => Err("unknown image format".into()), + } + } + // TODO: Somehow unify this with `impl Display for FileError`. + Err(err) => Err(match err { + FileError::NotFound(path) => { + eco_format!("file not found, searched at {}", path.display()) + } + FileError::AccessDenied => "access denied".into(), + FileError::IsDirectory => "is a directory".into(), + FileError::Other(Some(msg)) => msg, + FileError::Other(None) => "unspecified error".into(), + _ => eco_format!("unexpected error: {}", err), + }), + } + } +} diff --git a/tests/ref/image-svg-linked-jpg1.png b/tests/ref/image-svg-linked-jpg1.png new file mode 100644 index 0000000000000000000000000000000000000000..c21846d62b1ffc13d39622829abe19d3505c309c GIT binary patch literal 650 zcmeAS@N?(olHy`uVBq!ia0vp^6+j%o!2~2@f2!SMU|>~V<%DVN2X-D^Bp{T3&t~;> z-~WvH{Bis5e`r+bipjps{P^ddpZ{3bAFjK9KmWsqMM|8498nj%E;Na45!$j~>Vn-O zvp9seEMV2maOB=KdtvoMtvjuW68z~B?+%E*5G{Fq!CZk`{%|FK=7|HJ3ydbTG1W9E zr99lVV0P=f#^oPa?;Ub}vDmTp14r6IE=>#N7xj(vcskh^wlA}m?y7I~bd+bVmEL!l zAy|So;eG0=eCG{@VcA8ZVdlkcb7Fnm@%#u5m-EZIjez9Wraj9KA z;*D+>t|fil)!%bOs*3NNJA-W0aj9Q&0=!#h@;N(lM}gcAcJy)XU4>13FYKMm#i71H z$bpnyTRdsOS062)@1Wv$TQ4v-^|Afpjp<<971nL~MSX`1N#SPNt>R zWSr=7wYIq;clXZO4^k-yf;Hu(Kd;o(7SWl%{piufC!enk_)v1|`0?7LLte}HL>!PC{xWt~$(699oFCYJyJ literal 0 HcmV?d00001 diff --git a/tests/ref/image-svg-linked-jpg2.png b/tests/ref/image-svg-linked-jpg2.png new file mode 100644 index 0000000000000000000000000000000000000000..05748d82d53e2c26d591b387346122b0c1f11ecf GIT binary patch literal 646 zcmeAS@N?(olHy`uVBq!ia0vp^6+j%o!2~2@f2!SMU|{m~ba4!+xb^nVe(x)ZG6z0> z7ZJ@|E6BO^nv|)FiAZPoG<$~t&9iOA7MnOSClpLOGC8kk!WqSY^9Edsg~i2T(}Vvr zZa@6B^3eN93U^B9lq-C%`}~tT?(pZ@dCyOOKizayRYf2wLpUm9p)U}SI`T! z3Re4v!Zz(-#RC&+{)Fz9jqwZGuZS!it%SU6qyW4wV%3FnLU5A3%V{0iE@#ouDb z`IphCpfC8f&7RipPV(*ZT0~4=O4f*3HKy(S@>nnI`oFq2FFOs7v9+H&T)M!dWW~C> zcQ4$%`{IId!B(ahT5F_besKmAzfidjb}p2V=H6A~bp7Sr#GXK8VW59N+*{tN3-_vt zLOci*u$z1#*XcU@FXfok%q0siytr`xVcOb-FY=@H_tRpM{0Q^5uu0En}ays{g^!q#L)m zUcXkH&;R+e`{eWOTeoi5E8FGj-*&H*LwCysCvM#>_KiTV0;3Zey}1wdB^(y;?lbyO Q2uxoLp00i_>zopr03EX?EC2ui literal 0 HcmV?d00001 diff --git a/tests/ref/image-svg-linked-many-formats.png b/tests/ref/image-svg-linked-many-formats.png new file mode 100644 index 0000000000000000000000000000000000000000..86bcdc51252113851a1df55c5c90944edfe2ec4d GIT binary patch literal 4006 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 M07*qoM6N<$g0j`z<^TWy literal 0 HcmV?d00001 diff --git a/tests/suite/visualize/image.typ b/tests/suite/visualize/image.typ index 97736622..e2b7fbd1 100644 --- a/tests/suite/visualize/image.typ +++ b/tests/suite/visualize/image.typ @@ -79,6 +79,93 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B ```.text )) +--- image-svg-linked-jpg1 --- +#set page(fill: gray) +#image(bytes( + ``` + + + + + ```.text +)) + +--- image-svg-linked-jpg2 --- +#set page(fill: gray) +#image(bytes( + ``` + + + + + ```.text +)) + +--- image-svg-linked-many-formats --- +#set page(width: auto, height: auto, margin: 1pt) +#set text(1pt) +#image("../../../assets/images/linked.svg", width: 39pt) + +--- image-svg-linked-file-not-found --- +// Error: 8-7:2 failed to load linked image do-not-add-image-with-this-name.png in SVG (file not found, searched at tests/suite/visualize/do-not-add-image-with-this-name.png) +#image(bytes( + ``` + + + + ```.text +)) + +--- image-svg-linked-url --- +// Error: 8-7:2 failed to load linked image https://somedomain.com/image.png in SVG (URLs are not allowed) +#image(bytes( + ``` + + + + ```.text +)) + +--- image-svg-linked-pdf --- +// Error: 8-7:2 failed to load linked image ../../../assets/images/diagrams.pdf in SVG (PDF documents are not supported) +#image(bytes( + ``` + + + + ```.text +)) + +--- image-svg-linked-csv --- +// Error: 8-7:2 failed to load linked image ../../../assets/data/bad.csv in SVG (unknown image format) +#image(bytes( + ``` + + + + ```.text +)) + +--- image-svg-linked-absolute1 --- +// Error: 8-7:2 failed to load linked image /home/user/foo.svg in SVG (absolute paths are not allowed) +#image(bytes( + ``` + + + + ```.text +)) + +--- image-svg-linked-absolute2 --- +// Error: 8-7:2 failed to load linked image file:///home/user/foo.svg in SVG (absolute paths are not allowed) +#image(bytes( + ``` + + + + ```.text +)) + --- image-pixmap-rgb8 --- #image( bytes((