From 3862102398e825f5aa0c3d40dd323ded4b473103 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 30 Jan 2025 15:14:45 +0100 Subject: [PATCH] Refactor --- crates/typst-layout/src/image.rs | 94 +++++---- crates/typst-library/src/text/font/color.rs | 28 +-- .../typst-library/src/visualize/image/mod.rs | 182 +++++++----------- .../src/visualize/image/pixmap.rs | 81 ++++---- .../src/visualize/image/raster.rs | 1 + .../typst-library/src/visualize/image/svg.rs | 81 ++++---- crates/typst-pdf/src/image.rs | 3 +- crates/typst-render/src/image.rs | 10 +- crates/typst-svg/src/image.rs | 7 +- crates/typst-svg/src/text.rs | 15 +- tests/suite/visualize/image.typ | 22 +-- 11 files changed, 246 insertions(+), 278 deletions(-) diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index 69a72e750..b9ae69573 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -10,8 +10,8 @@ use typst_library::layout::{ use typst_library::loading::DataSource; use typst_library::text::families; use typst_library::visualize::{ - Curve, Image, ImageElem, ImageFit, ImageFormat, ImageOptions, ImageSource, - RasterFormat, VectorFormat, + Curve, Image, ImageElem, ImageFit, ImageFormat, ImageKind, ImageSource, PixmapImage, + RasterFormat, RasterImage, SvgImage, VectorFormat, }; /// Layout the image. @@ -28,47 +28,51 @@ pub fn layout_image( // Take the format that was explicitly defined, or parse the extension, // or try to detect the format. let Derived { source, derived: data } = &elem.source; - let format = match (elem.format(styles), source) { - (Smart::Custom(v), _) => v, - (Smart::Auto, ImageSource::Readable(data)) => { - determine_format(elem.path().as_str(), data).at(span)? - } - (Smart::Auto, ImageSource::Pixmap(_)) => { - bail!(span, "pixmaps require an explicit image format to be given"); - } + let format = match elem.format(styles) { + Smart::Custom(v) => v, + Smart::Auto => determine_format(source, data).at(span)?, }; // Warn the user if the image contains a foreign object. Not perfect // because the svg could also be encoded, but that's an edge case. - if let ImageSource::Readable(data) = source { - if format == ImageFormat::Vector(VectorFormat::Svg) { - let has_foreign_object = - data.as_str().is_some_and(|s| s.contains(">(), - flatten_text: elem.flatten_text(styles), - scaling: elem.scaling(styles), - }, - ) - .at(span)?; + let kind = match (format, source) { + (ImageFormat::Pixmap(format), ImageSource::Pixmap(source)) => { + ImageKind::Pixmap(PixmapImage::new(source.clone(), format).at(span)?) + } + (ImageFormat::Raster(format), ImageSource::Data(_)) => { + ImageKind::Raster(RasterImage::new(data.clone(), format).at(span)?) + } + (ImageFormat::Vector(VectorFormat::Svg), ImageSource::Data(_)) => ImageKind::Svg( + SvgImage::with_fonts( + data.clone(), + engine.world, + elem.flatten_text(styles), + &families(styles).map(|f| f.as_str()).collect::>(), + ) + .at(span)?, + ), + (ImageFormat::Pixmap(_), _) => bail!(span, "source must be a pixmap"), + (ImageFormat::Raster(_) | ImageFormat::Vector(_), _) => { + bail!(span, "expected readable source for the given format (str or bytes)") + } + }; + + let image = Image::new(kind, elem.alt(styles), elem.scaling(styles)); // Determine the image's pixel aspect ratio. let pxw = image.width(); @@ -131,20 +135,26 @@ pub fn layout_image( } /// Try to determine the image format based on the data. -fn determine_format(source: &DataSource, data: &Bytes) -> StrResult { - 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(); +fn determine_format(source: &ImageSource, data: &Bytes) -> StrResult { + match source { + ImageSource::Data(DataSource::Path(path)) => { + 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(ImageFormat::Raster(RasterFormat::Png)), - "jpg" | "jpeg" => return Ok(ImageFormat::Raster(RasterFormat::Jpg)), - "gif" => return Ok(ImageFormat::Raster(RasterFormat::Gif)), - "svg" | "svgz" => return Ok(ImageFormat::Vector(VectorFormat::Svg)), - _ => {} + match ext.as_str() { + "png" => return Ok(ImageFormat::Raster(RasterFormat::Png)), + "jpg" | "jpeg" => return Ok(ImageFormat::Raster(RasterFormat::Jpg)), + "gif" => return Ok(ImageFormat::Raster(RasterFormat::Gif)), + "svg" | "svgz" => return Ok(ImageFormat::Vector(VectorFormat::Svg)), + _ => {} + } + } + ImageSource::Data(DataSource::Bytes(_)) => {} + ImageSource::Pixmap(_) => { + bail!("pixmaps require an explicit image format to be given") } } diff --git a/crates/typst-library/src/text/font/color.rs b/crates/typst-library/src/text/font/color.rs index e2487e316..93bce1dfd 100644 --- a/crates/typst-library/src/text/font/color.rs +++ b/crates/typst-library/src/text/font/color.rs @@ -7,12 +7,11 @@ use typst_syntax::Span; use usvg::tiny_skia_path; use xmlwriter::XmlWriter; -use crate::foundations::Bytes; +use crate::foundations::{Bytes, Smart}; use crate::layout::{Abs, Frame, FrameItem, Point, Size}; -use crate::loading::Readable; use crate::text::{Font, Glyph}; use crate::visualize::{ - FixedStroke, Geometry, Image, ImageSource, RasterFormat, VectorFormat, + FixedStroke, Geometry, Image, RasterFormat, RasterImage, SvgImage, }; /// Whether this glyph should be rendered via simple outlining instead of via @@ -105,12 +104,9 @@ fn draw_raster_glyph( upem: Abs, raster_image: ttf_parser::RasterGlyphImage, ) -> Option<()> { - let image = Image::new( - Bytes::new(raster_image.data).to_vec(), - RasterFormat::Png.into(), - &Default::default(), - ) - .ok()?; + let data = Bytes::new(raster_image.data.to_vec()); + let image = + Image::new(RasterImage::new(data, RasterFormat::Png).ok()?, None, Smart::Auto); // Apple Color emoji doesn't provide offset information (or at least // not in a way ttf-parser understands), so we artificially shift their @@ -181,14 +177,8 @@ fn draw_colr_glyph( ttf.paint_color_glyph(glyph_id, 0, RgbaColor::new(0, 0, 0, 255), &mut glyph_painter)?; svg.end_element(); - let data = svg.end_document().into_bytes(); - - let image = Image::new( - Bytes::new(data).into(), - VectorFormat::Svg.into(), - &Default::default(), - ) - .ok()?; + let data = Bytes::from_string(svg.end_document()); + let image = Image::new(SvgImage::new(data).ok()?, None, Smart::Auto); let y_shift = Abs::pt(upem.to_pt() - y_max); let position = Point::new(Abs::pt(x_min), y_shift); @@ -263,8 +253,8 @@ fn draw_svg_glyph( ty = -top, ); - let source = ImageSource::Readable(Readable::Str(wrapper_svg.into())); - let image = Image::new(source, VectorFormat::Svg.into(), &Default::default()).ok()?; + let data = Bytes::from_string(wrapper_svg); + let image = Image::new(SvgImage::new(data).ok()?, None, Smart::Auto); let position = Point::new(Abs::pt(left), Abs::pt(top) + upem); let size = Size::new(Abs::pt(width), Abs::pt(height)); diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 0e62bd616..5c1a5b906 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -4,6 +4,7 @@ mod pixmap; mod raster; mod svg; +pub use self::pixmap::{PixmapFormat, PixmapImage, PixmapSource}; pub use self::raster::{RasterFormat, RasterImage}; pub use self::svg::SvgImage; @@ -13,15 +14,14 @@ use std::sync::Arc; use comemo::Tracked; use ecow::EcoString; -use pixmap::{Pixmap, PixmapFormat, PixmapSource}; use typst_syntax::{Span, Spanned}; use typst_utils::LazyHash; -use crate::diag::{bail, SourceResult, StrResult}; +use crate::diag::{SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, func, scope, AutoValue, Bytes, Cast, Content, Derived, Dict, NativeElement, - Packed, Show, Smart, StyleChain, Value, + 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}; @@ -97,9 +97,9 @@ pub struct ImageElem { /// A hint to the viewer how it should scale the image. /// - /// **Note:** This option may be ignored and results look different - /// depending on the format and viewer. - pub scaling: ImageScaling, + /// _Note:_ This option may be ignored and results look different depending + /// on the format and viewer. + pub scaling: Smart, } #[scope] @@ -145,10 +145,11 @@ impl ImageElem { flatten_text: Option, /// How the image should be scaled by the viewer. #[named] - scaling: Option, + scaling: Option>, ) -> StrResult { let bytes = data.into_bytes(); - let source = Derived::new(ImageSource::DataSource(DataSource::Bytes(bytes.clone())), bytes); + let source = + Derived::new(ImageSource::Data(DataSource::Bytes(bytes.clone())), bytes); let mut elem = ImageElem::new(source); if let Some(format) = format { elem.push_format(format); @@ -225,7 +226,7 @@ struct Repr { /// A text describing the image. alt: Option, /// The scaling algorithm to use. - scaling: ImageScaling, + scaling: Smart, } impl Image { @@ -236,40 +237,24 @@ impl Image { /// 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 source and a format. - #[comemo::memoize] - #[typst_macros::time(name = "load image")] + /// Create an image from a kind. pub fn new( - data: Derived, - format: ImageFormat, - options: &ImageOptions, - ) -> StrResult { - let kind = match format { - ImageFormat::Raster(format) => { - let ImageSource::DataSource(_) = data.source else { - bail!("expected non-pixmap source for the given format"); - }; - ImageKind::Raster(RasterImage::new(data.derived, format)?) - } - ImageFormat::Vector(VectorFormat::Svg) => { - let ImageSource::DataSource(_) = data.source else { - bail!("expected non-pixmap source for the given format"); - }; - ImageKind::Svg(SvgImage::new(data.derived, options)?) - } - ImageFormat::Pixmap(format) => { - let ImageSource::Pixmap(source) = data.source else { - bail!("source must be a pixmap"); - }; - ImageKind::Pixmap(Pixmap::new(source, format)?) - } - }; + kind: impl Into, + alt: Option, + scaling: Smart, + ) -> Self { + Self::new_impl(kind.into(), alt, scaling) + } - Ok(Self(Arc::new(LazyHash::new(Repr { - kind, - alt: options.alt.clone(), - scaling: options.scaling, - })))) + /// 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. @@ -314,7 +299,7 @@ impl Image { } /// The image scaling algorithm to use for this image. - pub fn scaling(&self) -> ImageScaling { + pub fn scaling(&self) -> Smart { self.0.scaling } @@ -339,13 +324,13 @@ impl Debug for Image { /// Information specifying the source of an image's byte data. #[derive(Debug, Clone, PartialEq, Hash)] pub enum ImageSource { - DataSource(DataSource), - Pixmap(Arc), + Data(DataSource), + Pixmap(PixmapSource), } impl From for ImageSource { fn from(bytes: Bytes) -> Self { - ImageSource::DataSource(DataSource::Bytes(bytes)) + ImageSource::Data(DataSource::Bytes(bytes)) } } @@ -354,8 +339,8 @@ impl Load for Spanned { fn load(&self, world: Tracked) -> SourceResult { match &self.v { - ImageSource::DataSource(data_source) => Spanned::new(data_source, self.span).load(world), - ImageSource::Pixmap(pixmap_source) => Ok(pixmap_source.data.clone()), + ImageSource::Data(data) => Spanned::new(data, self.span).load(world), + ImageSource::Pixmap(pixmap) => Ok(pixmap.data.clone()), } } } @@ -363,20 +348,40 @@ impl Load for Spanned { cast! { ImageSource, self => match self { - Self::DataSource(data) => data.into_value(), + Self::Data(data) => data.into_value(), Self::Pixmap(pixmap) => pixmap.into_value(), }, - data: DataSource => ImageSource::DataSource(data), - mut dict: Dict => { - let source = ImageSource::Pixmap(Arc::new(PixmapSource { - data: dict.take("data")?.cast()?, - pixel_width: dict.take("pixel-width")?.cast()?, - pixel_height: dict.take("pixel-height")?.cast()?, - icc_profile: dict.take("icc-profile").ok().map(|value| value.cast()).transpose()?, - })); - dict.finish(&["data", "pixel-width", "pixel-height", "icc-profile"])?; - source - }, + data: DataSource => Self::Data(data), + pixmap: PixmapSource => Self::Pixmap(pixmap), +} + +/// A kind of image. +#[derive(Clone, Hash)] +pub enum ImageKind { + /// A raster image. + Raster(RasterImage), + /// An SVG image. + Svg(SvgImage), + /// An image constructed from a pixmap. + Pixmap(PixmapImage), +} + +impl From for ImageKind { + fn from(image: RasterImage) -> Self { + Self::Raster(image) + } +} + +impl From for ImageKind { + fn from(image: SvgImage) -> Self { + Self::Svg(image) + } +} + +impl From for ImageKind { + fn from(image: PixmapImage) -> Self { + Self::Pixmap(image) + } } /// A raster or vector image format. @@ -444,66 +449,11 @@ cast! { } /// The image scaling algorithm a viewer should use. -#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum ImageScaling { - /// Use the default scaling algorithm. - #[default] - Auto, /// Scale photos with a smoothing algorithm such as bilinear interpolation. Smooth, /// Scale with nearest neighbor or similar to preserve the pixelated look /// of the image. Pixelated, } - -cast! { - ImageScaling, - self => match self { - ImageScaling::Auto => Value::Auto, - ImageScaling::Pixelated => "pixelated".into_value(), - ImageScaling::Smooth => "smooth".into_value(), - }, - _: AutoValue => ImageScaling::Auto, - "pixelated" => ImageScaling::Pixelated, - "smooth" => ImageScaling::Smooth, -} - -/// A kind of image. -#[derive(Hash)] -pub enum ImageKind { - /// A raster image. - Raster(RasterImage), - /// An SVG image. - Svg(SvgImage), - /// An image constructed from a pixmap. - Pixmap(Pixmap), -} - -pub struct ImageOptions<'a> { - pub alt: Option, - pub scaling: ImageScaling, - pub world: Option>, - pub families: &'a [&'a str], - pub flatten_text: bool, -} - -impl Default for ImageOptions<'_> { - fn default() -> Self { - ImageOptions { - alt: None, - scaling: ImageScaling::Auto, - world: None, - families: &[], - flatten_text: false, - } - } -} - -impl Hash for ImageOptions<'_> { - fn hash(&self, state: &mut H) { - self.alt.hash(state); - self.scaling.hash(state); - self.families.hash(state); - self.flatten_text.hash(state); - } -} diff --git a/crates/typst-library/src/visualize/image/pixmap.rs b/crates/typst-library/src/visualize/image/pixmap.rs index 895d908ce..ec35741a9 100644 --- a/crates/typst-library/src/visualize/image/pixmap.rs +++ b/crates/typst-library/src/visualize/image/pixmap.rs @@ -5,44 +5,22 @@ use image::{DynamicImage, ImageBuffer, Pixel}; use crate::diag::{bail, StrResult}; use crate::foundations::{cast, dict, Bytes, Cast, Dict}; -#[derive(Debug, Clone, PartialEq, Hash)] -pub struct PixmapSource { - pub data: Bytes, - pub pixel_width: u32, - pub pixel_height: u32, - pub icc_profile: Option, -} - -cast! { - Arc, - self => dict!("data" => self.data.clone(), "pixel_width" => self.pixel_width, "pixel_height" => self.pixel_height, "icc_profile" => self.icc_profile.clone()).into_value(), - mut dict: Dict => { - let source = Arc::new(PixmapSource { - data: dict.take("data")?.cast()?, - pixel_width: dict.take("pixel-width")?.cast()?, - pixel_height: dict.take("pixel-height")?.cast()?, - icc_profile: dict.take("icc-profile").ok().map(|value| value.cast()).transpose()?, - }); - dict.finish(&["data", "pixel-width", "pixel-height", "icc-profile"])?; - source - } -} - /// A raster image based on a flat pixmap. #[derive(Clone, Hash)] -pub struct Pixmap(Arc); +pub struct PixmapImage(Arc); /// The internal representation. #[derive(Hash)] struct Repr { - source: Arc, + source: PixmapSource, format: PixmapFormat, } -impl Pixmap { +impl PixmapImage { /// Build a new [`Pixmap`] from a flat, uncompressed byte sequence. #[comemo::memoize] - pub fn new(source: Arc, format: PixmapFormat) -> StrResult { + #[typst_macros::time(name = "load pixmap")] + pub fn new(source: PixmapSource, format: PixmapFormat) -> StrResult { if source.pixel_width == 0 || source.pixel_height == 0 { bail!("zero-sized images are not allowed"); } @@ -53,6 +31,7 @@ impl Pixmap { PixmapFormat::Luma8 => 1, PixmapFormat::Lumaa8 => 2, }; + let Some(expected_size) = source .pixel_width .checked_mul(source.pixel_height) @@ -60,6 +39,7 @@ impl Pixmap { else { bail!("provided pixel dimensions are too large"); }; + if expected_size as usize != source.data.len() { bail!("provided pixel dimensions and pixmap data do not match"); } @@ -83,14 +63,14 @@ impl Pixmap { } /// The raw data encoded in the given format. - pub fn data(&self) -> &[u8] { - self.0.source.data.as_slice() + pub fn data(&self) -> &Bytes { + &self.0.source.data } - /// Transform the image data into an [`DynamicImage`]. + /// Transform the image data into a [`DynamicImage`]. #[comemo::memoize] pub fn to_image(&self) -> Arc { - // TODO optimize by returning a `View` if possible? + // TODO: Optimize by returning a `View` if possible? fn decode>( source: &PixmapSource, ) -> ImageBuffer> { @@ -118,13 +98,42 @@ impl Pixmap { /// Determines how the given image is interpreted and encoded. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum PixmapFormat { - /// The red, green, and blue channels are each eight bit integers. - /// There is no alpha channel. + /// Red, green, and blue channels, one byte per channel. + /// No alpha channel. Rgb8, - /// The red, green, blue, and alpha channels are each eight bit integers. + /// Red, green, blue, and alpha channels, one byte per channel. Rgba8, - /// A single eight bit channel representing brightness. + /// A single byte channel representing brightness. Luma8, - /// One byte of brightness, another for alpha. + /// Brightness and alpha, one byte per channel. Lumaa8, } + +/// Raw pixmap data and relevant metadata. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct PixmapSource { + pub data: Bytes, + pub pixel_width: u32, + pub pixel_height: u32, + pub icc_profile: Option, +} + +cast! { + PixmapSource, + self => dict! { + "data" => self.data.clone(), + "pixel-width" => self.pixel_width, + "pixel-height" => self.pixel_height, + "icc-profile" => self.icc_profile.clone() + }.into_value(), + mut dict: Dict => { + let source = PixmapSource { + data: dict.take("data")?.cast()?, + pixel_width: dict.take("pixel-width")?.cast()?, + pixel_height: dict.take("pixel-height")?.cast()?, + icc_profile: dict.take("icc-profile").ok().map(|v| v.cast()).transpose()?, + }; + dict.finish(&["data", "pixel-width", "pixel-height", "icc-profile"])?; + source + } +} diff --git a/crates/typst-library/src/visualize/image/raster.rs b/crates/typst-library/src/visualize/image/raster.rs index c425d96d4..08fd11c4a 100644 --- a/crates/typst-library/src/visualize/image/raster.rs +++ b/crates/typst-library/src/visualize/image/raster.rs @@ -28,6 +28,7 @@ struct Repr { impl RasterImage { /// Decode a raster image. #[comemo::memoize] + #[typst_macros::time(name = "load raster image")] pub fn new(data: Bytes, format: RasterFormat) -> StrResult { fn decode_with( decoder: ImageResult, diff --git a/crates/typst-library/src/visualize/image/svg.rs b/crates/typst-library/src/visualize/image/svg.rs index 6fee5995a..dcc55077b 100644 --- a/crates/typst-library/src/visualize/image/svg.rs +++ b/crates/typst-library/src/visualize/image/svg.rs @@ -14,8 +14,6 @@ use crate::text::{ }; use crate::World; -use super::ImageOptions; - /// A decoded SVG. #[derive(Clone, Hash)] pub struct SvgImage(Arc); @@ -30,48 +28,53 @@ struct Repr { } impl SvgImage { - /// Decode an SVG image. + /// Decode an SVG image without fonts. #[comemo::memoize] - pub fn new(data: Bytes, options: &ImageOptions) -> StrResult { - let (tree, font_hash) = match options.world { - Some(world) => { - let book = world.book(); - let resolver = - Mutex::new(FontResolver::new(world, book, options.families)); - 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) - }), - select_fallback: Box::new(|c, exclude_fonts, db| { - resolver.lock().unwrap().select_fallback( - c, - exclude_fonts, - db, - ) - }), - }, - ..base_options() - }, - ) - .map_err(format_usvg_error)?; - let font_hash = resolver.into_inner().unwrap().finish(); - (tree, font_hash) - } - None => { - let tree = usvg::Tree::from_data(&data, &base_options()) - .map_err(format_usvg_error)?; - let font_hash = 0; - (tree, font_hash) - } - }; + #[typst_macros::time(name = "load svg")] + pub fn new(data: Bytes) -> StrResult { + let tree = + usvg::Tree::from_data(&data, &base_options()).map_err(format_usvg_error)?; + Ok(Self(Arc::new(Repr { + data, + size: tree_size(&tree), + font_hash: 0, + flatten_text: false, + tree, + }))) + } + + /// Decode an SVG image with access to fonts. + #[comemo::memoize] + #[typst_macros::time(name = "load svg")] + pub fn with_fonts( + data: Bytes, + world: Tracked, + flatten_text: bool, + families: &[&str], + ) -> StrResult { + let book = world.book(); + let resolver = Mutex::new(FontResolver::new(world, book, families)); + 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) + }), + select_fallback: Box::new(|c, exclude_fonts, db| { + resolver.lock().unwrap().select_fallback(c, exclude_fonts, db) + }), + }, + ..base_options() + }, + ) + .map_err(format_usvg_error)?; + let font_hash = resolver.into_inner().unwrap().finish(); Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash, - flatten_text: options.flatten_text, + flatten_text, tree, }))) } diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs index 1491c0888..c9a1125fa 100644 --- a/crates/typst-pdf/src/image.rs +++ b/crates/typst-pdf/src/image.rs @@ -5,6 +5,7 @@ use ecow::eco_format; use image::{DynamicImage, GenericImageView, Rgba}; use pdf_writer::{Chunk, Filter, Finish, Ref}; use typst_library::diag::{At, SourceResult, StrResult}; +use typst_library::foundations::Smart; use typst_library::visualize::{ ColorSpace, Image, ImageKind, ImageScaling, RasterFormat, SvgImage, }; @@ -136,7 +137,7 @@ pub fn deferred_image( // PDF/A does not appear to allow interpolation[^1]. // [^1]: https://github.com/typst/typst/issues/2942 - let interpolate = image.scaling() == ImageScaling::Smooth && !pdfa; + let interpolate = image.scaling() == Smart::Custom(ImageScaling::Smooth) && !pdfa; let deferred = Deferred::new(move || match image.kind() { ImageKind::Raster(raster) => { diff --git a/crates/typst-render/src/image.rs b/crates/typst-render/src/image.rs index ee8ee4822..70697c900 100644 --- a/crates/typst-render/src/image.rs +++ b/crates/typst-render/src/image.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use image::imageops::FilterType; use image::{GenericImageView, Rgba}; use tiny_skia as sk; +use typst_library::foundations::Smart; use typst_library::layout::Size; use typst_library::visualize::{Image, ImageKind, ImageScaling}; @@ -81,7 +82,7 @@ fn build_texture(image: &Image, w: u32, h: u32) -> Option> { /// Scale a rastered image to a given size and return texture. fn scale_image( image: &image::DynamicImage, - scaling: ImageScaling, + scaling: Smart, w: u32, h: u32, ) -> Option> { @@ -93,9 +94,10 @@ fn scale_image( } else { let upscale = w > image.width(); let filter = match scaling { - ImageScaling::Auto if upscale => FilterType::CatmullRom, - ImageScaling::Smooth if upscale => FilterType::CatmullRom, - ImageScaling::Pixelated => FilterType::Nearest, + Smart::Auto | Smart::Custom(ImageScaling::Smooth) if upscale => { + FilterType::CatmullRom + } + Smart::Custom(ImageScaling::Pixelated) => FilterType::Nearest, _ => FilterType::Lanczos3, // downscale }; buf = image.resize_exact(w, h, filter); diff --git a/crates/typst-svg/src/image.rs b/crates/typst-svg/src/image.rs index c5523b8b0..1f099336b 100644 --- a/crates/typst-svg/src/image.rs +++ b/crates/typst-svg/src/image.rs @@ -4,6 +4,7 @@ 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}; use typst_library::visualize::{ Image, ImageFormat, ImageKind, ImageScaling, RasterFormat, VectorFormat, @@ -21,13 +22,13 @@ impl SVGRenderer { self.xml.write_attribute("height", &size.y.to_pt()); self.xml.write_attribute("preserveAspectRatio", "none"); match image.scaling() { - ImageScaling::Auto => {} - ImageScaling::Smooth => { + 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 self.xml.write_attribute("style", "image-rendering: smooth") } - ImageScaling::Pixelated => { + Smart::Custom(ImageScaling::Pixelated) => { self.xml.write_attribute("style", "image-rendering: pixelated") } } diff --git a/crates/typst-svg/src/text.rs b/crates/typst-svg/src/text.rs index 36e67a161..7b78e6de3 100644 --- a/crates/typst-svg/src/text.rs +++ b/crates/typst-svg/src/text.rs @@ -3,10 +3,12 @@ use std::io::Read; use base64::Engine; use ecow::EcoString; use ttf_parser::GlyphId; -use typst_library::foundations::Bytes; +use typst_library::foundations::{Bytes, Smart}; use typst_library::layout::{Abs, Point, Ratio, Size, Transform}; use typst_library::text::{Font, TextItem}; -use typst_library::visualize::{FillRule, Image, Paint, RasterFormat, RelativeTo}; +use typst_library::visualize::{ + FillRule, Image, Paint, RasterFormat, RasterImage, RelativeTo, +}; use typst_utils::hash128; use crate::{SVGRenderer, State, SvgMatrix, SvgPathBuilder}; @@ -245,11 +247,10 @@ fn convert_bitmap_glyph_to_image(font: &Font, id: GlyphId) -> Option<(Image, f64 return None; } let image = Image::new( - Bytes::new(raster.data.to_vec()).into(), - RasterFormat::Png.into(), - &Default::default(), - ) - .ok()?; + RasterImage::new(Bytes::new(raster.data.to_vec()), RasterFormat::Png).ok()?, + None, + Smart::Auto, + ); Some((image, raster.x as f64, raster.y as f64)) } diff --git a/tests/suite/visualize/image.typ b/tests/suite/visualize/image.typ index 80e48f940..7869da556 100644 --- a/tests/suite/visualize/image.typ +++ b/tests/suite/visualize/image.typ @@ -66,7 +66,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B ) --- image-pixmap-rgb8 --- -#image.decode( +#image( ( data: bytes(( 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, @@ -81,7 +81,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B ) --- image-pixmap-rgba8 --- -#image.decode( +#image( ( data: bytes(( 0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0xFF, @@ -96,7 +96,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B ) --- image-pixmap-luma8 --- -#image.decode( +#image( ( data: bytes(range(16).map(x => x * 16)), pixel-width: 4, @@ -107,7 +107,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B ) --- image-pixmap-lumaa8 --- -#image.decode( +#image( ( data: bytes(range(16).map(x => (0x80, x * 16)).flatten()), pixel-width: 4, @@ -118,7 +118,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B ) --- image-scaling-methods --- -#let img(scaling) = image.decode( +#let img(scaling) = image( ( data: bytes(( 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, @@ -179,8 +179,8 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B #image.decode(read("/assets/images/tiger.jpg", encoding: none), format: "png", width: 80%) --- image-pixmap-empty --- -// Error: 1:2-8:2 zero-sized images are not allowed -#image.decode( +// Error: 1:2-8:2 zero-sized images are not allowed +#image( ( data: bytes(()), pixel-width: 0, @@ -191,7 +191,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B --- image-pixmap-invalid-size --- // Error: 1:2-8:2 provided pixel dimensions and pixmap data do not match -#image.decode( +#image( ( data: bytes((0x00, 0x00, 0x00)), pixel-width: 16, @@ -202,7 +202,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B --- image-pixmap-unknown-attribute --- // Error: 2:3-7:4 unexpected key "stowaway", valid keys are "data", "pixel-width", "pixel-height", and "icc-profile" -#image.decode( +#image( ( data: bytes((0x00, 0x00, 0x00)), pixel-width: 1, @@ -214,7 +214,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B --- image-pixmap-but-png-format --- // Error: 1:2-8:2 expected readable source for the given format (str or bytes) -#image.decode( +#image( ( data: bytes((0x00, 0x00, 0x00)), pixel-width: 1, @@ -225,7 +225,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B --- image-png-but-pixmap-format --- // Error: 1:2-4:2 source must be a pixmap -#image.decode( +#image( read("/assets/images/tiger.jpg", encoding: none), format: "rgba8", )