Laurenz Stampfl 96dd67e011
Switch PDF backend to krilla (#5420)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-04-01 14:42:52 +00:00

245 lines
7.8 KiB
Rust

use std::hash::{Hash, Hasher};
use std::sync::{Arc, OnceLock};
use image::{DynamicImage, EncodableLayout, GenericImageView, Rgba};
use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace};
use krilla::surface::Surface;
use krilla_svg::{SurfaceExt, SvgSettings};
use typst_library::diag::{bail, SourceResult};
use typst_library::foundations::Smart;
use typst_library::layout::{Abs, Angle, Ratio, Size, Transform};
use typst_library::visualize::{
ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, RasterImage,
};
use typst_syntax::Span;
use crate::convert::{FrameContext, GlobalContext};
use crate::util::{SizeExt, TransformExt};
#[typst_macros::time(name = "handle image")]
pub(crate) fn handle_image(
gc: &mut GlobalContext,
fc: &mut FrameContext,
image: &Image,
size: Size,
surface: &mut Surface,
span: Span,
) -> SourceResult<()> {
surface.push_transform(&fc.state().transform().to_krilla());
surface.set_location(span.into_raw().get());
let interpolate = image.scaling() == Smart::Custom(ImageScaling::Smooth);
if let Some(alt) = image.alt() {
surface.start_alt_text(alt);
}
gc.image_spans.insert(span);
match image.kind() {
ImageKind::Raster(raster) => {
let (exif_transform, new_size) = exif_transform(raster, size);
surface.push_transform(&exif_transform.to_krilla());
let image = match convert_raster(raster.clone(), interpolate) {
None => bail!(span, "failed to process image"),
Some(i) => i,
};
if !gc.image_to_spans.contains_key(&image) {
gc.image_to_spans.insert(image.clone(), span);
}
surface.draw_image(image, new_size.to_krilla());
surface.pop();
}
ImageKind::Svg(svg) => {
surface.draw_svg(
svg.tree(),
size.to_krilla(),
SvgSettings { embed_text: true, ..Default::default() },
);
}
}
if image.alt().is_some() {
surface.end_alt_text();
}
surface.pop();
surface.reset_location();
Ok(())
}
struct Repr {
/// The original, underlying raster image.
raster: RasterImage,
/// The alpha channel of the raster image, if existing.
alpha_channel: OnceLock<Option<Vec<u8>>>,
/// A (potentially) converted version of the dynamic image stored `raster` that is
/// guaranteed to either be in luma8 or rgb8, and thus can be used for the
/// `color_channel` method of `CustomImage`.
actual_dynamic: OnceLock<Arc<DynamicImage>>,
}
/// A wrapper around `RasterImage` so that we can implement `CustomImage`.
#[derive(Clone)]
struct PdfImage(Arc<Repr>);
impl PdfImage {
pub fn new(raster: RasterImage) -> Self {
Self(Arc::new(Repr {
raster,
alpha_channel: OnceLock::new(),
actual_dynamic: OnceLock::new(),
}))
}
}
impl Hash for PdfImage {
fn hash<H: Hasher>(&self, state: &mut H) {
// `alpha_channel` and `actual_dynamic` are generated from the underlying `RasterImage`,
// so this is enough. Since `raster` is prehashed, this is also very cheap.
self.0.raster.hash(state);
}
}
impl CustomImage for PdfImage {
fn color_channel(&self) -> &[u8] {
self.0
.actual_dynamic
.get_or_init(|| {
let dynamic = self.0.raster.dynamic();
let channel_count = dynamic.color().channel_count();
match (dynamic.as_ref(), channel_count) {
// Pure luma8 or rgb8 image, can use it directly.
(DynamicImage::ImageLuma8(_), _) => dynamic.clone(),
(DynamicImage::ImageRgb8(_), _) => dynamic.clone(),
// Grey-scale image, convert to luma8.
(_, 1 | 2) => Arc::new(DynamicImage::ImageLuma8(dynamic.to_luma8())),
// Anything else, convert to rgb8.
_ => Arc::new(DynamicImage::ImageRgb8(dynamic.to_rgb8())),
}
})
.as_bytes()
}
fn alpha_channel(&self) -> Option<&[u8]> {
self.0
.alpha_channel
.get_or_init(|| {
self.0.raster.dynamic().color().has_alpha().then(|| {
self.0
.raster
.dynamic()
.pixels()
.map(|(_, _, Rgba([_, _, _, a]))| a)
.collect()
})
})
.as_ref()
.map(|v| &**v)
}
fn bits_per_component(&self) -> BitsPerComponent {
BitsPerComponent::Eight
}
fn size(&self) -> (u32, u32) {
(self.0.raster.width(), self.0.raster.height())
}
fn icc_profile(&self) -> Option<&[u8]> {
if matches!(
self.0.raster.dynamic().as_ref(),
DynamicImage::ImageLuma8(_)
| DynamicImage::ImageLumaA8(_)
| DynamicImage::ImageRgb8(_)
| DynamicImage::ImageRgba8(_)
) {
self.0.raster.icc().map(|b| b.as_bytes())
} else {
// In all other cases, the dynamic will be converted into RGB8 or LUMA8, so the ICC
// profile may become invalid, and thus we don't include it.
None
}
}
fn color_space(&self) -> ImageColorspace {
// Remember that we convert all images to either RGB or luma.
if self.0.raster.dynamic().color().has_color() {
ImageColorspace::Rgb
} else {
ImageColorspace::Luma
}
}
}
#[comemo::memoize]
fn convert_raster(
raster: RasterImage,
interpolate: bool,
) -> Option<krilla::image::Image> {
if let RasterFormat::Exchange(ExchangeFormat::Jpg) = raster.format() {
let image_data: Arc<dyn AsRef<[u8]> + Send + Sync> =
Arc::new(raster.data().clone());
let icc_profile = raster.icc().map(|i| {
let i: Arc<dyn AsRef<[u8]> + Send + Sync> = Arc::new(i.clone());
i
});
krilla::image::Image::from_jpeg_with_icc(
image_data.into(),
icc_profile.map(|i| i.into()),
interpolate,
)
} else {
krilla::image::Image::from_custom(PdfImage::new(raster), interpolate)
}
}
fn exif_transform(image: &RasterImage, size: Size) -> (Transform, Size) {
let base = |hp: bool, vp: bool, mut base_ts: Transform, size: Size| {
if hp {
// Flip horizontally in-place.
base_ts = base_ts.pre_concat(
Transform::scale(-Ratio::one(), Ratio::one())
.pre_concat(Transform::translate(-size.x, Abs::zero())),
)
}
if vp {
// Flip vertically in-place.
base_ts = base_ts.pre_concat(
Transform::scale(Ratio::one(), -Ratio::one())
.pre_concat(Transform::translate(Abs::zero(), -size.y)),
)
}
base_ts
};
let no_flipping =
|hp: bool, vp: bool| (base(hp, vp, Transform::identity(), size), size);
let with_flipping = |hp: bool, vp: bool| {
let base_ts = Transform::rotate_at(Angle::deg(90.0), Abs::zero(), Abs::zero())
.pre_concat(Transform::scale(Ratio::one(), -Ratio::one()));
let inv_size = Size::new(size.y, size.x);
(base(hp, vp, base_ts, inv_size), inv_size)
};
match image.exif_rotation() {
Some(2) => no_flipping(true, false),
Some(3) => no_flipping(true, true),
Some(4) => no_flipping(false, true),
Some(5) => with_flipping(false, false),
Some(6) => with_flipping(true, false),
Some(7) => with_flipping(true, true),
Some(8) => with_flipping(false, true),
_ => no_flipping(false, false),
}
}