use serde_derive::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, PartialOrd, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ImageOptimizationOptions {
#[serde(default = "default_quality", skip_serializing_if = "Option::is_none")]
pub quality: Option<f32>,
#[serde(
default = "default_max_img_size",
skip_serializing_if = "Option::is_none"
)]
pub max_image_size: Option<String>,
#[serde(
default = "default_dither_greyscale",
skip_serializing_if = "Option::is_none"
)]
pub dither_greyscale: Option<bool>,
#[serde(
default = "default_convert_to_greyscale",
skip_serializing_if = "Option::is_none"
)]
pub convert_to_greyscale: Option<bool>,
#[serde(
default = "default_auto_optimize",
skip_serializing_if = "Option::is_none"
)]
pub auto_optimize: Option<bool>,
#[serde(default = "default_format", skip_serializing_if = "Option::is_none")]
pub format: Option<ImageCompression>,
}
const fn default_quality() -> Option<f32> {
Some(0.85)
}
fn default_max_img_size() -> Option<String> {
Some("2MB".to_string())
}
const fn default_convert_to_greyscale() -> Option<bool> {
Some(false)
}
const fn default_dither_greyscale() -> Option<bool> {
None
}
const fn default_auto_optimize() -> Option<bool> {
Some(true)
}
const fn default_format() -> Option<ImageCompression> {
Some(ImageCompression::Auto)
}
impl Default for ImageOptimizationOptions {
fn default() -> Self {
ImageOptimizationOptions {
quality: default_quality(),
convert_to_greyscale: default_convert_to_greyscale(),
max_image_size: default_max_img_size(),
dither_greyscale: default_dither_greyscale(),
auto_optimize: default_auto_optimize(),
format: default_format(),
}
}
}
#[derive(Debug, Copy, Clone, Serialize, PartialOrd, PartialEq, Default, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ImageCompression {
#[default]
Auto,
Jpeg,
Jpeg2000,
Flate,
Lzw,
RunLength,
None,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum OutputImageFormat {
Png,
Jpeg,
Webp,
Avif,
Gif,
Bmp,
Tiff,
Tga,
Pnm,
}
impl OutputImageFormat {
pub fn mime_type(&self) -> &'static str {
match self {
Self::Png => "image/png",
Self::Jpeg => "image/jpeg",
Self::Webp => "image/webp",
Self::Avif => "image/avif",
Self::Gif => "image/gif",
Self::Bmp => "image/bmp",
Self::Tiff => "image/tiff",
Self::Tga => "image/x-tga",
Self::Pnm => "image/x-portable-anymap",
}
}
}
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
#[serde(rename_all = "lowercase")]
pub enum RawImageFormat {
R8,
RG8,
RGB8,
RGBA8,
R16,
RG16,
RGB16,
RGBA16,
BGR8,
BGRA8,
RGBF32,
RGBAF32,
}
#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(try_from = "RawImageDataWire", into = "RawImageDataWire")]
pub enum RawImageData {
U8(Vec<u8>),
U16(Vec<u16>),
F32(Vec<f32>),
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum RawImageDataWire {
U8(Vec<u8>),
U16(Vec<u16>),
F32(Vec<f32>),
U8B64(String),
U16B64(String),
F32B64(String),
}
impl From<RawImageData> for RawImageDataWire {
fn from(d: RawImageData) -> Self {
use base64::Engine;
let b64 = |bytes: &[u8]| base64::prelude::BASE64_STANDARD.encode(bytes);
match d {
RawImageData::U8(v) => RawImageDataWire::U8B64(b64(&v)),
RawImageData::U16(v) => {
let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
RawImageDataWire::U16B64(b64(&bytes))
}
RawImageData::F32(v) => {
let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
RawImageDataWire::F32B64(b64(&bytes))
}
}
}
}
impl TryFrom<RawImageDataWire> for RawImageData {
type Error = String;
fn try_from(w: RawImageDataWire) -> Result<Self, String> {
use base64::Engine;
let dec = |s: &str| {
base64::prelude::BASE64_STANDARD
.decode(s)
.map_err(|e| format!("pixel data base64: {e}"))
};
Ok(match w {
RawImageDataWire::U8(v) => RawImageData::U8(v),
RawImageDataWire::U16(v) => RawImageData::U16(v),
RawImageDataWire::F32(v) => RawImageData::F32(v),
RawImageDataWire::U8B64(s) => RawImageData::U8(dec(&s)?),
RawImageDataWire::U16B64(s) => {
let b = dec(&s)?;
if b.len() % 2 != 0 {
return Err("u16b64 pixel data has odd byte length".to_string());
}
RawImageData::U16(
b.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect(),
)
}
RawImageDataWire::F32B64(s) => {
let b = dec(&s)?;
if b.len() % 4 != 0 {
return Err("f32b64 pixel data length not a multiple of 4".to_string());
}
RawImageData::F32(
b.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect(),
)
}
})
}
}
#[cfg(test)]
mod pixel_wire_tests {
use super::*;
#[test]
fn pixels_serialize_as_base64_and_accept_legacy_arrays() {
let d = RawImageData::U8(vec![1, 2, 3, 255]);
let json = serde_json::to_string(&d).unwrap();
assert!(json.contains("u8b64"), "new format must be base64: {json}");
assert_eq!(serde_json::from_str::<RawImageData>(&json).unwrap(), d);
let legacy: RawImageData = serde_json::from_str(r#"{"u8":[1,2,3,255]}"#).unwrap();
assert_eq!(legacy, d);
let d16 = RawImageData::U16(vec![0x0102, 0xFFEE]);
let json16 = serde_json::to_string(&d16).unwrap();
assert_eq!(serde_json::from_str::<RawImageData>(&json16).unwrap(), d16);
}
}
#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct RawImage {
pub pixels: RawImageData,
pub width: usize,
pub height: usize,
pub data_format: RawImageFormat,
pub tag: Vec<u8>,
}
impl RawImage {
#[cfg(not(feature = "images"))]
pub fn decode_from_bytes(_bytes: &[u8], _warnings: &mut Vec<crate::PdfWarnMsg>) -> Result<Self, String> {
Err("RawImage::decode_from_bytes requires the 'images' feature".to_string())
}
#[cfg(not(feature = "images"))]
pub async fn decode_from_bytes_async(_bytes: &[u8], _warnings: &mut Vec<crate::PdfWarnMsg>) -> Result<Self, String> {
Err("RawImage::decode_from_bytes_async requires the 'images' feature".to_string())
}
#[cfg(not(feature = "images"))]
pub fn encode_to_bytes(&self, formats: &[OutputImageFormat]) -> Result<(Vec<u8>, OutputImageFormat), String> {
let png_data = vec![
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89,
0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01,
0x0D, 0x0A, 0x2D, 0xB4,
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
];
let format = formats.first().copied().unwrap_or(OutputImageFormat::Png);
Ok((png_data, format))
}
#[cfg(not(feature = "images"))]
pub async fn encode_to_bytes_async(&self, formats: &[OutputImageFormat]) -> Result<(Vec<u8>, OutputImageFormat), String> {
self.encode_to_bytes(formats)
}
}
pub(crate) fn raw_image_to_basic_stream(
im: &RawImage,
doc: &mut lopdf::Document,
) -> lopdf::Stream {
use lopdf::Object::{Integer, Name, Reference};
fn to_u8(px: &RawImageData) -> Vec<u8> {
match px {
RawImageData::U8(v) => v.clone(),
RawImageData::U16(v) => v.iter().map(|x| (x >> 8) as u8).collect(),
RawImageData::F32(v) => v
.iter()
.map(|x| (x.clamp(0.0, 1.0) * 255.0) as u8)
.collect(),
}
}
let raw = to_u8(&im.pixels);
let (ncolor, has_alpha, bgr, cs) = match im.data_format {
RawImageFormat::R8 | RawImageFormat::R16 => (1, false, false, "DeviceGray"),
RawImageFormat::RG8 | RawImageFormat::RG16 => (1, true, false, "DeviceGray"),
RawImageFormat::RGB8 | RawImageFormat::RGB16 | RawImageFormat::RGBF32 => {
(3, false, false, "DeviceRGB")
}
RawImageFormat::RGBA8 | RawImageFormat::RGBA16 | RawImageFormat::RGBAF32 => {
(3, true, false, "DeviceRGB")
}
RawImageFormat::BGR8 => (3, false, true, "DeviceRGB"),
RawImageFormat::BGRA8 => (3, true, true, "DeviceRGB"),
};
let stride = ncolor + usize::from(has_alpha);
let mut color = Vec::with_capacity(im.width * im.height * ncolor);
let mut alpha = if has_alpha {
Vec::with_capacity(im.width * im.height)
} else {
Vec::new()
};
for px in raw.chunks_exact(stride) {
match (ncolor, bgr) {
(3, false) => color.extend_from_slice(&px[0..3]),
(3, true) => color.extend_from_slice(&[px[2], px[1], px[0]]),
_ => color.push(px[0]),
}
if has_alpha {
alpha.push(px[stride - 1]);
}
}
fn flate(data: &[u8]) -> Vec<u8> {
use std::io::Write;
let mut e =
flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
let _ = e.write_all(data);
e.finish().unwrap_or_default()
}
let mut dict = lopdf::Dictionary::from_iter(vec![
("Type", Name("XObject".into())),
("Subtype", Name("Image".into())),
("Width", Integer(im.width as i64)),
("Height", Integer(im.height as i64)),
("BitsPerComponent", Integer(8)),
("ColorSpace", Name(cs.into())),
("Filter", Name("FlateDecode".into())),
]);
if has_alpha {
let smask_dict = lopdf::Dictionary::from_iter(vec![
("Type", Name("XObject".into())),
("Subtype", Name("Image".into())),
("Width", Integer(im.width as i64)),
("Height", Integer(im.height as i64)),
("BitsPerComponent", Integer(8)),
("ColorSpace", Name("DeviceGray".into())),
("Filter", Name("FlateDecode".into())),
]);
let smask_id = doc.add_object(lopdf::Stream::new(smask_dict, flate(&alpha)));
dict.set("SMask", Reference(smask_id));
}
lopdf::Stream::new(dict, flate(&color)).with_compression(false)
}