Skip to main content

laser_pdf/elements/
image.rs

1use ::image::{DynamicImage, GenericImageView};
2use miniz_oxide::deflate::{CompressionLevel, compress_to_vec_zlib};
3use pdf_writer::Filter;
4use utils::mm_to_pt;
5
6use crate::{image::Image, *};
7
8use super::svg::Svg;
9
10const INCH_TO_MM: f32 = 25.4;
11
12/// An image element that can render both pixel images and SVG graphics.
13///
14/// The image is automatically scaled to fit the available width while maintaining
15/// aspect ratio. Supports various image formats through the `Image` enum.
16pub struct ImageElement<'a> {
17    /// Reference to the image data (pixel or SVG)
18    pub image: &'a Image,
19}
20
21impl<'a> Element for ImageElement<'a> {
22    fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
23        match self.image {
24            Image::Svg(svg) => Svg { data: svg }.first_location_usage(ctx),
25            Image::Pixel(image) => {
26                let (height, _) = calculate_size(image, ctx.width);
27
28                if ctx.break_appropriate_for_min_height(height) {
29                    FirstLocationUsage::WillSkip
30                } else {
31                    FirstLocationUsage::WillUse
32                }
33            }
34        }
35    }
36
37    fn measure(&self, mut ctx: MeasureCtx) -> ElementSize {
38        match self.image {
39            Image::Svg(svg) => Svg { data: svg }.measure(ctx),
40            Image::Pixel(image) => {
41                let (height, element_size) = calculate_size(image, ctx.width);
42
43                ctx.break_if_appropriate_for_min_height(height);
44
45                element_size
46            }
47        }
48    }
49
50    fn draw(&self, mut ctx: DrawCtx) -> ElementSize {
51        match self.image {
52            Image::Svg(svg) => Svg { data: svg }.draw(ctx),
53            Image::Pixel(image) => {
54                let (height, element_size) = calculate_size(image, ctx.width);
55
56                ctx.break_if_appropriate_for_min_height(height);
57
58                // a bit of a copy-paste from
59                // https://github.com/typst/pdf-writer/blob/main/examples/image.rs
60
61                // Define some indirect reference ids we'll use.
62                let image_id = ctx.pdf.alloc();
63                let s_mask_id = ctx.pdf.alloc();
64                let image_name = ctx.pdf.pages[ctx.location.page_idx].add_x_object(image_id);
65
66                let dynamic = image;
67
68                // Now, there are multiple considerations:
69                // - Writing an XObject with just the raw samples would work, but lead to
70                //   huge file sizes since the image would be embedded without any
71                //   compression.
72                // - We can encode the samples with a filter. However, which filter is best
73                //   depends on the file format. For example, for JPEGs you should use
74                //   DCT-Decode and for PNGs you should use Deflate.
75                // - When the image has transparency, we need to provide that separately
76                //   through an extra linked SMask image.
77                let level = CompressionLevel::DefaultLevel as u8;
78                let encoded = compress_to_vec_zlib(dynamic.to_rgb8().as_raw(), level);
79
80                // If there's an alpha channel, extract the pixel alpha values.
81                let mask = dynamic.color().has_alpha().then(|| {
82                    let alphas: Vec<_> = dynamic.pixels().map(|p| (p.2).0[3]).collect();
83                    compress_to_vec_zlib(&alphas, level)
84                });
85                let (filter, encoded, mask) = (Filter::FlateDecode, encoded, mask);
86
87                // Write the stream for the image we want to embed.
88                let mut image = ctx.pdf.pdf.image_xobject(image_id, &encoded);
89                image.filter(filter);
90                image.width(dynamic.width() as i32);
91                image.height(dynamic.height() as i32);
92                image.color_space().device_rgb();
93                image.bits_per_component(8);
94                if mask.is_some() {
95                    image.s_mask(s_mask_id);
96                }
97                drop(image);
98
99                // Add SMask if the image has transparency.
100                if let Some(encoded) = &mask {
101                    let mut s_mask = ctx.pdf.pdf.image_xobject(s_mask_id, encoded);
102                    s_mask.filter(filter);
103                    s_mask.width(dynamic.width() as i32);
104                    s_mask.height(dynamic.height() as i32);
105                    s_mask.color_space().device_gray();
106                    s_mask.bits_per_component(8);
107                }
108
109                ctx.location
110                    .layer(ctx.pdf)
111                    .save_state()
112                    .transform([
113                        mm_to_pt(element_size.width.unwrap()),
114                        0.,
115                        0.,
116                        mm_to_pt(element_size.height.unwrap()),
117                        mm_to_pt(ctx.location.pos.0),
118                        mm_to_pt(ctx.location.pos.1 - element_size.height.unwrap()),
119                    ])
120                    .x_object(Name(image_name.as_bytes()))
121                    .restore_state();
122
123                element_size
124            }
125        }
126    }
127}
128
129#[inline]
130fn calculate_size(image: &DynamicImage, width: WidthConstraint) -> (f32, ElementSize) {
131    let dimensions = {
132        let (x, y) = image.dimensions();
133        (x as f32 * INCH_TO_MM, y as f32 * INCH_TO_MM)
134    };
135
136    let width = width.constrain(dimensions.0);
137
138    let size = (width, dimensions.1 * width / dimensions.0);
139
140    (
141        size.1,
142        ElementSize {
143            width: Some(size.0),
144            height: Some(size.1),
145        },
146    )
147}