Skip to main content

graphitepdf_kit/
image_render.rs

1use std::fmt::Write as _;
2use std::io::Cursor;
3
4use crate::error::{GraphitePdfKitError, Result};
5use crate::svg_render::{SvgRenderOptions, render_svg_node_to_page_content_with_options};
6use graphitepdf_image::{Image, ImageFormat, RasterImage};
7
8#[derive(Clone, Debug, PartialEq)]
9pub struct ImageRenderOptions {
10    pub x: f64,
11    pub y: f64,
12    pub width: Option<f64>,
13    pub height: Option<f64>,
14}
15
16impl ImageRenderOptions {
17    pub fn new() -> Self {
18        Self::default()
19    }
20
21    pub fn position(mut self, x: f64, y: f64) -> Self {
22        self.x = x;
23        self.y = y;
24        self
25    }
26
27    pub fn width(mut self, width: f64) -> Self {
28        self.width = Some(width);
29        self
30    }
31
32    pub fn height(mut self, height: f64) -> Self {
33        self.height = Some(height);
34        self
35    }
36
37    pub fn size(mut self, width: f64, height: f64) -> Self {
38        self.width = Some(width);
39        self.height = Some(height);
40        self
41    }
42}
43
44impl Default for ImageRenderOptions {
45    fn default() -> Self {
46        Self {
47            x: 0.0,
48            y: 0.0,
49            width: None,
50            height: None,
51        }
52    }
53}
54
55pub fn render_image_to_page_content(image: &Image) -> Result<Vec<u8>> {
56    render_image_to_page_content_with_options(image, &ImageRenderOptions::default())
57}
58
59pub fn render_image_to_page_content_with_options(
60    image: &Image,
61    options: &ImageRenderOptions,
62) -> Result<Vec<u8>> {
63    match image {
64        Image::Raster(raster) => render_raster_to_page_content(raster, options),
65        Image::Svg(svg) => {
66            let (width, height) = resolve_size(svg.width as f64, svg.height as f64, options)?;
67            render_svg_node_to_page_content_with_options(
68                &svg.data,
69                &SvgRenderOptions::new()
70                    .position(options.x, options.y)
71                    .size(width, height),
72            )
73        }
74    }
75}
76
77fn render_raster_to_page_content(
78    raster: &RasterImage,
79    options: &ImageRenderOptions,
80) -> Result<Vec<u8>> {
81    let (width, height) = resolve_size(raster.width as f64, raster.height as f64, options)?;
82    let decoded = decode_raster_image(raster)?;
83    let mut content = String::new();
84
85    content.push_str("q\n");
86    let _ = writeln!(
87        content,
88        "{} 0 0 {} {} {} cm",
89        format_number(width),
90        format_number(height),
91        format_number(options.x),
92        format_number(options.y)
93    );
94    content.push_str("BI\n");
95    let _ = writeln!(content, "/Width {}", decoded.width);
96    let _ = writeln!(content, "/Height {}", decoded.height);
97    let _ = writeln!(content, "/ColorSpace /{}", decoded.color_space);
98    content.push_str("/BitsPerComponent 8\n");
99    content.push_str("/Filter /ASCIIHexDecode\n");
100    content.push_str("ID\n");
101    content.push_str(&hex_encode(&decoded.data));
102    content.push_str(">\nEI\nQ\n");
103
104    Ok(content.into_bytes())
105}
106
107fn resolve_size(
108    natural_width: f64,
109    natural_height: f64,
110    options: &ImageRenderOptions,
111) -> Result<(f64, f64)> {
112    if natural_width <= 0.0 || natural_height <= 0.0 {
113        return Err(GraphitePdfKitError::ImageError(
114            "image dimensions must be positive".to_string(),
115        ));
116    }
117
118    let size = match (options.width, options.height) {
119        (Some(width), Some(height)) => (width, height),
120        (Some(width), None) => (width, width * (natural_height / natural_width)),
121        (None, Some(height)) => (height * (natural_width / natural_height), height),
122        (None, None) => (natural_width, natural_height),
123    };
124
125    if size.0 <= 0.0 || size.1 <= 0.0 {
126        Err(GraphitePdfKitError::ImageError(
127            "rendered image dimensions must be positive".to_string(),
128        ))
129    } else {
130        Ok(size)
131    }
132}
133
134#[derive(Clone, Debug, PartialEq, Eq)]
135struct DecodedRaster {
136    width: u32,
137    height: u32,
138    color_space: &'static str,
139    data: Vec<u8>,
140}
141
142#[cfg(feature = "images")]
143fn decode_raster_image(raster: &RasterImage) -> Result<DecodedRaster> {
144    match raster.format {
145        ImageFormat::Png => decode_png(raster),
146        ImageFormat::Jpeg => decode_jpeg(raster),
147        ImageFormat::Svg => Err(GraphitePdfKitError::UnsupportedFeature(
148            "SVG raster decoding is not supported",
149        )),
150    }
151}
152
153#[cfg(not(feature = "images"))]
154fn decode_raster_image(_raster: &RasterImage) -> Result<DecodedRaster> {
155    Err(GraphitePdfKitError::UnsupportedFeature(
156        "image decoding requires the `images` feature",
157    ))
158}
159
160#[cfg(feature = "images")]
161fn decode_png(raster: &RasterImage) -> Result<DecodedRaster> {
162    let mut decoder = png::Decoder::new(Cursor::new(&raster.data));
163    decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
164    let mut reader = decoder.read_info().map_err(|error| {
165        GraphitePdfKitError::ImageError(format!("failed to decode PNG: {error}"))
166    })?;
167    let output_size = reader.output_buffer_size().ok_or_else(|| {
168        GraphitePdfKitError::ImageError("PNG decoder did not report an output size".to_string())
169    })?;
170    let mut buffer = vec![0; output_size];
171    let info = reader.next_frame(&mut buffer).map_err(|error| {
172        GraphitePdfKitError::ImageError(format!("failed to read PNG frame: {error}"))
173    })?;
174    let data = &buffer[..info.buffer_size()];
175
176    let rgb = match info.color_type {
177        png::ColorType::Rgb => data.to_vec(),
178        png::ColorType::Rgba => data
179            .chunks_exact(4)
180            .flat_map(|chunk| [chunk[0], chunk[1], chunk[2]])
181            .collect(),
182        png::ColorType::Grayscale => data
183            .iter()
184            .flat_map(|value| [*value, *value, *value])
185            .collect(),
186        png::ColorType::GrayscaleAlpha => data
187            .chunks_exact(2)
188            .flat_map(|chunk| [chunk[0], chunk[0], chunk[0]])
189            .collect(),
190        other => {
191            return Err(GraphitePdfKitError::ImageError(format!(
192                "unsupported PNG color type {other:?}"
193            )));
194        }
195    };
196
197    Ok(DecodedRaster {
198        width: info.width,
199        height: info.height,
200        color_space: "DeviceRGB",
201        data: rgb,
202    })
203}
204
205#[cfg(feature = "images")]
206fn decode_jpeg(raster: &RasterImage) -> Result<DecodedRaster> {
207    let mut decoder = jpeg_decoder::Decoder::new(Cursor::new(&raster.data));
208    let pixels = decoder.decode().map_err(|error| {
209        GraphitePdfKitError::ImageError(format!("failed to decode JPEG: {error}"))
210    })?;
211    let info = decoder.info().ok_or_else(|| {
212        GraphitePdfKitError::ImageError("JPEG decoder did not return image info".to_string())
213    })?;
214
215    let rgb = match info.pixel_format {
216        jpeg_decoder::PixelFormat::L8 => pixels
217            .iter()
218            .flat_map(|value| [*value, *value, *value])
219            .collect(),
220        jpeg_decoder::PixelFormat::RGB24 => pixels,
221        jpeg_decoder::PixelFormat::CMYK32 => pixels
222            .chunks_exact(4)
223            .flat_map(|chunk| cmyk_to_rgb(chunk[0], chunk[1], chunk[2], chunk[3]))
224            .collect(),
225        other => {
226            return Err(GraphitePdfKitError::ImageError(format!(
227                "unsupported JPEG pixel format {other:?}"
228            )));
229        }
230    };
231
232    Ok(DecodedRaster {
233        width: u32::from(info.width),
234        height: u32::from(info.height),
235        color_space: "DeviceRGB",
236        data: rgb,
237    })
238}
239
240#[cfg(feature = "images")]
241fn cmyk_to_rgb(c: u8, m: u8, y: u8, k: u8) -> [u8; 3] {
242    let c = f32::from(c) / 255.0;
243    let m = f32::from(m) / 255.0;
244    let y = f32::from(y) / 255.0;
245    let k = f32::from(k) / 255.0;
246
247    [
248        ((1.0 - (c * (1.0 - k) + k)) * 255.0).round() as u8,
249        ((1.0 - (m * (1.0 - k) + k)) * 255.0).round() as u8,
250        ((1.0 - (y * (1.0 - k) + k)) * 255.0).round() as u8,
251    ]
252}
253
254fn hex_encode(data: &[u8]) -> String {
255    let mut encoded = String::with_capacity(data.len() * 2);
256    for byte in data {
257        let _ = write!(encoded, "{byte:02X}");
258    }
259    encoded
260}
261
262fn format_number(value: f64) -> String {
263    let rounded = (value * 1000.0).round() / 1000.0;
264    let mut rendered = format!("{rounded:.3}");
265    while rendered.contains('.') && rendered.ends_with('0') {
266        rendered.pop();
267    }
268    if rendered.ends_with('.') {
269        rendered.pop();
270    }
271    if rendered == "-0" {
272        String::from("0")
273    } else {
274        rendered
275    }
276}