summaryrefslogtreecommitdiff
path: root/crates/typst-pdf
diff options
context:
space:
mode:
authorfrozolotl <44589151+frozolotl@users.noreply.github.com>2025-01-31 10:56:25 +0100
committerGitHub <noreply@github.com>2025-01-31 09:56:25 +0000
commit3eb6e87af1d8870a38cc5914e345d07373e1e8c1 (patch)
tree800de2bc1ed9a5c7f8efc21e2741c0cd5c4728f6 /crates/typst-pdf
parentbe1fa91a00a9bff6c5eb9744266f252b8cc23fe4 (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.rs165
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!(),
+ }
+}