lightweight_pdf/
images.rs1use lightweight_pdf_core::ImageFormat;
13use lightweight_pdf_writer::{ColorSpace, ImageDataFilter, ImageXObject};
14
15#[derive(Debug)]
16pub enum ImageEmbedError {
17 PngFeatureDisabled,
20 DecodeFailed,
23}
24
25impl core::fmt::Display for ImageEmbedError {
26 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
27 match self {
28 ImageEmbedError::PngFeatureDisabled => write!(f, "PNG image in document, but the `png` feature is disabled"),
29 ImageEmbedError::DecodeFailed => write!(f, "failed to decode PNG image data"),
30 }
31 }
32}
33
34#[cfg(feature = "png")]
39const MAX_DECOMPRESSED_BYTES: usize = 200_000_000;
40
41pub fn build_pdf_image(bytes: &[u8], format: ImageFormat, components: u8) -> Result<ImageXObject, ImageEmbedError> {
42 match format {
43 ImageFormat::Jpeg => Ok(build_jpeg(bytes, components)),
44 ImageFormat::Png => build_png(bytes),
45 }
46}
47
48fn build_jpeg(bytes: &[u8], components: u8) -> ImageXObject {
49 let color_space = if components == 1 {
50 ColorSpace::DeviceGray
51 } else {
52 ColorSpace::DeviceRgb
53 };
54 ImageXObject {
55 width_px: 0,
62 height_px: 0,
63 color_space,
64 bits_per_component: 8,
65 filter: ImageDataFilter::DctDecode,
66 bytes: bytes.to_vec(),
67 smask: None,
68 }
69}
70
71#[cfg(feature = "png")]
72fn build_png(bytes: &[u8]) -> Result<ImageXObject, ImageEmbedError> {
73 let limits = png::Limits {
74 bytes: MAX_DECOMPRESSED_BYTES,
75 };
76 let decoder = png::Decoder::new_with_limits(std::io::Cursor::new(bytes), limits);
77 let mut reader = decoder.read_info().map_err(|_| ImageEmbedError::DecodeFailed)?;
78 let mut buf = vec![0u8; reader.output_buffer_size().ok_or(ImageEmbedError::DecodeFailed)?];
79 let info = reader.next_frame(&mut buf).map_err(|_| ImageEmbedError::DecodeFailed)?;
80 let pixels = &buf[..info.buffer_size()];
81
82 match info.color_type {
83 png::ColorType::Rgb => Ok(ImageXObject {
84 width_px: info.width,
85 height_px: info.height,
86 color_space: ColorSpace::DeviceRgb,
87 bits_per_component: 8,
88 filter: ImageDataFilter::None,
89 bytes: pixels.to_vec(),
90 smask: None,
91 }),
92 png::ColorType::Rgba => {
93 let width = usize::try_from(info.width).expect("u32 width fits in usize on every supported target");
103 let height = usize::try_from(info.height).expect("u32 height fits in usize on every supported target");
104 let pixel_count = width.checked_mul(height).ok_or(ImageEmbedError::DecodeFailed)?;
105 let mut rgb = Vec::with_capacity(pixel_count * 3);
106 let mut alpha = Vec::with_capacity(pixel_count);
107 for px in pixels.chunks_exact(4) {
108 rgb.extend_from_slice(&px[0..3]);
109 alpha.push(px[3]);
110 }
111 let smask = ImageXObject {
112 width_px: info.width,
113 height_px: info.height,
114 color_space: ColorSpace::DeviceGray,
115 bits_per_component: 8,
116 filter: ImageDataFilter::None,
117 bytes: alpha,
118 smask: None,
119 };
120 Ok(ImageXObject {
121 width_px: info.width,
122 height_px: info.height,
123 color_space: ColorSpace::DeviceRgb,
124 bits_per_component: 8,
125 filter: ImageDataFilter::None,
126 bytes: rgb,
127 smask: Some(Box::new(smask)),
128 })
129 }
130 _ => Err(ImageEmbedError::DecodeFailed),
134 }
135}
136
137#[cfg(not(feature = "png"))]
138fn build_png(_bytes: &[u8]) -> Result<ImageXObject, ImageEmbedError> {
139 Err(ImageEmbedError::PngFeatureDisabled)
140}