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}