typst_pdf/
image.rs

1use std::hash::{Hash, Hasher};
2use std::sync::{Arc, OnceLock};
3
4use ecow::eco_format;
5use image::{DynamicImage, EncodableLayout, GenericImageView, Rgba};
6use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace};
7use krilla::pdf::PdfDocument;
8use krilla::surface::Surface;
9use krilla_svg::{SurfaceExt, SvgSettings};
10use typst_library::diag::{At, SourceResult};
11use typst_library::foundations::Smart;
12use typst_library::layout::{Abs, Angle, Ratio, Size, Transform};
13use typst_library::visualize::{
14    ExchangeFormat, Image, ImageKind, ImageScaling, PdfImage, RasterFormat, RasterImage,
15};
16use typst_syntax::Span;
17use typst_utils::defer;
18
19use crate::convert::{FrameContext, GlobalContext};
20use crate::tags;
21use crate::util::{SizeExt, TransformExt};
22
23#[typst_macros::time(name = "handle image")]
24pub(crate) fn handle_image(
25    gc: &mut GlobalContext,
26    fc: &mut FrameContext,
27    image: &Image,
28    size: Size,
29    surface: &mut Surface,
30    span: Span,
31) -> SourceResult<()> {
32    surface.push_transform(&fc.state().transform().to_krilla());
33    surface.set_location(span.into_raw());
34    let mut surface = defer(surface, |s| {
35        s.pop();
36        s.reset_location();
37    });
38
39    let interpolate = image.scaling() == Smart::Custom(ImageScaling::Smooth);
40
41    gc.image_spans.insert(span);
42
43    let mut handle = tags::image(gc, fc, &mut surface, image, size);
44    let surface = handle.surface();
45
46    match image.kind() {
47        ImageKind::Raster(raster) => {
48            let (exif_transform, new_size) = exif_transform(raster, size);
49            surface.push_transform(&exif_transform.to_krilla());
50            let mut surface = defer(surface, |s| s.pop());
51
52            let image = convert_raster(raster.clone(), interpolate)
53                .map_err(|err| eco_format!("failed to process image ({err})"))
54                .at(span)?;
55
56            if !gc.image_to_spans.contains_key(&image) {
57                gc.image_to_spans.insert(image.clone(), span);
58            }
59
60            if let Some(size) = new_size.to_krilla() {
61                surface.draw_image(image, size);
62            }
63        }
64        ImageKind::Svg(svg) => {
65            if let Some(size) = size.to_krilla() {
66                surface.draw_svg(
67                    svg.tree(),
68                    size,
69                    SvgSettings { embed_text: true, ..Default::default() },
70                );
71            }
72        }
73        ImageKind::Pdf(pdf) => {
74            if let Some(size) = size.to_krilla() {
75                surface.draw_pdf_page(&convert_pdf(pdf), size, pdf.page_index());
76            }
77        }
78    }
79
80    Ok(())
81}
82
83struct Repr {
84    /// The original, underlying raster image.
85    raster: RasterImage,
86    /// The alpha channel of the raster image, if existing.
87    alpha_channel: OnceLock<Option<Vec<u8>>>,
88    /// A (potentially) converted version of the dynamic image stored `raster` that is
89    /// guaranteed to either be in luma8 or rgb8, and thus can be used for the
90    /// `color_channel` method of `CustomImage`.
91    actual_dynamic: OnceLock<Arc<DynamicImage>>,
92}
93
94/// A wrapper around `RasterImage` so that we can implement `CustomImage`.
95#[derive(Clone)]
96struct PdfRasterImage(Arc<Repr>);
97
98impl PdfRasterImage {
99    pub fn new(raster: RasterImage) -> Self {
100        Self(Arc::new(Repr {
101            raster,
102            alpha_channel: OnceLock::new(),
103            actual_dynamic: OnceLock::new(),
104        }))
105    }
106}
107
108impl Hash for PdfRasterImage {
109    fn hash<H: Hasher>(&self, state: &mut H) {
110        // `alpha_channel` and `actual_dynamic` are generated from the underlying `RasterImage`,
111        // so this is enough. Since `raster` is prehashed, this is also very cheap.
112        self.0.raster.hash(state);
113    }
114}
115
116impl CustomImage for PdfRasterImage {
117    fn color_channel(&self) -> &[u8] {
118        self.0
119            .actual_dynamic
120            .get_or_init(|| {
121                let dynamic = self.0.raster.dynamic();
122                let channel_count = dynamic.color().channel_count();
123
124                match (dynamic.as_ref(), channel_count) {
125                    // Pure luma8 or rgb8 image, can use it directly.
126                    (DynamicImage::ImageLuma8(_), _) => dynamic.clone(),
127                    (DynamicImage::ImageRgb8(_), _) => dynamic.clone(),
128                    // Grey-scale image, convert to luma8.
129                    (_, 1 | 2) => Arc::new(DynamicImage::ImageLuma8(dynamic.to_luma8())),
130                    // Anything else, convert to rgb8.
131                    _ => Arc::new(DynamicImage::ImageRgb8(dynamic.to_rgb8())),
132                }
133            })
134            .as_bytes()
135    }
136
137    fn alpha_channel(&self) -> Option<&[u8]> {
138        self.0
139            .alpha_channel
140            .get_or_init(|| {
141                self.0.raster.dynamic().color().has_alpha().then(|| {
142                    self.0
143                        .raster
144                        .dynamic()
145                        .pixels()
146                        .map(|(_, _, Rgba([_, _, _, a]))| a)
147                        .collect()
148                })
149            })
150            .as_ref()
151            .map(|v| &**v)
152    }
153
154    fn bits_per_component(&self) -> BitsPerComponent {
155        BitsPerComponent::Eight
156    }
157
158    fn size(&self) -> (u32, u32) {
159        (self.0.raster.width(), self.0.raster.height())
160    }
161
162    fn icc_profile(&self) -> Option<&[u8]> {
163        if matches!(
164            self.0.raster.dynamic().as_ref(),
165            DynamicImage::ImageLuma8(_)
166                | DynamicImage::ImageLumaA8(_)
167                | DynamicImage::ImageRgb8(_)
168                | DynamicImage::ImageRgba8(_)
169        ) {
170            self.0.raster.icc().map(|b| b.as_bytes())
171        } else {
172            // In all other cases, the dynamic will be converted into RGB8 or LUMA8, so the ICC
173            // profile may become invalid, and thus we don't include it.
174            None
175        }
176    }
177
178    fn color_space(&self) -> ImageColorspace {
179        // Remember that we convert all images to either RGB or luma.
180        if self.0.raster.dynamic().color().has_color() {
181            ImageColorspace::Rgb
182        } else {
183            ImageColorspace::Luma
184        }
185    }
186}
187
188#[comemo::memoize]
189fn convert_raster(
190    raster: RasterImage,
191    interpolate: bool,
192) -> Result<krilla::image::Image, String> {
193    if let RasterFormat::Exchange(ExchangeFormat::Jpg) = raster.format() {
194        let image_data: Arc<dyn AsRef<[u8]> + Send + Sync> =
195            Arc::new(raster.data().clone());
196        let icc_profile = raster.icc().map(|i| {
197            let i: Arc<dyn AsRef<[u8]> + Send + Sync> = Arc::new(i.clone());
198            i
199        });
200
201        krilla::image::Image::from_jpeg_with_icc(
202            image_data.into(),
203            icc_profile.map(|i| i.into()),
204            interpolate,
205        )
206    } else {
207        krilla::image::Image::from_custom(PdfRasterImage::new(raster), interpolate)
208    }
209}
210
211#[comemo::memoize]
212fn convert_pdf(pdf: &PdfImage) -> PdfDocument {
213    PdfDocument::new(pdf.document().pdf().clone())
214}
215
216fn exif_transform(image: &RasterImage, size: Size) -> (Transform, Size) {
217    // For JPEGs, we want to apply the EXIF orientation as a transformation
218    // because we don't recode them. For other formats, the transform is already
219    // baked into the dynamic image data.
220    if image.format() != RasterFormat::Exchange(ExchangeFormat::Jpg) {
221        return (Transform::identity(), size);
222    }
223
224    let base = |hp: bool, vp: bool, mut base_ts: Transform, size: Size| {
225        if hp {
226            // Flip horizontally in-place.
227            base_ts = base_ts.pre_concat(
228                Transform::scale(-Ratio::one(), Ratio::one())
229                    .pre_concat(Transform::translate(-size.x, Abs::zero())),
230            )
231        }
232
233        if vp {
234            // Flip vertically in-place.
235            base_ts = base_ts.pre_concat(
236                Transform::scale(Ratio::one(), -Ratio::one())
237                    .pre_concat(Transform::translate(Abs::zero(), -size.y)),
238            )
239        }
240
241        base_ts
242    };
243
244    let no_flipping =
245        |hp: bool, vp: bool| (base(hp, vp, Transform::identity(), size), size);
246
247    let with_flipping = |hp: bool, vp: bool| {
248        let base_ts = Transform::rotate_at(Angle::deg(90.0), Abs::zero(), Abs::zero())
249            .pre_concat(Transform::scale(Ratio::one(), -Ratio::one()));
250        let inv_size = Size::new(size.y, size.x);
251        (base(hp, vp, base_ts, inv_size), inv_size)
252    };
253
254    match image.exif_rotation() {
255        Some(2) => no_flipping(true, false),
256        Some(3) => no_flipping(true, true),
257        Some(4) => no_flipping(false, true),
258        Some(5) => with_flipping(false, false),
259        Some(6) => with_flipping(false, true),
260        Some(7) => with_flipping(true, true),
261        Some(8) => with_flipping(true, false),
262        _ => no_flipping(false, false),
263    }
264}