diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index b9ae69573..f1256f369 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -41,11 +41,11 @@ pub fn layout_image( if has_foreign_object { engine.sink.warn(warning!( - span, - "image contains foreign object"; - hint: "SVG images with foreign objects might render incorrectly in typst"; - hint: "see https://github.com/typst/typst/issues/1421 for more information" - )); + span, + "image contains foreign object"; + hint: "SVG images with foreign objects might render incorrectly in typst"; + hint: "see https://github.com/typst/typst/issues/1421 for more information" + )); } } diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 5c1a5b906..b6710fc93 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -9,7 +9,6 @@ pub use self::raster::{RasterFormat, RasterImage}; pub use self::svg::SvgImage; use std::fmt::{self, Debug, Formatter}; -use std::hash::Hash; use std::sync::Arc; use comemo::Tracked; diff --git a/crates/typst-library/src/visualize/image/pixmap.rs b/crates/typst-library/src/visualize/image/pixmap.rs index 41c75841b..6a474c37b 100644 --- a/crates/typst-library/src/visualize/image/pixmap.rs +++ b/crates/typst-library/src/visualize/image/pixmap.rs @@ -67,9 +67,9 @@ impl PixmapImage { &self.0.source.data } - /// Transform the image data into a [`DynamicImage`]. + /// Transform the image data to a [`DynamicImage`]. #[comemo::memoize] - pub fn to_image(&self) -> Arc { + pub fn to_dynamic(&self) -> Arc { // TODO: Optimize by returning a `View` if possible? fn decode>( source: &PixmapSource, diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs index c9a1125fa..1a045b468 100644 --- a/crates/typst-pdf/src/image.rs +++ b/crates/typst-pdf/src/image.rs @@ -130,14 +130,14 @@ pub fn deferred_image( Some(to_color_space(raster.dynamic().color())) } ImageKind::Pixmap(pixmap) if pixmap.icc_profile().is_none() => { - Some(to_color_space(pixmap.to_image().color())) + Some(to_color_space(pixmap.to_dynamic().color())) } _ => None, }; - // PDF/A does not appear to allow interpolation[^1]. - // [^1]: https://github.com/typst/typst/issues/2942 - let interpolate = image.scaling() == Smart::Custom(ImageScaling::Smooth) && !pdfa; + // PDF/A does not appear to allow interpolation. + // See https://github.com/typst/typst/issues/2942. + let interpolate = !pdfa && image.scaling() == Smart::Custom(ImageScaling::Smooth); let deferred = Deferred::new(move || match image.kind() { ImageKind::Raster(raster) => { @@ -159,7 +159,7 @@ pub fn deferred_image( Ok(EncodedImage::Svg(chunk, id)) } ImageKind::Pixmap(pixmap) => Ok(encode_raster_image( - &pixmap.to_image(), + &pixmap.to_dynamic(), pixmap.icc_profile(), EncodeFormat::Flate, interpolate, @@ -215,16 +215,6 @@ fn encode_raster_image( } } -/// Matches an [`image::ColorType`] to [`ColorSpace`]. -fn to_color_space(color: image::ColorType) -> ColorSpace { - use image::ColorType::*; - match color { - L8 | La8 | L16 | La16 => ColorSpace::D65Gray, - Rgb8 | Rgba8 | Rgb16 | Rgba16 | Rgb32F | Rgba32F => ColorSpace::Srgb, - _ => unimplemented!(), - } -} - /// Encode an image's alpha channel if present. #[typst_macros::time(name = "encode alpha")] fn encode_alpha(image: &DynamicImage) -> (Vec, Filter) { @@ -282,3 +272,13 @@ enum EncodeFormat { DctDecode, Flate, } + +/// Matches an [`image::ColorType`] to [`ColorSpace`]. +fn to_color_space(color: image::ColorType) -> ColorSpace { + use image::ColorType::*; + match color { + L8 | La8 | L16 | La16 => ColorSpace::D65Gray, + Rgb8 | Rgba8 | Rgb16 | Rgba16 | Rgb32F | Rgba32F => ColorSpace::Srgb, + _ => unimplemented!(), + } +} diff --git a/crates/typst-render/src/image.rs b/crates/typst-render/src/image.rs index 70697c900..02da5eddb 100644 --- a/crates/typst-render/src/image.rs +++ b/crates/typst-render/src/image.rs @@ -59,34 +59,35 @@ pub fn render_image( /// Prepare a texture for an image at a scaled size. #[comemo::memoize] fn build_texture(image: &Image, w: u32, h: u32) -> Option> { + let mut texture = sk::Pixmap::new(w, h)?; match image.kind() { - ImageKind::Raster(raster) => scale_image(raster.dynamic(), image.scaling(), w, h), - ImageKind::Pixmap(raster) => { - scale_image(&raster.to_image(), image.scaling(), w, h) + ImageKind::Raster(raster) => { + scale_image(&mut texture, raster.dynamic(), image.scaling()) + } + ImageKind::Pixmap(pixmap) => { + scale_image(&mut texture, &pixmap.to_dynamic(), image.scaling()) } - // Safety: We do not keep any references to tree nodes beyond the scope - // of `with`. ImageKind::Svg(svg) => { - let mut pixmap = sk::Pixmap::new(w, h)?; let tree = svg.tree(); let ts = tiny_skia::Transform::from_scale( w as f32 / tree.size().width(), h as f32 / tree.size().height(), ); - resvg::render(tree, ts, &mut pixmap.as_mut()); - Some(Arc::new(pixmap)) + resvg::render(tree, ts, &mut texture.as_mut()); } } + Some(Arc::new(texture)) } -/// Scale a rastered image to a given size and return texture. +/// Scale a rastered image to a given size and write it into the `texture`. fn scale_image( + texture: &mut sk::Pixmap, image: &image::DynamicImage, scaling: Smart, - w: u32, - h: u32, -) -> Option> { - let mut pixmap = sk::Pixmap::new(w, h)?; +) { + let w = texture.width(); + let h = texture.height(); + let buf; let resized = if (w, h) == (image.width(), image.height()) { // Small optimization to not allocate in case image is not resized. @@ -94,18 +95,16 @@ fn scale_image( } else { let upscale = w > image.width(); let filter = match scaling { - Smart::Auto | Smart::Custom(ImageScaling::Smooth) if upscale => { - FilterType::CatmullRom - } Smart::Custom(ImageScaling::Pixelated) => FilterType::Nearest, + _ if upscale => FilterType::CatmullRom, _ => FilterType::Lanczos3, // downscale }; buf = image.resize_exact(w, h, filter); &buf }; - for ((_, _, src), dest) in resized.pixels().zip(pixmap.pixels_mut()) { + + for ((_, _, src), dest) in resized.pixels().zip(texture.pixels_mut()) { let Rgba([r, g, b, a]) = src; *dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply(); } - Some(Arc::new(pixmap)) } diff --git a/crates/typst-svg/src/image.rs b/crates/typst-svg/src/image.rs index 1f099336b..6c90b115b 100644 --- a/crates/typst-svg/src/image.rs +++ b/crates/typst-svg/src/image.rs @@ -1,8 +1,5 @@ -use std::io::Cursor; - use base64::Engine; use ecow::{eco_format, EcoString}; -use image::error::UnsupportedError; use image::{codecs::png::PngEncoder, ImageEncoder}; use typst_library::foundations::Smart; use typst_library::layout::{Abs, Axes}; @@ -24,8 +21,8 @@ impl SVGRenderer { match image.scaling() { Smart::Auto => {} Smart::Custom(ImageScaling::Smooth) => { - // This is still experimental and not implemented in all major browsers[^1]. - // [^1]: https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility + // This is still experimental and not implemented in all major browsers. + // https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility self.xml.write_attribute("style", "image-rendering: smooth") } Smart::Custom(ImageScaling::Pixelated) => { @@ -51,20 +48,19 @@ pub fn convert_image_to_base64_url(image: &Image) -> EcoString { }, ImageFormat::Pixmap(_) => "png", }; - let data_owned; + + let mut buf; let data = match image.kind() { ImageKind::Raster(raster) => raster.data(), ImageKind::Svg(svg) => svg.data(), ImageKind::Pixmap(pixmap) => { - let mut data = Cursor::new(vec![]); - let mut encoder = PngEncoder::new(&mut data); + buf = vec![]; + let mut encoder = PngEncoder::new(&mut buf); if let Some(icc_profile) = pixmap.icc_profile() { - let _: Result<(), UnsupportedError> = - encoder.set_icc_profile(icc_profile.to_vec()); + encoder.set_icc_profile(icc_profile.to_vec()).ok(); } - pixmap.to_image().write_with_encoder(encoder).unwrap(); - data_owned = data.into_inner(); - &*data_owned + pixmap.to_dynamic().write_with_encoder(encoder).unwrap(); + buf.as_slice() } };