use lightweight_pdf_core::ImageFormat;
use lightweight_pdf_writer::{ColorSpace, ImageDataFilter, ImageXObject};
#[derive(Debug)]
pub enum ImageEmbedError {
PngFeatureDisabled,
DecodeFailed,
}
impl core::fmt::Display for ImageEmbedError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ImageEmbedError::PngFeatureDisabled => write!(f, "PNG image in document, but the `png` feature is disabled"),
ImageEmbedError::DecodeFailed => write!(f, "failed to decode PNG image data"),
}
}
}
#[cfg(feature = "png")]
const MAX_DECOMPRESSED_BYTES: usize = 200_000_000;
pub fn build_pdf_image(bytes: &[u8], format: ImageFormat, components: u8) -> Result<ImageXObject, ImageEmbedError> {
match format {
ImageFormat::Jpeg => Ok(build_jpeg(bytes, components)),
ImageFormat::Png => build_png(bytes),
}
}
fn build_jpeg(bytes: &[u8], components: u8) -> ImageXObject {
let color_space = if components == 1 {
ColorSpace::DeviceGray
} else {
ColorSpace::DeviceRgb
};
ImageXObject {
width_px: 0,
height_px: 0,
color_space,
bits_per_component: 8,
filter: ImageDataFilter::DctDecode,
bytes: bytes.to_vec(),
smask: None,
}
}
#[cfg(feature = "png")]
fn build_png(bytes: &[u8]) -> Result<ImageXObject, ImageEmbedError> {
let limits = png::Limits {
bytes: MAX_DECOMPRESSED_BYTES,
};
let decoder = png::Decoder::new_with_limits(std::io::Cursor::new(bytes), limits);
let mut reader = decoder.read_info().map_err(|_| ImageEmbedError::DecodeFailed)?;
let mut buf = vec![0u8; reader.output_buffer_size().ok_or(ImageEmbedError::DecodeFailed)?];
let info = reader.next_frame(&mut buf).map_err(|_| ImageEmbedError::DecodeFailed)?;
let pixels = &buf[..info.buffer_size()];
match info.color_type {
png::ColorType::Rgb => Ok(ImageXObject {
width_px: info.width,
height_px: info.height,
color_space: ColorSpace::DeviceRgb,
bits_per_component: 8,
filter: ImageDataFilter::None,
bytes: pixels.to_vec(),
smask: None,
}),
png::ColorType::Rgba => {
let width = usize::try_from(info.width).expect("u32 width fits in usize on every supported target");
let height = usize::try_from(info.height).expect("u32 height fits in usize on every supported target");
let pixel_count = width.checked_mul(height).ok_or(ImageEmbedError::DecodeFailed)?;
let mut rgb = Vec::with_capacity(pixel_count * 3);
let mut alpha = Vec::with_capacity(pixel_count);
for px in pixels.chunks_exact(4) {
rgb.extend_from_slice(&px[0..3]);
alpha.push(px[3]);
}
let smask = ImageXObject {
width_px: info.width,
height_px: info.height,
color_space: ColorSpace::DeviceGray,
bits_per_component: 8,
filter: ImageDataFilter::None,
bytes: alpha,
smask: None,
};
Ok(ImageXObject {
width_px: info.width,
height_px: info.height,
color_space: ColorSpace::DeviceRgb,
bits_per_component: 8,
filter: ImageDataFilter::None,
bytes: rgb,
smask: Some(Box::new(smask)),
})
}
_ => Err(ImageEmbedError::DecodeFailed),
}
}
#[cfg(not(feature = "png"))]
fn build_png(_bytes: &[u8]) -> Result<ImageXObject, ImageEmbedError> {
Err(ImageEmbedError::PngFeatureDisabled)
}