diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index 83c282133..503c30820 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -51,9 +51,14 @@ pub fn layout_image( // Construct the image itself. let kind = match format { - ImageFormat::Raster(format) => { - ImageKind::Raster(RasterImage::new(data.clone(), format).at(span)?) - } + ImageFormat::Raster(format) => ImageKind::Raster( + RasterImage::new( + data.clone(), + format, + elem.icc(styles).as_ref().map(|icc| icc.derived.clone()), + ) + .at(span)?, + ), ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg( SvgImage::with_fonts( data.clone(), diff --git a/crates/typst-library/src/text/font/color.rs b/crates/typst-library/src/text/font/color.rs index 3a904dc6f..a7e8d55ef 100644 --- a/crates/typst-library/src/text/font/color.rs +++ b/crates/typst-library/src/text/font/color.rs @@ -105,8 +105,11 @@ fn draw_raster_glyph( raster_image: ttf_parser::RasterGlyphImage, ) -> Option<()> { let data = Bytes::new(raster_image.data.to_vec()); - let image = - Image::new(RasterImage::new(data, ExchangeFormat::Png).ok()?, None, Smart::Auto); + let image = Image::new( + RasterImage::new(data, ExchangeFormat::Png, Smart::Auto).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 diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 95cc7721b..19b1bdb4f 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -127,6 +127,21 @@ pub struct ImageElem { /// _Note:_ The exact look may differ across PDF viewers. pub scaling: Smart, + /// An ICC profile for the image. + /// + /// ICC profiles define how to interpret the colors in an image. When set + /// to `{auto}`, Typst will try to extract an ICC profile from the image. + #[parse(match args.named::>>("icc")? { + Some(Spanned { v: Smart::Custom(source), span }) => Some(Smart::Custom({ + let data = Spanned::new(&source, span).load(engine.world)?; + Derived::new(source, data) + })), + Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto), + None => None, + })] + #[borrowed] + pub icc: Smart>, + /// Whether text in SVG images should be converted into curves before /// embedding. This will result in the text becoming unselectable in the /// output. diff --git a/crates/typst-library/src/visualize/image/raster.rs b/crates/typst-library/src/visualize/image/raster.rs index bf661e426..86455ce19 100644 --- a/crates/typst-library/src/visualize/image/raster.rs +++ b/crates/typst-library/src/visualize/image/raster.rs @@ -12,7 +12,7 @@ use image::{ }; use crate::diag::{bail, StrResult}; -use crate::foundations::{cast, dict, Bytes, Cast, Dict, Value}; +use crate::foundations::{cast, dict, Bytes, Cast, Dict, Smart, Value}; /// A decoded raster image. #[derive(Clone, Hash)] @@ -23,31 +23,43 @@ struct Repr { data: Bytes, format: RasterFormat, dynamic: image::DynamicImage, - icc: Option>, + icc: Option, dpi: Option, } impl RasterImage { /// Decode a raster image. - pub fn new(data: Bytes, format: impl Into) -> StrResult { - Self::new_impl(data, format.into()) + pub fn new( + data: Bytes, + format: impl Into, + icc: Smart, + ) -> StrResult { + Self::new_impl(data, format.into(), icc) } /// The internal, non-generic implementation. #[comemo::memoize] #[typst_macros::time(name = "load raster image")] - fn new_impl(data: Bytes, format: RasterFormat) -> StrResult { + fn new_impl( + data: Bytes, + format: RasterFormat, + icc: Smart, + ) -> StrResult { let (dynamic, icc, dpi) = match format { RasterFormat::Exchange(format) => { - fn decode_with( + fn decode( decoder: ImageResult, - ) -> ImageResult<(image::DynamicImage, Option>)> { + icc: Smart, + ) -> ImageResult<(image::DynamicImage, Option)> { let mut decoder = decoder?; - let icc = decoder - .icc_profile() - .ok() - .flatten() - .filter(|icc| !icc.is_empty()); + let icc = icc.custom().or_else(|| { + decoder + .icc_profile() + .ok() + .flatten() + .filter(|icc| !icc.is_empty()) + .map(Bytes::new) + }); decoder.set_limits(Limits::default())?; let dynamic = image::DynamicImage::from_decoder(decoder)?; Ok((dynamic, icc)) @@ -55,9 +67,9 @@ impl RasterImage { let cursor = io::Cursor::new(&data); let (mut dynamic, icc) = match format { - ExchangeFormat::Jpg => decode_with(JpegDecoder::new(cursor)), - ExchangeFormat::Png => decode_with(PngDecoder::new(cursor)), - ExchangeFormat::Gif => decode_with(GifDecoder::new(cursor)), + ExchangeFormat::Jpg => decode(JpegDecoder::new(cursor), icc), + ExchangeFormat::Png => decode(PngDecoder::new(cursor), icc), + ExchangeFormat::Gif => decode(GifDecoder::new(cursor), icc), } .map_err(format_image_error)?; @@ -115,7 +127,7 @@ impl RasterImage { PixelEncoding::Lumaa8 => to::>(&data, format).into(), }; - (dynamic, None, None) + (dynamic, icc.custom(), None) } }; @@ -405,8 +417,7 @@ fn format_image_error(error: image::ImageError) -> EcoString { #[cfg(test)] mod tests { - use super::{ExchangeFormat, RasterImage}; - use crate::foundations::Bytes; + use super::*; #[test] fn test_image_dpi() { @@ -414,7 +425,7 @@ mod tests { fn test(path: &str, format: ExchangeFormat, dpi: f64) { let data = typst_dev_assets::get(path).unwrap(); let bytes = Bytes::new(data); - let image = RasterImage::new(bytes, format).unwrap(); + let image = RasterImage::new(bytes, format, Smart::Auto).unwrap(); assert_eq!(image.dpi().map(f64::round), Some(dpi)); } diff --git a/crates/typst-svg/src/text.rs b/crates/typst-svg/src/text.rs index 296812da1..2386006d7 100644 --- a/crates/typst-svg/src/text.rs +++ b/crates/typst-svg/src/text.rs @@ -247,7 +247,12 @@ fn convert_bitmap_glyph_to_image(font: &Font, id: GlyphId) -> Option<(Image, f64 return None; } let image = Image::new( - RasterImage::new(Bytes::new(raster.data.to_vec()), ExchangeFormat::Png).ok()?, + RasterImage::new( + Bytes::new(raster.data.to_vec()), + ExchangeFormat::Png, + Smart::Auto, + ) + .ok()?, None, Smart::Auto, );