use std::collections::HashMap;
use zpdf_color::IccCache;
use zpdf_content::interpreter::ContentInterpreter;
use zpdf_content::output_intent_cmyk_profile;
use zpdf_core::{Matrix, Result};
use zpdf_display_list::RenderCommand;
use zpdf_document::{DocInfo, PdfDocument, StructTree, XmpMetadata};
use zpdf_image::{DecodedImage, ImageCache};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ConversionMode {
#[default]
TextOnly,
Rich,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ConversionOptions {
pub mode: ConversionMode,
pub use_structure: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ImagePlacement {
pub transform: Matrix,
pub alpha: f32,
}
#[derive(Debug, Clone)]
pub struct ConvertedImage {
pub image: DecodedImage,
pub placements: Vec<ImagePlacement>,
}
#[derive(Debug, Clone)]
pub struct ConvertedPage {
pub index: usize,
pub label: Option<String>,
pub width_points: f64,
pub height_points: f64,
pub rotation: i32,
pub text: String,
pub images: Vec<ConvertedImage>,
}
#[derive(Debug, Clone)]
pub struct ConvertedDocument {
pub pdf_version: (u8, u8),
pub total_pages: usize,
pub info: Option<DocInfo>,
pub xmp: Option<XmpMetadata>,
pub pages: Vec<ConvertedPage>,
pub structure_order_used: bool,
}
pub fn convert_pdf(
doc: &PdfDocument,
page_indices: &[usize],
options: ConversionOptions,
) -> Result<ConvertedDocument> {
let rich = options.mode == ConversionMode::Rich;
let struct_tree: Option<StructTree> = if options.use_structure {
doc.struct_tree()
} else {
None
};
let page_labels = rich.then(|| doc.page_labels()).flatten();
let oc_config = doc.oc_config();
let doc_intents = if rich {
doc.output_intents()
} else {
Vec::new()
};
let mut icc_cache = IccCache::new();
let mut pages = Vec::with_capacity(page_indices.len());
let limits = doc.file().limits();
let mut retained_image_bytes = 0u64;
let mut retained_text_bytes = 0u64;
for &page_index in page_indices {
let page = doc.page(page_index)?;
let page_box = page.effective_box();
let mut fonts = doc.load_page_fonts(&page);
let content = doc.page_content_bytes(&page)?;
let mut spans = Vec::new();
let mut images = ImageCache::new();
let output_intent = if rich {
output_intent_cmyk_profile(
doc.file(),
doc.page_output_intents(&page),
&doc_intents,
&mut icc_cache,
)
} else {
None
};
let mut interpreter = ContentInterpreter::new(page_box)
.with_fonts(&mut fonts)
.with_document(doc.file(), &page.resources)
.with_text_sink(&mut spans)
.with_operand_stack_limit(limits.max_operand_stack_depth as usize);
if rich {
let remaining_image_bytes = limits
.max_image_cache_bytes
.saturating_sub(retained_image_bytes);
if remaining_image_bytes > 0 {
interpreter = interpreter
.with_colors(&mut icc_cache)
.with_images(&mut images)
.with_image_cache_limit(remaining_image_bytes);
}
}
if let Some(oc) = &oc_config {
interpreter = interpreter.with_optional_content(oc);
}
if let Some(profile) = output_intent {
interpreter = interpreter.with_output_intent_cmyk(profile);
}
let display_list = interpreter.interpret(&content);
let text = match &struct_tree {
Some(tree) => zpdf_content::text::struct_ordered_text(&spans, page_index, tree),
None => zpdf_content::text::spans_to_text(spans, 2.0),
};
retained_text_bytes = retained_text_bytes
.checked_add(u64::try_from(text.len()).unwrap_or(u64::MAX))
.ok_or(zpdf_core::Error::StreamSizeLimit(
limits.max_decoded_stream_bytes,
))?;
if retained_text_bytes > limits.max_decoded_stream_bytes {
return Err(zpdf_core::Error::StreamSizeLimit(
limits.max_decoded_stream_bytes,
));
}
let converted_images = if rich {
take_display_list_images(&display_list.commands, &mut images)
} else {
Vec::new()
};
retained_image_bytes = retained_image_bytes.saturating_add(
converted_images
.iter()
.map(|image| u64::try_from(image.image.data.capacity()).unwrap_or(u64::MAX))
.fold(0u64, u64::saturating_add),
);
pages.push(ConvertedPage {
index: page_index,
label: page_labels
.as_ref()
.and_then(|labels| labels.label(page_index)),
width_points: page_box.width(),
height_points: page_box.height(),
rotation: page.rotate,
text,
images: converted_images,
});
}
Ok(ConvertedDocument {
pdf_version: doc.version(),
total_pages: doc.page_count(),
info: rich.then(|| doc.info()).flatten(),
xmp: rich.then(|| doc.xmp_metadata()).flatten(),
pages,
structure_order_used: struct_tree
.as_ref()
.is_some_and(|tree| !tree.children.is_empty()),
})
}
fn take_display_list_images(
commands: &[RenderCommand],
cache: &mut ImageCache,
) -> Vec<ConvertedImage> {
let mut images = Vec::<ConvertedImage>::new();
let mut positions = HashMap::<u32, usize>::new();
for command in commands {
let RenderCommand::DrawImage(draw) = command else {
continue;
};
let converted_index = match positions.get(&draw.image_id).copied() {
Some(index) => index,
None => {
let Some(image) = cache.remove(draw.image_id) else {
continue;
};
let index = images.len();
images.push(ConvertedImage {
image,
placements: Vec::new(),
});
positions.insert(draw.image_id, index);
index
}
};
images[converted_index].placements.push(ImagePlacement {
transform: draw.transform,
alpha: draw.alpha,
});
}
images
}