use crate::utils::svg_to_pdf::SvgToPdfConverter;
use crate::{ConversionError, ConversionResult};
use duc::types::DucExternalFile;
use hipdf::blocks::BlockManager;
use hipdf::embed_pdf::PdfEmbedder;
use hipdf::hatching::HatchingManager;
use hipdf::lopdf::{content::Operation, Dictionary, Document, Object, Stream};
use hipdf::ocg::OCGManager;
use std::collections::HashMap;
#[derive(Debug)]
pub enum ResourceError {
LoadError(String),
ProcessError(String),
UnsupportedFormat(String),
EmbedError(String),
}
impl std::fmt::Display for ResourceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ResourceError::LoadError(msg) => write!(f, "Resource load error: {}", msg),
ResourceError::ProcessError(msg) => write!(f, "Resource process error: {}", msg),
ResourceError::UnsupportedFormat(msg) => write!(f, "Unsupported format: {}", msg),
ResourceError::EmbedError(msg) => write!(f, "Resource embed error: {}", msg),
}
}
}
impl std::error::Error for ResourceError {}
#[derive(Debug, Clone, PartialEq)]
pub enum ResourceType {
Svg,
Png,
Jpeg,
WebP,
Pdf,
Unsupported,
}
#[derive(Debug, Clone)]
pub struct ResourceInfo {
pub id: String,
pub resource_type: ResourceType,
pub object_id: Option<u32>,
pub width: Option<f64>,
pub height: Option<f64>,
}
pub struct ResourceStreamer {
resource_cache: HashMap<String, ResourceInfo>,
document: Option<Document>,
pdf_embedder: Option<PdfEmbedder>,
block_manager: Option<BlockManager>,
hatching_manager: Option<HatchingManager>,
ocg_manager: Option<OCGManager>,
}
impl ResourceStreamer {
pub fn new() -> Self {
Self {
resource_cache: HashMap::new(),
document: None,
pdf_embedder: None,
block_manager: None,
hatching_manager: None,
ocg_manager: None,
}
}
pub fn initialize(
&mut self,
document: &mut Document,
pdf_embedder: PdfEmbedder,
block_manager: BlockManager,
hatching_manager: HatchingManager,
ocg_manager: OCGManager,
) {
self.document = Some(document.clone());
self.pdf_embedder = Some(pdf_embedder);
self.block_manager = Some(block_manager);
self.hatching_manager = Some(hatching_manager);
self.ocg_manager = Some(ocg_manager);
}
pub fn process_external_files(
&mut self,
external_files: &[DucExternalFile],
files_data: Option<&std::collections::HashMap<String, Vec<u8>>>,
) -> ConversionResult<()> {
for file in external_files {
let rev_data = files_data
.and_then(|d| d.get(&file.active_revision_id))
.map(|b| b.as_ref() as &[u8]);
let resource_info = self.process_single_file(file, rev_data)?;
self.resource_cache.insert(file.id.clone(), resource_info);
}
Ok(())
}
fn process_single_file(
&mut self,
file: &DucExternalFile,
rev_data: Option<&[u8]>,
) -> ConversionResult<ResourceInfo> {
let mime_type = file
.revisions
.get(&file.active_revision_id)
.map(|r| r.mime_type.clone())
.unwrap_or_default();
let resource_type = self.detect_resource_type(&mime_type);
match resource_type {
ResourceType::Svg => self.process_svg_file(file, rev_data),
ResourceType::Png | ResourceType::Jpeg | ResourceType::WebP => {
self.process_image_file(file, rev_data, &resource_type)
}
ResourceType::Pdf => self.process_pdf_file(file, rev_data),
ResourceType::Unsupported => Err(ConversionError::ResourceLoadError(format!(
"Unsupported resource type: {}",
mime_type
))),
}
}
fn detect_resource_type(&self, mime_type: &str) -> ResourceType {
match mime_type.to_lowercase().as_str() {
"image/svg+xml" => ResourceType::Svg,
"image/png" => ResourceType::Png,
"image/jpeg" | "image/jpg" => ResourceType::Jpeg,
"image/webp" => ResourceType::WebP,
"application/pdf" => ResourceType::Pdf,
_ => ResourceType::Unsupported,
}
}
fn process_svg_file(
&mut self,
file: &DucExternalFile,
rev_data: Option<&[u8]>,
) -> ConversionResult<ResourceInfo> {
let _revision = file
.revisions
.get(&file.active_revision_id)
.ok_or_else(|| {
ConversionError::ResourceLoadError(format!(
"No active revision for file {}",
file.id
))
})?;
let data = rev_data.ok_or_else(|| {
ConversionError::ResourceLoadError(format!(
"No data blob for revision {}",
file.active_revision_id
))
})?;
let document = self.document.as_mut().ok_or_else(|| {
ConversionError::ResourceLoadError("PDF document not initialized".to_string())
})?;
let (xobject_id, width, height) =
SvgToPdfConverter::convert_svg_bytes_to_xobject(document, data)?;
Ok(ResourceInfo {
id: file.id.clone(),
resource_type: ResourceType::Svg,
object_id: Some(xobject_id),
width: Some(width),
height: Some(height),
})
}
fn process_image_file(
&mut self,
file: &DucExternalFile,
rev_data: Option<&[u8]>,
resource_type: &ResourceType,
) -> ConversionResult<ResourceInfo> {
let _revision = file
.revisions
.get(&file.active_revision_id)
.ok_or_else(|| {
ConversionError::ResourceLoadError(format!(
"No active revision for file {}",
file.id
))
})?;
let image_data = rev_data.ok_or_else(|| {
ConversionError::ResourceLoadError(format!(
"No data blob for revision {}",
file.active_revision_id
))
})?;
let xobject_id = {
let mut document = self.document.take().ok_or_else(|| {
ConversionError::ResourceLoadError("PDF document not initialized".to_string())
})?;
let result = self.create_image_xobject(&mut document, image_data, resource_type)?;
self.document = Some(document);
result
};
let (width, height) = (100.0, 100.0);
Ok(ResourceInfo {
id: file.id.clone(),
resource_type: resource_type.clone(),
object_id: Some(xobject_id),
width: Some(width),
height: Some(height),
})
}
fn process_pdf_file(
&mut self,
file: &DucExternalFile,
rev_data: Option<&[u8]>,
) -> ConversionResult<ResourceInfo> {
let _revision = file
.revisions
.get(&file.active_revision_id)
.ok_or_else(|| {
ConversionError::ResourceLoadError(format!(
"No active revision for file {}",
file.id
))
})?;
let pdf_data = rev_data.ok_or_else(|| {
ConversionError::ResourceLoadError(format!(
"No data blob for revision {}",
file.active_revision_id
))
})?;
let pdf_embedder = self.pdf_embedder.as_mut().ok_or_else(|| {
ConversionError::ResourceLoadError("PDF embedder not initialized".to_string())
})?;
let _document = self.document.as_mut().ok_or_else(|| {
ConversionError::ResourceLoadError("PDF document not initialized".to_string())
})?;
let embed_id = format!("pdf_{}", file.id);
pdf_embedder
.load_pdf_from_bytes(pdf_data, &embed_id)
.map_err(|e| {
ConversionError::ResourceLoadError(format!("Failed to embed PDF: {}", e))
})?;
let (width, height) = (100.0, 100.0);
Ok(ResourceInfo {
id: file.id.clone(),
resource_type: ResourceType::Pdf,
object_id: None, width: Some(width),
height: Some(height),
})
}
pub fn get_resource_info(&self, resource_id: &str) -> Option<&ResourceInfo> {
self.resource_cache.get(resource_id)
}
pub fn get_resource_object_id(&self, resource_id: &str) -> Option<u32> {
self.resource_cache
.get(resource_id)
.and_then(|info| info.object_id)
}
fn create_image_xobject(
&mut self,
document: &mut Document,
image_data: &[u8],
resource_type: &ResourceType,
) -> ConversionResult<u32> {
let mut image_dict = Dictionary::new();
image_dict.set("Type", Object::Name("XObject".as_bytes().to_vec()));
image_dict.set("Subtype", Object::Name("Image".as_bytes().to_vec()));
match resource_type {
ResourceType::Png => {
image_dict.set("Filter", Object::Name("FlateDecode".as_bytes().to_vec()));
image_dict.set("ColorSpace", Object::Name("DeviceRGB".as_bytes().to_vec()));
image_dict.set("BitsPerComponent", Object::Integer(8));
}
ResourceType::Jpeg => {
image_dict.set("Filter", Object::Name("DCTDecode".as_bytes().to_vec()));
image_dict.set("ColorSpace", Object::Name("DeviceRGB".as_bytes().to_vec()));
image_dict.set("BitsPerComponent", Object::Integer(8));
}
_ => {
return Err(ConversionError::ResourceLoadError(
"Unsupported image type".to_string(),
))
}
}
image_dict.set("Width", Object::Integer(100)); image_dict.set("Height", Object::Integer(100));
let stream = Stream::new(image_dict, image_data.to_vec());
let (object_id, _) = document.add_object(Object::Stream(stream));
Ok(object_id)
}
pub fn stream_svg_resource(
&self,
resource_id: &str,
x: f64,
y: f64,
width: f64,
height: f64,
) -> ConversionResult<Vec<hipdf::lopdf::content::Operation>> {
use hipdf::lopdf::content::Operation;
let resource_info = match self.resource_cache.get(resource_id) {
Some(info) => info,
None => {
let mut operations = Vec::new();
operations.push(Operation::new(
&format!("% SVG resource not found: {}", resource_id),
vec![],
));
return Ok(operations);
}
};
if resource_info.resource_type != ResourceType::Svg {
let mut operations = Vec::new();
operations.push(Operation::new(
&format!("% Resource {} is not an SVG", resource_id),
vec![],
));
return Ok(operations);
}
let object_id = match resource_info.object_id {
Some(id) => id,
None => {
let mut operations = Vec::new();
operations.push(Operation::new(
&format!("% SVG resource {} has no object ID", resource_id),
vec![],
));
return Ok(operations);
}
};
let mut operations = Vec::new();
operations.push(Operation::new("q", vec![]));
operations.push(Operation::new(
"cm",
vec![
Object::Real(width as f32),
Object::Real(0.0),
Object::Real(0.0),
Object::Real(height as f32),
Object::Real(x as f32),
Object::Real(y as f32),
],
));
operations.push(Operation::new(
"Do",
vec![Object::Name(format!("XObject{}", object_id).into_bytes())],
));
operations.push(Operation::new("Q", vec![]));
Ok(operations)
}
pub fn stream_image_resource(
&self,
resource_id: &str,
x: f64,
y: f64,
width: f64,
height: f64,
) -> ConversionResult<Vec<hipdf::lopdf::content::Operation>> {
use hipdf::lopdf::content::Operation;
let resource_info = match self.resource_cache.get(resource_id) {
Some(info) => info,
None => {
let mut operations = Vec::new();
operations.push(Operation::new(
&format!("% Image resource not found: {}", resource_id),
vec![],
));
return Ok(operations);
}
};
if !matches!(
resource_info.resource_type,
ResourceType::Png | ResourceType::Jpeg
) {
let mut operations = Vec::new();
operations.push(Operation::new(
&format!("% Resource {} is not an image", resource_id),
vec![],
));
return Ok(operations);
}
let _object_id = match resource_info.object_id {
Some(id) => id,
None => {
let mut operations = Vec::new();
operations.push(Operation::new(
&format!("% Image resource {} has no object ID", resource_id),
vec![],
));
return Ok(operations);
}
};
let mut operations = Vec::new();
operations.push(Operation::new("q", vec![]));
operations.push(Operation::new(
"cm",
vec![
Object::Real(width as f32),
Object::Real(0.0),
Object::Real(0.0),
Object::Real(height as f32),
Object::Real(x as f32),
Object::Real(y as f32),
],
));
operations.push(Operation::new(
"Do",
vec![Object::Name(format!("Img{}", resource_id).into_bytes())],
));
operations.push(Operation::new("Q", vec![]));
Ok(operations)
}
pub fn stream_pdf_resource(
&self,
resource_id: &str,
x: f64,
y: f64,
width: f64,
height: f64,
) -> ConversionResult<Vec<hipdf::lopdf::content::Operation>> {
let resource_info = match self.resource_cache.get(resource_id) {
Some(info) => info,
None => {
let mut operations = Vec::new();
operations.push(Operation::new(
&format!("% PDF resource not found: {}", resource_id),
vec![],
));
return Ok(operations);
}
};
if resource_info.resource_type != ResourceType::Pdf {
let mut operations = Vec::new();
operations.push(Operation::new(
&format!("% Resource {} is not a PDF", resource_id),
vec![],
));
return Ok(operations);
}
let xobject_id = match resource_info.object_id {
Some(id) => id,
None => {
let mut operations = Vec::new();
operations.push(Operation::new(
&format!("% PDF resource {} has no object ID", resource_id),
vec![],
));
return Ok(operations);
}
};
let mut operations = Vec::new();
operations.push(Operation::new("q", vec![]));
operations.push(Operation::new(
"cm",
vec![
Object::Real(width as f32),
Object::Real(0.0),
Object::Real(0.0),
Object::Real(height as f32),
Object::Real(x as f32),
Object::Real(y as f32),
],
));
operations.push(Operation::new(
"Do",
vec![Object::Name(format!("XObject{}", xobject_id).into_bytes())],
));
operations.push(Operation::new("Q", vec![]));
Ok(operations)
}
pub fn clear_cache(&mut self) {
self.resource_cache.clear();
}
pub fn cache_size(&self) -> usize {
self.resource_cache.len()
}
}
impl Default for ResourceStreamer {
fn default() -> Self {
Self::new()
}
}