diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 7ee96378d..0532f2f6b 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -61,22 +61,21 @@ pub struct ImageElem { /// The image's format. /// /// By default, the format is detected automatically. Typically, you thus - /// only need to specify this when providing raw bytes as the `source` ( - /// even then, Typst will try to figure out the format automatically, but - /// that's not always possible). + /// only need to specify this when providing raw bytes as the + /// [`source`]($image.source) (even then, Typst will try to figure out the + /// format automatically, but that's not always possible). /// - /// Supported formats include common exchange image formats (`{"png"}`, - /// `{"jpg"}`, `{"gif"}`, and `{"svg"}`) as well as raw pixel data. - /// Embedding PDFs as images is [not currently - /// supported](https://github.com/typst/typst/issues/145). + /// Supported formats are `{"png"}`, `{"jpg"}`, `{"gif"}`, `{"svg"}` as well + /// as raw pixel data. Embedding PDFs as images is + /// [not currently supported](https://github.com/typst/typst/issues/145). /// - /// When providing raw pixel data as the [`source`]($image.source), you must - /// specify a dictionary with the following keys as the `format`: + /// When providing raw pixel data as the `source`, you must specify a + /// dictionary with the following keys as the `format`: /// - `encoding` ([str]): The encoding of the pixel data. One of: - /// - `{"rgb8"}` (three 8-bit channels: Red, green, blue.) - /// - `{"rgba8"}` (four 8-bit channels: Red, green, blue, alpha.) - /// - `{"luma8"}` (one 8-bit channel: Brightness.) - /// - `{"lumaa8"}` (two 8-bit channels: Brightness and alpha.) + /// - `{"rgb8"}` (three 8-bit channels: red, green, blue.) + /// - `{"rgba8"}` (four 8-bit channels: red, green, blue, alpha.) + /// - `{"luma8"}` (one 8-bit channel: brightness.) + /// - `{"lumaa8"}` (two 8-bit channels: brightness and alpha.) /// - `width` ([int]): The pixel width of the image. /// - `height` ([int]): The pixel height of the image. /// @@ -270,7 +269,7 @@ 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 kind. + /// Create an image from a `RasterImage` or `SvgImage`. pub fn new( kind: impl Into, alt: Option, diff --git a/crates/typst-library/src/visualize/image/raster.rs b/crates/typst-library/src/visualize/image/raster.rs index 8f24a9fed..bf661e426 100644 --- a/crates/typst-library/src/visualize/image/raster.rs +++ b/crates/typst-library/src/visualize/image/raster.rs @@ -100,7 +100,7 @@ impl RasterImage { bail!("pixel dimensions and pixel data do not match"); } - fn cast_as>( + fn to>( data: &Bytes, format: PixelFormat, ) -> ImageBuffer> { @@ -109,18 +109,10 @@ impl RasterImage { } let dynamic = match format.encoding { - PixelEncoding::Rgb8 => { - cast_as::>(&data, format).into() - } - PixelEncoding::Rgba8 => { - cast_as::>(&data, format).into() - } - PixelEncoding::Luma8 => { - cast_as::>(&data, format).into() - } - PixelEncoding::Lumaa8 => { - cast_as::>(&data, format).into() - } + PixelEncoding::Rgb8 => to::>(&data, format).into(), + PixelEncoding::Rgba8 => to::>(&data, format).into(), + PixelEncoding::Luma8 => to::>(&data, format).into(), + PixelEncoding::Lumaa8 => to::>(&data, format).into(), }; (dynamic, None, None) @@ -177,7 +169,7 @@ impl Hash for Repr { /// A raster graphics format. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum RasterFormat { - /// A format used in image exchange. + /// A format typically used in image exchange. Exchange(ExchangeFormat), /// A format of raw pixel data. Pixel(PixelFormat), @@ -205,7 +197,7 @@ cast! { v: PixelFormat => Self::Pixel(v), } -/// An raster format typically used in image exchange, with efficient encoding. +/// A raster format typically used in image exchange, with efficient encoding. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum ExchangeFormat { /// Raster format for illustrations and transparent graphics. @@ -260,13 +252,13 @@ pub struct PixelFormat { /// Determines the channel encoding of raw pixel data. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum PixelEncoding { - /// Raw image data with three 8-bit channels: Red, green, blue. + /// Three 8-bit channels: Red, green, blue. Rgb8, - /// Raw image data with four 8-bit channels: Red, green, blue, alpha. + /// Four 8-bit channels: Red, green, blue, alpha. Rgba8, - /// Raw image data with one 8-bit channel: Brightness. + /// One 8-bit channel: Brightness. Luma8, - /// Raw image data with two 8-bit channels: Brightness and alpha. + /// Two 8-bit channels: Brightness and alpha. Lumaa8, } diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs index c8dfb1dcc..a88417909 100644 --- a/crates/typst-pdf/src/image.rs +++ b/crates/typst-pdf/src/image.rs @@ -7,7 +7,8 @@ use pdf_writer::{Chunk, Filter, Finish, Ref}; use typst_library::diag::{At, SourceResult, StrResult}; use typst_library::foundations::Smart; use typst_library::visualize::{ - ColorSpace, ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, SvgImage, + ColorSpace, ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, + RasterImage, SvgImage, }; use typst_utils::Deferred; @@ -137,15 +138,7 @@ pub fn deferred_image( let interpolate = !pdfa && image.scaling() == Smart::Custom(ImageScaling::Smooth); let deferred = Deferred::new(move || match image.kind() { - ImageKind::Raster(raster) => { - let format = if raster.format() == RasterFormat::Exchange(ExchangeFormat::Jpg) - { - EncodeFormat::DctDecode - } else { - EncodeFormat::Flate - }; - Ok(encode_raster_image(raster.dynamic(), raster.icc(), format, interpolate)) - } + ImageKind::Raster(raster) => Ok(encode_raster_image(raster, interpolate)), ImageKind::Svg(svg) => { let (chunk, id) = encode_svg(svg, pdfa) .map_err(|err| eco_format!("failed to convert SVG to PDF: {err}"))?; @@ -158,36 +151,31 @@ pub fn deferred_image( /// Encode an image with a suitable filter. #[typst_macros::time(name = "encode raster image")] -fn encode_raster_image( - image: &DynamicImage, - icc_profile: Option<&[u8]>, - format: EncodeFormat, - interpolate: bool, -) -> EncodedImage { - let color_space = to_color_space(image.color()); +fn encode_raster_image(image: &RasterImage, interpolate: bool) -> EncodedImage { + let dynamic = image.dynamic(); + let color_space = to_color_space(dynamic.color()); - let (filter, data, bits_per_component) = match format { - EncodeFormat::DctDecode => { + let (filter, data, bits_per_component) = + if image.format() == RasterFormat::Exchange(ExchangeFormat::Jpg) { let mut data = Cursor::new(vec![]); - image.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); + dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); (Filter::DctDecode, data.into_inner(), 8) - } - EncodeFormat::Flate => { + } else { // TODO: Encode flate streams with PNG-predictor? - let (data, bits_per_component) = match (image, color_space) { + let (data, bits_per_component) = match (dynamic, color_space) { + // RGB image. (DynamicImage::ImageRgb8(rgb), _) => (deflate(rgb.as_raw()), 8), // Grayscale image (DynamicImage::ImageLuma8(luma), _) => (deflate(luma.as_raw()), 8), - (_, ColorSpace::D65Gray) => (deflate(image.to_luma8().as_raw()), 8), + (_, ColorSpace::D65Gray) => (deflate(dynamic.to_luma8().as_raw()), 8), // Anything else - _ => (deflate(image.to_rgb8().as_raw()), 8), + _ => (deflate(dynamic.to_rgb8().as_raw()), 8), }; (Filter::FlateDecode, data, bits_per_component) - } - }; + }; - let compressed_icc = icc_profile.map(deflate); - let alpha = image.color().has_alpha().then(|| encode_alpha(image)); + let compressed_icc = image.icc().map(deflate); + let alpha = dynamic.color().has_alpha().then(|| encode_alpha(dynamic)); EncodedImage::Raster { data, @@ -254,12 +242,6 @@ pub enum EncodedImage { Svg(Chunk, Ref), } -/// How the raster image should be encoded. -enum EncodeFormat { - DctDecode, - Flate, -} - /// Matches an [`image::ColorType`] to [`ColorSpace`]. fn to_color_space(color: image::ColorType) -> ColorSpace { use image::ColorType::*; diff --git a/crates/typst-render/src/image.rs b/crates/typst-render/src/image.rs index 48ac6199a..7425bdd2f 100644 --- a/crates/typst-render/src/image.rs +++ b/crates/typst-render/src/image.rs @@ -62,7 +62,29 @@ 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(&mut texture, raster.dynamic(), image.scaling()) + let w = texture.width(); + let h = texture.height(); + + let buf; + let dynamic = raster.dynamic(); + let resized = if (w, h) == (dynamic.width(), dynamic.height()) { + // Small optimization to not allocate in case image is not resized. + dynamic + } else { + let upscale = w > dynamic.width(); + let filter = match image.scaling() { + Smart::Custom(ImageScaling::Pixelated) => FilterType::Nearest, + _ if upscale => FilterType::CatmullRom, + _ => FilterType::Lanczos3, // downscale + }; + buf = dynamic.resize_exact(w, h, filter); + &buf + }; + + 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(); + } } ImageKind::Svg(svg) => { let tree = svg.tree(); @@ -75,33 +97,3 @@ fn build_texture(image: &Image, w: u32, h: u32) -> Option> { } Some(Arc::new(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, -) { - 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. - image - } else { - let upscale = w > image.width(); - let filter = match scaling { - 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(texture.pixels_mut()) { - let Rgba([r, g, b, a]) = src; - *dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply(); - } -} diff --git a/crates/typst-svg/src/image.rs b/crates/typst-svg/src/image.rs index 2fdb9f139..d74432026 100644 --- a/crates/typst-svg/src/image.rs +++ b/crates/typst-svg/src/image.rs @@ -4,8 +4,7 @@ use image::{codecs::png::PngEncoder, ImageEncoder}; use typst_library::foundations::Smart; use typst_library::layout::{Abs, Axes}; use typst_library::visualize::{ - ExchangeFormat, Image, ImageFormat, ImageKind, ImageScaling, RasterFormat, - VectorFormat, + ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, }; use crate::SVGRenderer; @@ -38,23 +37,18 @@ impl SVGRenderer { /// `data:image/{format};base64,`. #[comemo::memoize] pub fn convert_image_to_base64_url(image: &Image) -> EcoString { - let format = match image.format() { - ImageFormat::Raster(RasterFormat::Exchange(f)) => match f { - ExchangeFormat::Png => "png", - ExchangeFormat::Jpg => "jpeg", - ExchangeFormat::Gif => "gif", - }, - ImageFormat::Raster(RasterFormat::Pixel(_)) => "png", - ImageFormat::Vector(f) => match f { - VectorFormat::Svg => "svg+xml", - }, - }; - let mut buf; - let data = match image.kind() { + let (format, data): (&str, &[u8]) = match image.kind() { ImageKind::Raster(raster) => match raster.format() { - RasterFormat::Exchange(_) => raster.data(), - RasterFormat::Pixel(_) => { + RasterFormat::Exchange(format) => ( + match format { + ExchangeFormat::Png => "png", + ExchangeFormat::Jpg => "jpeg", + ExchangeFormat::Gif => "gif", + }, + raster.data(), + ), + RasterFormat::Pixel(_) => ("png", { buf = vec![]; let mut encoder = PngEncoder::new(&mut buf); if let Some(icc_profile) = raster.icc() { @@ -62,9 +56,9 @@ pub fn convert_image_to_base64_url(image: &Image) -> EcoString { } raster.dynamic().write_with_encoder(encoder).unwrap(); buf.as_slice() - } + }), }, - ImageKind::Svg(svg) => svg.data(), + ImageKind::Svg(svg) => ("svg+xml", svg.data()), }; let mut url = eco_format!("data:image/{format};base64,");