use std::ops::Range;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use easypdf_core::Result;
use easypdf_core::{AtomicFileOutput, PdfInput, ResourceLimits};
use crate::{
ImagePolicy, MarkdownExportResult, MarkdownProfile, OcrPolicy, PdfMarkdownBuilder,
PdfMarkdownProcessor, TablePolicy,
};
#[derive(Clone)]
#[must_use]
pub struct PdfMarkdownExportBuilder {
input: PdfInput,
output: PathBuf,
pages: Option<Range<usize>>,
profile: MarkdownProfile,
table_policy: TablePolicy,
image_policy: ImagePolicy,
ocr_policy: OcrPolicy,
limits: ResourceLimits,
processors: Vec<Arc<dyn PdfMarkdownProcessor>>,
}
impl std::fmt::Debug for PdfMarkdownExportBuilder {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("PdfMarkdownExportBuilder")
.field("input", &self.input)
.field("output", &self.output)
.field("pages", &self.pages)
.field("profile", &self.profile)
.field("table_policy", &self.table_policy)
.field("image_policy", &self.image_policy)
.field("ocr_policy", &self.ocr_policy)
.field("limits", &self.limits)
.field("processor_count", &self.processors.len())
.finish()
}
}
impl PdfMarkdownExportBuilder {
pub fn new(input: impl Into<PathBuf>, output: impl Into<PathBuf>) -> Self {
Self {
input: PdfInput::from_path(input.into()),
output: output.into(),
pages: None,
profile: MarkdownProfile::default(),
table_policy: TablePolicy::default(),
image_policy: ImagePolicy::default(),
ocr_policy: OcrPolicy::default(),
limits: ResourceLimits::default(),
processors: Vec::new(),
}
}
pub fn from_bytes(bytes: impl Into<Vec<u8>>, output: impl Into<PathBuf>) -> Self {
Self {
input: PdfInput::from_bytes(bytes),
output: output.into(),
pages: None,
profile: MarkdownProfile::default(),
table_policy: TablePolicy::default(),
image_policy: ImagePolicy::default(),
ocr_policy: OcrPolicy::default(),
limits: ResourceLimits::default(),
processors: Vec::new(),
}
}
pub fn pages(mut self, pages: Range<usize>) -> Self {
self.pages = Some(pages);
self
}
pub const fn profile(mut self, profile: MarkdownProfile) -> Self {
self.profile = profile;
self
}
pub const fn tables(mut self, policy: TablePolicy) -> Self {
self.table_policy = policy;
self
}
pub fn images(mut self, policy: ImagePolicy) -> Self {
self.image_policy = policy;
self
}
pub const fn ocr(mut self, policy: OcrPolicy) -> Self {
self.ocr_policy = policy;
self
}
pub const fn resource_limits(mut self, limits: ResourceLimits) -> Self {
self.limits = limits;
self
}
pub fn processor(mut self, processor: Arc<dyn PdfMarkdownProcessor>) -> Self {
self.processors.push(processor);
self
}
pub fn do_export(self) -> Result<MarkdownExportResult> {
let mut builder = PdfMarkdownBuilder::from_input(self.input)
.profile(self.profile)
.tables(self.table_policy)
.images(self.image_policy)
.ocr(self.ocr_policy)
.resource_limits(self.limits);
if let Some(pages) = self.pages {
builder = builder.pages(pages);
}
for processor in self.processors {
builder = builder.processor(processor);
}
let conversion = builder.do_convert()?;
AtomicFileOutput::new(&self.output).write(conversion.markdown().as_bytes())?;
Ok(MarkdownExportResult::new(
self.output,
conversion.report().clone(),
))
}
#[must_use]
pub fn output(&self) -> &Path {
&self.output
}
}
#[cfg(test)]
#[allow(clippy::uninlined_format_args, clippy::float_cmp)]
mod tests {
use super::*;
#[test]
fn new_creates_builder() {
let builder = PdfMarkdownExportBuilder::new("/tmp/input.pdf", "/tmp/output.md");
assert_eq!(builder.output(), Path::new("/tmp/output.md"));
}
#[test]
fn from_bytes_creates_builder() {
let builder = PdfMarkdownExportBuilder::from_bytes(vec![1, 2, 3], "/tmp/out.md");
assert_eq!(builder.output(), Path::new("/tmp/out.md"));
}
#[test]
fn pages_sets_range() {
let builder = PdfMarkdownExportBuilder::new("/tmp/in.pdf", "/tmp/out.md").pages(2..5);
assert!(builder.pages.is_some());
assert_eq!(builder.pages.unwrap(), 2..5);
}
#[test]
fn profile_sets_profile() {
let builder = PdfMarkdownExportBuilder::new("/tmp/in.pdf", "/tmp/out.md")
.profile(MarkdownProfile::Llm);
assert_eq!(builder.profile, MarkdownProfile::Llm);
}
#[test]
fn tables_sets_policy() {
let builder = PdfMarkdownExportBuilder::new("/tmp/in.pdf", "/tmp/out.md")
.tables(TablePolicy::PlainText);
assert_eq!(builder.table_policy, TablePolicy::PlainText);
}
#[test]
fn images_sets_policy() {
let builder = PdfMarkdownExportBuilder::new("/tmp/in.pdf", "/tmp/out.md")
.images(ImagePolicy::Reference);
assert_eq!(builder.image_policy, ImagePolicy::Reference);
}
#[test]
fn ocr_sets_policy() {
let builder =
PdfMarkdownExportBuilder::new("/tmp/in.pdf", "/tmp/out.md").ocr(OcrPolicy::Auto);
assert_eq!(builder.ocr_policy, OcrPolicy::Auto);
}
#[test]
fn resource_limits_sets_limits() {
let limits = ResourceLimits::strict();
let builder =
PdfMarkdownExportBuilder::new("/tmp/in.pdf", "/tmp/out.md").resource_limits(limits);
assert_eq!(builder.limits.max_input_bytes(), limits.max_input_bytes());
}
#[test]
fn debug_format() {
let builder = PdfMarkdownExportBuilder::new("/tmp/in.pdf", "/tmp/out.md");
let dbg = format!("{:?}", builder);
assert!(dbg.contains("PdfMarkdownExportBuilder"));
assert!(dbg.contains("input"));
assert!(dbg.contains("output"));
}
#[test]
fn clone_preserves_values() {
let builder = PdfMarkdownExportBuilder::new("/tmp/in.pdf", "/tmp/out.md")
.profile(MarkdownProfile::Llm);
let cloned = builder.clone();
assert_eq!(builder.profile, cloned.profile);
assert_eq!(builder.output(), cloned.output());
}
}