Add icc argument

This commit is contained in:
Laurenz 2025-01-30 19:34:51 +01:00
parent e00508f22d
commit b68971d01e
5 changed files with 64 additions and 25 deletions

View File

@ -51,9 +51,14 @@ pub fn layout_image(
// Construct the image itself. // Construct the image itself.
let kind = match format { let kind = match format {
ImageFormat::Raster(format) => { ImageFormat::Raster(format) => ImageKind::Raster(
ImageKind::Raster(RasterImage::new(data.clone(), format).at(span)?) RasterImage::new(
} data.clone(),
format,
elem.icc(styles).as_ref().map(|icc| icc.derived.clone()),
)
.at(span)?,
),
ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg( ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg(
SvgImage::with_fonts( SvgImage::with_fonts(
data.clone(), data.clone(),

View File

@ -105,8 +105,11 @@ fn draw_raster_glyph(
raster_image: ttf_parser::RasterGlyphImage, raster_image: ttf_parser::RasterGlyphImage,
) -> Option<()> { ) -> Option<()> {
let data = Bytes::new(raster_image.data.to_vec()); let data = Bytes::new(raster_image.data.to_vec());
let image = let image = Image::new(
Image::new(RasterImage::new(data, ExchangeFormat::Png).ok()?, None, Smart::Auto); RasterImage::new(data, ExchangeFormat::Png, Smart::Auto).ok()?,
None,
Smart::Auto,
);
// Apple Color emoji doesn't provide offset information (or at least // Apple Color emoji doesn't provide offset information (or at least
// not in a way ttf-parser understands), so we artificially shift their // not in a way ttf-parser understands), so we artificially shift their

View File

@ -127,6 +127,21 @@ pub struct ImageElem {
/// _Note:_ The exact look may differ across PDF viewers. /// _Note:_ The exact look may differ across PDF viewers.
pub scaling: Smart<ImageScaling>, pub scaling: Smart<ImageScaling>,
/// 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::<Spanned<Smart<DataSource>>>("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<Derived<DataSource, Bytes>>,
/// Whether text in SVG images should be converted into curves before /// Whether text in SVG images should be converted into curves before
/// embedding. This will result in the text becoming unselectable in the /// embedding. This will result in the text becoming unselectable in the
/// output. /// output.

View File

@ -12,7 +12,7 @@ use image::{
}; };
use crate::diag::{bail, StrResult}; 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. /// A decoded raster image.
#[derive(Clone, Hash)] #[derive(Clone, Hash)]
@ -23,31 +23,43 @@ struct Repr {
data: Bytes, data: Bytes,
format: RasterFormat, format: RasterFormat,
dynamic: image::DynamicImage, dynamic: image::DynamicImage,
icc: Option<Vec<u8>>, icc: Option<Bytes>,
dpi: Option<f64>, dpi: Option<f64>,
} }
impl RasterImage { impl RasterImage {
/// Decode a raster image. /// Decode a raster image.
pub fn new(data: Bytes, format: impl Into<RasterFormat>) -> StrResult<RasterImage> { pub fn new(
Self::new_impl(data, format.into()) data: Bytes,
format: impl Into<RasterFormat>,
icc: Smart<Bytes>,
) -> StrResult<RasterImage> {
Self::new_impl(data, format.into(), icc)
} }
/// The internal, non-generic implementation. /// The internal, non-generic implementation.
#[comemo::memoize] #[comemo::memoize]
#[typst_macros::time(name = "load raster image")] #[typst_macros::time(name = "load raster image")]
fn new_impl(data: Bytes, format: RasterFormat) -> StrResult<RasterImage> { fn new_impl(
data: Bytes,
format: RasterFormat,
icc: Smart<Bytes>,
) -> StrResult<RasterImage> {
let (dynamic, icc, dpi) = match format { let (dynamic, icc, dpi) = match format {
RasterFormat::Exchange(format) => { RasterFormat::Exchange(format) => {
fn decode_with<T: ImageDecoder>( fn decode<T: ImageDecoder>(
decoder: ImageResult<T>, decoder: ImageResult<T>,
) -> ImageResult<(image::DynamicImage, Option<Vec<u8>>)> { icc: Smart<Bytes>,
) -> ImageResult<(image::DynamicImage, Option<Bytes>)> {
let mut decoder = decoder?; let mut decoder = decoder?;
let icc = decoder let icc = icc.custom().or_else(|| {
.icc_profile() decoder
.ok() .icc_profile()
.flatten() .ok()
.filter(|icc| !icc.is_empty()); .flatten()
.filter(|icc| !icc.is_empty())
.map(Bytes::new)
});
decoder.set_limits(Limits::default())?; decoder.set_limits(Limits::default())?;
let dynamic = image::DynamicImage::from_decoder(decoder)?; let dynamic = image::DynamicImage::from_decoder(decoder)?;
Ok((dynamic, icc)) Ok((dynamic, icc))
@ -55,9 +67,9 @@ impl RasterImage {
let cursor = io::Cursor::new(&data); let cursor = io::Cursor::new(&data);
let (mut dynamic, icc) = match format { let (mut dynamic, icc) = match format {
ExchangeFormat::Jpg => decode_with(JpegDecoder::new(cursor)), ExchangeFormat::Jpg => decode(JpegDecoder::new(cursor), icc),
ExchangeFormat::Png => decode_with(PngDecoder::new(cursor)), ExchangeFormat::Png => decode(PngDecoder::new(cursor), icc),
ExchangeFormat::Gif => decode_with(GifDecoder::new(cursor)), ExchangeFormat::Gif => decode(GifDecoder::new(cursor), icc),
} }
.map_err(format_image_error)?; .map_err(format_image_error)?;
@ -115,7 +127,7 @@ impl RasterImage {
PixelEncoding::Lumaa8 => to::<image::LumaA<u8>>(&data, format).into(), PixelEncoding::Lumaa8 => to::<image::LumaA<u8>>(&data, format).into(),
}; };
(dynamic, None, None) (dynamic, icc.custom(), None)
} }
}; };
@ -405,8 +417,7 @@ fn format_image_error(error: image::ImageError) -> EcoString {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ExchangeFormat, RasterImage}; use super::*;
use crate::foundations::Bytes;
#[test] #[test]
fn test_image_dpi() { fn test_image_dpi() {
@ -414,7 +425,7 @@ mod tests {
fn test(path: &str, format: ExchangeFormat, dpi: f64) { fn test(path: &str, format: ExchangeFormat, dpi: f64) {
let data = typst_dev_assets::get(path).unwrap(); let data = typst_dev_assets::get(path).unwrap();
let bytes = Bytes::new(data); 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)); assert_eq!(image.dpi().map(f64::round), Some(dpi));
} }

View File

@ -247,7 +247,12 @@ fn convert_bitmap_glyph_to_image(font: &Font, id: GlyphId) -> Option<(Image, f64
return None; return None;
} }
let image = Image::new( 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, None,
Smart::Auto, Smart::Auto,
); );