easypdf_markdown/
pdf_markdown_builder.rs1use std::ops::Range;
4use std::sync::Arc;
5
6use easypdf_core::Result;
7use easypdf_core::{PdfInput, ResourceLimits};
8use easypdf_reader::PdfReader;
9
10use crate::{
11 ImagePolicy, MarkdownConversionResult, MarkdownExportReport, MarkdownProcessorCapabilities,
12 MarkdownProfile, MarkdownRenderer, MarkdownWarning, OcrPolicy, PdfMarkdownProcessor,
13 TablePolicy,
14};
15
16#[derive(Clone)]
18#[must_use]
19pub struct PdfMarkdownBuilder {
20 input: PdfInput,
21 pages: Option<Range<usize>>,
22 profile: MarkdownProfile,
23 table_policy: TablePolicy,
24 image_policy: ImagePolicy,
25 ocr_policy: OcrPolicy,
26 limits: ResourceLimits,
27 processors: Vec<Arc<dyn PdfMarkdownProcessor>>,
28}
29
30impl std::fmt::Debug for PdfMarkdownBuilder {
31 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 formatter
33 .debug_struct("PdfMarkdownBuilder")
34 .field("input", &self.input)
35 .field("pages", &self.pages)
36 .field("profile", &self.profile)
37 .field("table_policy", &self.table_policy)
38 .field("image_policy", &self.image_policy)
39 .field("ocr_policy", &self.ocr_policy)
40 .field("limits", &self.limits)
41 .field("processor_count", &self.processors.len())
42 .finish()
43 }
44}
45
46impl PdfMarkdownBuilder {
47 pub fn new(input: impl Into<std::path::PathBuf>) -> Self {
49 Self::from_input(PdfInput::from_path(input.into()))
50 }
51
52 pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Self {
54 Self::from_input(PdfInput::from_bytes(bytes))
55 }
56
57 #[must_use = "conversion builder must be configured or executed"]
59 pub fn from_input(input: PdfInput) -> Self {
60 Self {
61 input,
62 pages: None,
63 profile: MarkdownProfile::default(),
64 table_policy: TablePolicy::default(),
65 image_policy: ImagePolicy::default(),
66 ocr_policy: OcrPolicy::default(),
67 limits: ResourceLimits::default(),
68 processors: Vec::new(),
69 }
70 }
71
72 pub fn pages(mut self, pages: Range<usize>) -> Self {
74 self.pages = Some(pages);
75 self
76 }
77
78 pub const fn profile(mut self, profile: MarkdownProfile) -> Self {
80 self.profile = profile;
81 self
82 }
83
84 pub const fn tables(mut self, policy: TablePolicy) -> Self {
86 self.table_policy = policy;
87 self
88 }
89
90 pub fn images(mut self, policy: ImagePolicy) -> Self {
92 self.image_policy = policy;
93 self
94 }
95
96 pub const fn ocr(mut self, policy: OcrPolicy) -> Self {
98 self.ocr_policy = policy;
99 self
100 }
101
102 pub const fn resource_limits(mut self, limits: ResourceLimits) -> Self {
104 self.limits = limits;
105 self
106 }
107
108 pub fn processor(mut self, processor: Arc<dyn PdfMarkdownProcessor>) -> Self {
110 self.processors.push(processor);
111 self
112 }
113
114 pub fn do_convert(self) -> Result<MarkdownConversionResult> {
120 let mut reader = PdfReader::open_with_limits(&self.input, self.limits)?;
121 if let Some(pages) = self.pages {
122 reader = reader.try_pages(pages)?;
123 }
124 let mut document = reader.extract_document_model()?;
125 let mut warnings = Vec::new();
126 let mut capabilities = MarkdownProcessorCapabilities::new();
127 for processor in &self.processors {
128 capabilities = capabilities.union(processor.capabilities());
129 let (processed, mut processor_warnings) = processor.process(&self.input, document)?;
130 document = processed;
131 warnings.append(&mut processor_warnings);
132 }
133
134 append_capability_warnings(
135 &document,
136 self.table_policy,
137 &self.image_policy,
138 self.ocr_policy,
139 capabilities,
140 &mut warnings,
141 );
142 let renderer = MarkdownRenderer::new(self.profile)
143 .with_table_policy(self.table_policy)
144 .with_image_policy(self.image_policy);
145 let markdown = renderer.render(&document);
146 let blocks_written = document
147 .pages()
148 .iter()
149 .map(|page| page.blocks().len())
150 .sum();
151 let report = MarkdownExportReport::new(
152 document.page_count(),
153 blocks_written,
154 markdown.len(),
155 warnings,
156 );
157 Ok(MarkdownConversionResult::new(markdown, report))
158 }
159}
160
161fn append_capability_warnings(
162 document: &easypdf_core::PdfDocumentModel,
163 table_policy: TablePolicy,
164 image_policy: &ImagePolicy,
165 ocr_policy: OcrPolicy,
166 capabilities: MarkdownProcessorCapabilities,
167 warnings: &mut Vec<MarkdownWarning>,
168) {
169 if table_policy == TablePolicy::Detect && !capabilities.table_detection() {
170 warnings.push(MarkdownWarning::TableDetectionUnavailable);
171 }
172 if matches!(image_policy, ImagePolicy::ExtractTo(_)) && !capabilities.image_extraction() {
173 warnings.push(MarkdownWarning::ImageExtractionUnavailable);
174 }
175 for page in document
176 .pages()
177 .iter()
178 .filter(|page| page.blocks().is_empty())
179 {
180 warnings.push(MarkdownWarning::EmptyPage {
181 page_index: page.index(),
182 });
183 if ocr_policy == OcrPolicy::Auto && !capabilities.ocr() {
184 warnings.push(MarkdownWarning::OcrUnavailable {
185 page_index: page.index(),
186 });
187 }
188 }
189}
190
191#[cfg(test)]
192#[allow(clippy::uninlined_format_args, clippy::float_cmp)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn new_creates_builder() {
198 let builder = PdfMarkdownBuilder::new("/tmp/test.pdf");
199 assert!(builder.pages.is_none());
200 assert_eq!(builder.profile, MarkdownProfile::default());
201 }
202
203 #[test]
204 fn from_bytes_creates_builder() {
205 let builder = PdfMarkdownBuilder::from_bytes(vec![1, 2, 3]);
206 assert!(builder.pages.is_none());
207 }
208
209 #[test]
210 fn from_input_creates_builder() {
211 let input = PdfInput::from_bytes(vec![1, 2]);
212 let builder = PdfMarkdownBuilder::from_input(input);
213 assert!(builder.pages.is_none());
214 }
215
216 #[test]
217 fn pages_sets_range() {
218 let builder = PdfMarkdownBuilder::new("/tmp/test.pdf").pages(1..5);
219 assert_eq!(builder.pages, Some(1..5));
220 }
221
222 #[test]
223 fn profile_sets_profile() {
224 let builder = PdfMarkdownBuilder::new("/tmp/test.pdf").profile(MarkdownProfile::Llm);
225 assert_eq!(builder.profile, MarkdownProfile::Llm);
226 }
227
228 #[test]
229 fn tables_sets_policy() {
230 let builder = PdfMarkdownBuilder::new("/tmp/test.pdf").tables(TablePolicy::PlainText);
231 assert_eq!(builder.table_policy, TablePolicy::PlainText);
232 }
233
234 #[test]
235 fn images_sets_policy() {
236 let builder = PdfMarkdownBuilder::new("/tmp/test.pdf").images(ImagePolicy::Reference);
237 assert_eq!(builder.image_policy, ImagePolicy::Reference);
238 }
239
240 #[test]
241 fn ocr_sets_policy() {
242 let builder = PdfMarkdownBuilder::new("/tmp/test.pdf").ocr(OcrPolicy::Auto);
243 assert_eq!(builder.ocr_policy, OcrPolicy::Auto);
244 }
245
246 #[test]
247 fn resource_limits_sets_limits() {
248 let limits = ResourceLimits::strict();
249 let builder = PdfMarkdownBuilder::new("/tmp/test.pdf").resource_limits(limits);
250 assert_eq!(builder.limits.max_input_bytes(), limits.max_input_bytes());
251 }
252
253 #[test]
254 fn debug_format() {
255 let builder = PdfMarkdownBuilder::new("/tmp/test.pdf");
256 let dbg = format!("{:?}", builder);
257 assert!(dbg.contains("PdfMarkdownBuilder"));
258 }
259
260 #[test]
261 fn clone_preserves_values() {
262 let builder = PdfMarkdownBuilder::new("/tmp/test.pdf")
263 .profile(MarkdownProfile::Llm)
264 .tables(TablePolicy::PlainText);
265 let cloned = builder.clone();
266 assert_eq!(builder.profile, cloned.profile);
267 assert_eq!(builder.table_policy, cloned.table_policy);
268 }
269
270 #[test]
271 fn chaining_sets_all_fields() {
272 let builder = PdfMarkdownBuilder::new("/tmp/test.pdf")
273 .pages(0..10)
274 .profile(MarkdownProfile::Gfm)
275 .tables(TablePolicy::Detect)
276 .images(ImagePolicy::Ignore)
277 .ocr(OcrPolicy::Disabled)
278 .resource_limits(ResourceLimits::permissive());
279 assert_eq!(builder.pages, Some(0..10));
280 assert_eq!(builder.profile, MarkdownProfile::Gfm);
281 assert_eq!(builder.table_policy, TablePolicy::Detect);
282 assert_eq!(builder.image_policy, ImagePolicy::Ignore);
283 assert_eq!(builder.ocr_policy, OcrPolicy::Disabled);
284 }
285}