use std::collections::HashMap;
use std::io::Write;
use crate::object::Object;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageFormat {
Jpeg,
Png,
Raw,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorSpace {
DeviceGray,
DeviceRGB,
DeviceCMYK,
}
impl ColorSpace {
pub fn components(&self) -> u8 {
match self {
ColorSpace::DeviceGray => 1,
ColorSpace::DeviceRGB => 3,
ColorSpace::DeviceCMYK => 4,
}
}
pub fn pdf_name(&self) -> &'static str {
match self {
ColorSpace::DeviceGray => "DeviceGray",
ColorSpace::DeviceRGB => "DeviceRGB",
ColorSpace::DeviceCMYK => "DeviceCMYK",
}
}
}
#[derive(Debug, Clone)]
pub struct ImageData {
pub width: u32,
pub height: u32,
pub bits_per_component: u8,
pub color_space: ColorSpace,
pub format: ImageFormat,
pub data: Vec<u8>,
pub soft_mask: Option<Vec<u8>>,
}
impl ImageData {
pub fn new(width: u32, height: u32, color_space: ColorSpace, data: Vec<u8>) -> Self {
Self {
width,
height,
bits_per_component: 8,
color_space,
format: ImageFormat::Raw,
data,
soft_mask: None,
}
}
pub fn from_jpeg(data: Vec<u8>) -> Result<Self, ImageError> {
let (width, height, color_space) = parse_jpeg_header(&data)?;
Ok(Self {
width,
height,
bits_per_component: 8,
color_space,
format: ImageFormat::Jpeg,
data,
soft_mask: None,
})
}
pub fn from_png(data: &[u8]) -> Result<Self, ImageError> {
use image::GenericImageView;
let img = image::load_from_memory_with_format(data, image::ImageFormat::Png)
.map_err(|e| ImageError::DecodeError(e.to_string()))?;
let (width, height) = img.dimensions();
let (color_space, pixels, alpha) = match img.color() {
image::ColorType::L8 | image::ColorType::L16 => {
let gray = img.to_luma8();
(ColorSpace::DeviceGray, gray.into_raw(), None)
},
image::ColorType::La8 | image::ColorType::La16 => {
let rgba = img.to_luma_alpha8();
let mut gray = Vec::with_capacity((width * height) as usize);
let mut alpha_channel = Vec::with_capacity((width * height) as usize);
for pixel in rgba.pixels() {
gray.push(pixel.0[0]);
alpha_channel.push(pixel.0[1]);
}
(ColorSpace::DeviceGray, gray, Some(alpha_channel))
},
image::ColorType::Rgb8 | image::ColorType::Rgb16 => {
let rgb = img.to_rgb8();
(ColorSpace::DeviceRGB, rgb.into_raw(), None)
},
image::ColorType::Rgba8 | image::ColorType::Rgba16 => {
let rgba = img.to_rgba8();
let mut rgb = Vec::with_capacity((width * height * 3) as usize);
let mut alpha_channel = Vec::with_capacity((width * height) as usize);
for pixel in rgba.pixels() {
rgb.push(pixel.0[0]);
rgb.push(pixel.0[1]);
rgb.push(pixel.0[2]);
alpha_channel.push(pixel.0[3]);
}
(ColorSpace::DeviceRGB, rgb, Some(alpha_channel))
},
_ => {
let rgb = img.to_rgb8();
(ColorSpace::DeviceRGB, rgb.into_raw(), None)
},
};
let compressed = compress_image_data(&pixels)?;
Ok(Self {
width,
height,
bits_per_component: 8,
color_space,
format: ImageFormat::Png,
data: compressed,
soft_mask: alpha.map(|a| compress_image_data(&a)).transpose()?,
})
}
pub fn from_bytes(data: &[u8]) -> Result<Self, ImageError> {
if data.len() >= 2 && data[0] == 0xFF && data[1] == 0xD8 {
return Self::from_jpeg(data.to_vec());
}
if data.len() >= 8 && &data[0..8] == b"\x89PNG\r\n\x1a\n" {
return Self::from_png(data);
}
Err(ImageError::UnsupportedFormat)
}
pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Self, ImageError> {
let data = std::fs::read(path.as_ref()).map_err(|e| ImageError::IoError(e.to_string()))?;
Self::from_bytes(&data)
}
pub fn build_xobject_dict(&self) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("Type".to_string(), Object::Name("XObject".to_string()));
dict.insert("Subtype".to_string(), Object::Name("Image".to_string()));
dict.insert("Width".to_string(), Object::Integer(self.width as i64));
dict.insert("Height".to_string(), Object::Integer(self.height as i64));
dict.insert(
"ColorSpace".to_string(),
Object::Name(self.color_space.pdf_name().to_string()),
);
dict.insert(
"BitsPerComponent".to_string(),
Object::Integer(self.bits_per_component as i64),
);
match self.format {
ImageFormat::Jpeg => {
dict.insert("Filter".to_string(), Object::Name("DCTDecode".to_string()));
},
ImageFormat::Png => {
dict.insert("Filter".to_string(), Object::Name("FlateDecode".to_string()));
let mut decode_parms = HashMap::new();
decode_parms.insert("Predictor".to_string(), Object::Integer(15));
decode_parms.insert(
"Colors".to_string(),
Object::Integer(self.color_space.components() as i64),
);
decode_parms.insert(
"BitsPerComponent".to_string(),
Object::Integer(self.bits_per_component as i64),
);
decode_parms.insert("Columns".to_string(), Object::Integer(self.width as i64));
dict.insert("DecodeParms".to_string(), Object::Dictionary(decode_parms));
},
ImageFormat::Raw => {
},
}
dict.insert("Length".to_string(), Object::Integer(self.data.len() as i64));
dict
}
pub fn build_soft_mask_dict(&self) -> Option<HashMap<String, Object>> {
self.soft_mask.as_ref().map(|mask_data| {
let mut dict = HashMap::new();
dict.insert("Type".to_string(), Object::Name("XObject".to_string()));
dict.insert("Subtype".to_string(), Object::Name("Image".to_string()));
dict.insert("Width".to_string(), Object::Integer(self.width as i64));
dict.insert("Height".to_string(), Object::Integer(self.height as i64));
dict.insert("ColorSpace".to_string(), Object::Name("DeviceGray".to_string()));
dict.insert("BitsPerComponent".to_string(), Object::Integer(8));
dict.insert("Filter".to_string(), Object::Name("FlateDecode".to_string()));
dict.insert("Length".to_string(), Object::Integer(mask_data.len() as i64));
dict
})
}
pub fn aspect_ratio(&self) -> f32 {
self.width as f32 / self.height as f32
}
pub fn fit_to_box(&self, max_width: f32, max_height: f32) -> (f32, f32) {
let aspect = self.aspect_ratio();
let box_aspect = max_width / max_height;
if aspect > box_aspect {
(max_width, max_width / aspect)
} else {
(max_height * aspect, max_height)
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ImageError {
#[error("Unsupported image format")]
UnsupportedFormat,
#[error("Failed to decode image: {0}")]
DecodeError(String),
#[error("Compression error: {0}")]
CompressionError(String),
#[error("IO error: {0}")]
IoError(String),
#[error("Invalid image data: {0}")]
InvalidData(String),
}
fn parse_jpeg_header(data: &[u8]) -> Result<(u32, u32, ColorSpace), ImageError> {
if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
return Err(ImageError::InvalidData("Not a valid JPEG".to_string()));
}
let mut pos = 2;
while pos < data.len() - 1 {
if data[pos] != 0xFF {
pos += 1;
continue;
}
let marker = data[pos + 1];
pos += 2;
if marker == 0xFF || marker == 0x00 {
continue;
}
if matches!(
marker,
0xC0 | 0xC1
| 0xC2
| 0xC3
| 0xC5
| 0xC6
| 0xC7
| 0xC9
| 0xCA
| 0xCB
| 0xCD
| 0xCE
| 0xCF
) {
if pos + 7 > data.len() {
return Err(ImageError::InvalidData("Truncated JPEG header".to_string()));
}
let _precision = data[pos + 2];
let height = u16::from_be_bytes([data[pos + 3], data[pos + 4]]) as u32;
let width = u16::from_be_bytes([data[pos + 5], data[pos + 6]]) as u32;
let components = data[pos + 7];
let color_space = match components {
1 => ColorSpace::DeviceGray,
3 => ColorSpace::DeviceRGB,
4 => ColorSpace::DeviceCMYK,
_ => ColorSpace::DeviceRGB,
};
return Ok((width, height, color_space));
}
if pos + 2 > data.len() {
break;
}
let length = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
pos += length;
}
Err(ImageError::InvalidData("Could not find JPEG dimensions".to_string()))
}
fn compress_image_data(data: &[u8]) -> Result<Vec<u8>, ImageError> {
use flate2::write::ZlibEncoder;
use flate2::Compression;
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder
.write_all(data)
.map_err(|e| ImageError::CompressionError(e.to_string()))?;
encoder
.finish()
.map_err(|e| ImageError::CompressionError(e.to_string()))
}
#[derive(Debug, Clone)]
pub struct ImagePlacement {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl ImagePlacement {
pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
Self {
x,
y,
width,
height,
}
}
pub fn at_origin(width: f32, height: f32) -> Self {
Self::new(0.0, 0.0, width, height)
}
pub fn transform_matrix(&self) -> (f32, f32, f32, f32, f32, f32) {
(self.width, 0.0, 0.0, self.height, self.x, self.y)
}
}
#[derive(Debug, Default)]
pub struct ImageManager {
images: HashMap<String, ImageData>,
resource_ids: HashMap<String, String>,
next_id: u32,
}
impl ImageManager {
pub fn new() -> Self {
Self {
images: HashMap::new(),
resource_ids: HashMap::new(),
next_id: 1,
}
}
pub fn register(&mut self, name: impl Into<String>, image: ImageData) -> String {
let name = name.into();
let resource_id = format!("Im{}", self.next_id);
self.next_id += 1;
self.resource_ids.insert(name.clone(), resource_id.clone());
self.images.insert(name, image);
resource_id
}
pub fn register_from_file(
&mut self,
name: impl Into<String>,
path: impl AsRef<std::path::Path>,
) -> Result<String, ImageError> {
let image = ImageData::from_file(path)?;
Ok(self.register(name, image))
}
pub fn get(&self, name: &str) -> Option<&ImageData> {
self.images.get(name)
}
pub fn resource_id(&self, name: &str) -> Option<&str> {
self.resource_ids.get(name).map(|s| s.as_str())
}
pub fn images(&self) -> impl Iterator<Item = (&str, &ImageData)> {
self.images.iter().map(|(k, v)| (k.as_str(), v))
}
pub fn images_with_ids(&self) -> impl Iterator<Item = (&str, &str, &ImageData)> {
self.images.iter().filter_map(|(name, image)| {
self.resource_ids
.get(name)
.map(|id| (name.as_str(), id.as_str(), image))
})
}
pub fn len(&self) -> usize {
self.images.len()
}
pub fn is_empty(&self) -> bool {
self.images.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_space_components() {
assert_eq!(ColorSpace::DeviceGray.components(), 1);
assert_eq!(ColorSpace::DeviceRGB.components(), 3);
assert_eq!(ColorSpace::DeviceCMYK.components(), 4);
}
#[test]
fn test_color_space_pdf_name() {
assert_eq!(ColorSpace::DeviceGray.pdf_name(), "DeviceGray");
assert_eq!(ColorSpace::DeviceRGB.pdf_name(), "DeviceRGB");
assert_eq!(ColorSpace::DeviceCMYK.pdf_name(), "DeviceCMYK");
}
#[test]
fn test_image_aspect_ratio() {
let image = ImageData::new(200, 100, ColorSpace::DeviceRGB, vec![]);
assert!((image.aspect_ratio() - 2.0).abs() < 0.001);
}
#[test]
fn test_image_fit_to_box() {
let image = ImageData::new(200, 100, ColorSpace::DeviceRGB, vec![]);
let (w, h) = image.fit_to_box(100.0, 100.0);
assert!((w - 100.0).abs() < 0.001);
assert!((h - 50.0).abs() < 0.001);
let (w, h) = image.fit_to_box(100.0, 200.0);
assert!((w - 100.0).abs() < 0.001);
assert!((h - 50.0).abs() < 0.001);
}
#[test]
fn test_image_placement_transform() {
let placement = ImagePlacement::new(100.0, 200.0, 50.0, 75.0);
let (a, b, c, d, e, f) = placement.transform_matrix();
assert!((a - 50.0).abs() < 0.001);
assert!((d - 75.0).abs() < 0.001);
assert!((e - 100.0).abs() < 0.001);
assert!((f - 200.0).abs() < 0.001);
}
#[test]
fn test_image_manager() {
let mut manager = ImageManager::new();
assert!(manager.is_empty());
let image = ImageData::new(100, 100, ColorSpace::DeviceRGB, vec![0; 30000]);
let id = manager.register("test", image);
assert!(!manager.is_empty());
assert_eq!(manager.len(), 1);
assert!(manager.get("test").is_some());
assert_eq!(manager.resource_id("test"), Some(id.as_str()));
}
#[test]
fn test_xobject_dict_jpeg() {
let mut image = ImageData::new(100, 50, ColorSpace::DeviceRGB, vec![0xFF, 0xD8]);
image.format = ImageFormat::Jpeg;
let dict = image.build_xobject_dict();
assert_eq!(dict.get("Type"), Some(&Object::Name("XObject".to_string())));
assert_eq!(dict.get("Subtype"), Some(&Object::Name("Image".to_string())));
assert_eq!(dict.get("Width"), Some(&Object::Integer(100)));
assert_eq!(dict.get("Height"), Some(&Object::Integer(50)));
assert_eq!(dict.get("Filter"), Some(&Object::Name("DCTDecode".to_string())));
}
#[test]
fn test_invalid_jpeg_header() {
let result = parse_jpeg_header(&[0x00, 0x00]);
assert!(matches!(result, Err(ImageError::InvalidData(_))));
}
}