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.
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(),

View File

@ -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

View File

@ -127,6 +127,21 @@ pub struct ImageElem {
/// _Note:_ The exact look may differ across PDF viewers.
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
/// embedding. This will result in the text becoming unselectable in the
/// output.

View File

@ -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<Vec<u8>>,
icc: Option<Bytes>,
dpi: Option<f64>,
}
impl RasterImage {
/// Decode a raster image.
pub fn new(data: Bytes, format: impl Into<RasterFormat>) -> StrResult<RasterImage> {
Self::new_impl(data, format.into())
pub fn new(
data: Bytes,
format: impl Into<RasterFormat>,
icc: Smart<Bytes>,
) -> StrResult<RasterImage> {
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<RasterImage> {
fn new_impl(
data: Bytes,
format: RasterFormat,
icc: Smart<Bytes>,
) -> StrResult<RasterImage> {
let (dynamic, icc, dpi) = match format {
RasterFormat::Exchange(format) => {
fn decode_with<T: ImageDecoder>(
fn decode<T: ImageDecoder>(
decoder: ImageResult<T>,
) -> ImageResult<(image::DynamicImage, Option<Vec<u8>>)> {
icc: Smart<Bytes>,
) -> ImageResult<(image::DynamicImage, Option<Bytes>)> {
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::<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)]
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));
}

View File

@ -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,
);