diff options
| author | frozolotl <44589151+frozolotl@users.noreply.github.com> | 2025-01-31 10:56:25 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-01-31 09:56:25 +0000 |
| commit | 3eb6e87af1d8870a38cc5914e345d07373e1e8c1 (patch) | |
| tree | 800de2bc1ed9a5c7f8efc21e2741c0cd5c4728f6 /crates/typst-pdf | |
| parent | be1fa91a00a9bff6c5eb9744266f252b8cc23fe4 (diff) | |
Include images from raw pixmaps and more (#5632)
Co-authored-by: PgBiel <9021226+PgBiel@users.noreply.github.com>
Co-authored-by: Laurenz <laurmaedje@gmail.com>
Diffstat (limited to 'crates/typst-pdf')
| -rw-r--r-- | crates/typst-pdf/src/image.rs | 165 |
1 files changed, 88 insertions, 77 deletions
diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs index bff7bfef..550f60a4 100644 --- a/crates/typst-pdf/src/image.rs +++ b/crates/typst-pdf/src/image.rs @@ -5,8 +5,10 @@ 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, RasterFormat, RasterImage, SvgImage, + ColorSpace, ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, + RasterImage, SvgImage, }; use typst_utils::Deferred; @@ -32,11 +34,13 @@ pub fn write_images( EncodedImage::Raster { data, filter, - has_color, + color_space, + bits_per_component, width, height, - icc, + compressed_icc, alpha, + interpolate, } => { let image_ref = chunk.alloc(); out.insert(image.clone(), image_ref); @@ -45,23 +49,18 @@ pub fn write_images( image.filter(*filter); image.width(*width as i32); image.height(*height as i32); - image.bits_per_component(8); + image.bits_per_component(i32::from(*bits_per_component)); + image.interpolate(*interpolate); let mut icc_ref = None; let space = image.color_space(); - if icc.is_some() { + if compressed_icc.is_some() { let id = chunk.alloc.bump(); space.icc_based(id); icc_ref = Some(id); - } else if *has_color { - color::write( - ColorSpace::Srgb, - space, - &context.globals.color_functions, - ); } else { color::write( - ColorSpace::D65Gray, + *color_space, space, &context.globals.color_functions, ); @@ -79,20 +78,27 @@ pub fn write_images( mask.width(*width as i32); mask.height(*height as i32); mask.color_space().device_gray(); - mask.bits_per_component(8); + mask.bits_per_component(i32::from(*bits_per_component)); + mask.interpolate(*interpolate); } else { image.finish(); } - if let (Some(icc), Some(icc_ref)) = (icc, icc_ref) { - let mut stream = chunk.icc_profile(icc_ref, icc); + if let (Some(compressed_icc), Some(icc_ref)) = + (compressed_icc, icc_ref) + { + let mut stream = chunk.icc_profile(icc_ref, compressed_icc); stream.filter(Filter::FlateDecode); - if *has_color { - stream.n(3); - stream.alternate().srgb(); - } else { - stream.n(1); - stream.alternate().d65_gray(); + match color_space { + ColorSpace::Srgb => { + stream.n(3); + stream.alternate().srgb(); + } + ColorSpace::D65Gray => { + stream.n(1); + stream.alternate().d65_gray(); + } + _ => unimplemented!(), } } } @@ -122,35 +128,17 @@ pub fn deferred_image( ) -> (Deferred<StrResult<EncodedImage>>, Option<ColorSpace>) { let color_space = match image.kind() { ImageKind::Raster(raster) if raster.icc().is_none() => { - if raster.dynamic().color().channel_count() > 2 { - Some(ColorSpace::Srgb) - } else { - Some(ColorSpace::D65Gray) - } + Some(to_color_space(raster.dynamic().color())) } _ => None, }; + // PDF/A does not appear to allow interpolation. + // See https://github.com/typst/typst/issues/2942. + let interpolate = !pdfa && image.scaling() == Smart::Custom(ImageScaling::Smooth); + let deferred = Deferred::new(move || match image.kind() { - ImageKind::Raster(raster) => { - let raster = raster.clone(); - let (width, height) = (raster.width(), raster.height()); - let (data, filter, has_color) = encode_raster_image(&raster); - let icc = raster.icc().map(deflate); - - let alpha = - raster.dynamic().color().has_alpha().then(|| encode_alpha(&raster)); - - Ok(EncodedImage::Raster { - data, - filter, - has_color, - width, - height, - icc, - alpha, - }) - } + 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}"))?; @@ -161,42 +149,51 @@ pub fn deferred_image( (deferred, color_space) } -/// Encode an image with a suitable filter and return the data, filter and -/// whether the image has color. -/// -/// Skips the alpha channel as that's encoded separately. +/// Encode an image with a suitable filter. #[typst_macros::time(name = "encode raster image")] -fn encode_raster_image(image: &RasterImage) -> (Vec<u8>, Filter, bool) { +fn encode_raster_image(image: &RasterImage, interpolate: bool) -> EncodedImage { let dynamic = image.dynamic(); - let channel_count = dynamic.color().channel_count(); - let has_color = channel_count > 2; - - if image.format() == RasterFormat::Jpg { - let mut data = Cursor::new(vec![]); - dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); - (data.into_inner(), Filter::DctDecode, has_color) - } else { - // TODO: Encode flate streams with PNG-predictor? - let data = match (dynamic, channel_count) { - (DynamicImage::ImageLuma8(luma), _) => deflate(luma.as_raw()), - (DynamicImage::ImageRgb8(rgb), _) => deflate(rgb.as_raw()), - // Grayscale image - (_, 1 | 2) => deflate(dynamic.to_luma8().as_raw()), - // Anything else - _ => deflate(dynamic.to_rgb8().as_raw()), + let color_space = to_color_space(dynamic.color()); + + let (filter, data, bits_per_component) = + if image.format() == RasterFormat::Exchange(ExchangeFormat::Jpg) { + let mut data = Cursor::new(vec![]); + dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); + (Filter::DctDecode, data.into_inner(), 8) + } else { + // TODO: Encode flate streams with PNG-predictor? + 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(dynamic.to_luma8().as_raw()), 8), + // Anything else + _ => (deflate(dynamic.to_rgb8().as_raw()), 8), + }; + (Filter::FlateDecode, data, bits_per_component) }; - (data, Filter::FlateDecode, has_color) + + let compressed_icc = image.icc().map(|data| deflate(data)); + let alpha = dynamic.color().has_alpha().then(|| encode_alpha(dynamic)); + + EncodedImage::Raster { + data, + filter, + color_space, + bits_per_component, + width: image.width(), + height: image.height(), + compressed_icc, + alpha, + interpolate, } } /// Encode an image's alpha channel if present. #[typst_macros::time(name = "encode alpha")] -fn encode_alpha(raster: &RasterImage) -> (Vec<u8>, Filter) { - let pixels: Vec<_> = raster - .dynamic() - .pixels() - .map(|(_, _, Rgba([_, _, _, a]))| a) - .collect(); +fn encode_alpha(image: &DynamicImage) -> (Vec<u8>, Filter) { + let pixels: Vec<_> = image.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect(); (deflate(&pixels), Filter::FlateDecode) } @@ -224,19 +221,33 @@ pub enum EncodedImage { data: Vec<u8>, /// The filter to use for the image. filter: Filter, - /// Whether the image has color. - has_color: bool, + /// Which color space this image is encoded in. + color_space: ColorSpace, + /// How many bits of each color component are stored. + bits_per_component: u8, /// The image's width. width: u32, /// The image's height. height: u32, - /// The image's ICC profile, pre-deflated, if any. - icc: Option<Vec<u8>>, + /// The image's ICC profile, deflated, if any. + compressed_icc: Option<Vec<u8>>, /// The alpha channel of the image, pre-deflated, if any. alpha: Option<(Vec<u8>, Filter)>, + /// Whether image interpolation should be enabled. + interpolate: bool, }, /// A vector graphic. /// /// The chunk is the SVG converted to PDF objects. Svg(Chunk, Ref), } + +/// Matches an [`image::ColorType`] to [`ColorSpace`]. +fn to_color_space(color: image::ColorType) -> ColorSpace { + use image::ColorType::*; + match color { + L8 | La8 | L16 | La16 => ColorSpace::D65Gray, + Rgb8 | Rgba8 | Rgb16 | Rgba16 | Rgb32F | Rgba32F => ColorSpace::Srgb, + _ => unimplemented!(), + } +} |
