Skip to main content

easypdf_core/
traits.rs

1//! 定义 `easypdf-rust` 核心扩展点的 trait。
2//!
3//! 包含 `PdfModel`、`PdfReadListener`、`PdfWriteHandler`、`PdfConverter`、
4//! `PdfEngine`、`EngineCapabilities` 和 `CapabilityLevel`。
5
6use crate::error::Result;
7
8/// 可渲染为 PDF 内容的类型 trait。
9///
10/// 通常通过 `#[derive(PdfModel)]` 派生而非手动实现。
11/// 将 Rust 结构体映射为 PDF 元素。
12pub trait PdfModel {
13    /// 将此模型渲染为 PDF 内容。
14    ///
15    /// 实现由派生宏生成,处理布局、定位和样式应用。
16    ///
17    /// # Errors
18    ///
19    /// 模型转换或内容渲染失败时返回错误。
20    fn render(&self) -> Result<Vec<RenderedElement>>;
21
22    /// 返回此模型的元数据(页面尺寸、方向、边距等)。
23    fn metadata(&self) -> PdfModelMetadata;
24
25    /// 返回表单填写和数据映射场景的字段描述符。
26    ///
27    /// 每个标注了 `#[pdf(field = "...")]` 或类似属性的字段
28    /// 会生成一个描述符。默认实现返回空向量。
29    fn field_descriptors(&self) -> Vec<PdfFieldDescriptor> {
30        Vec::new()
31    }
32}
33
34/// [`PdfModel`] 中单个字段的描述符,由派生宏生成。
35///
36/// 携带 `#[pdf(field = "...", order = N, ...)]` 属性的元数据,
37/// 用于模板填充和验证逻辑。
38#[derive(Debug, Clone)]
39pub struct PdfFieldDescriptor {
40    /// 映射到的 PDF 表单字段名称。
41    pub field_name: String,
42    /// Rust 字段名称。
43    pub rust_field_name: String,
44    /// 显示顺序(值越小越靠前)。
45    pub order: u32,
46    /// 可选的格式字符串(如日期的 "YYYY-MM-DD")。
47    pub format: Option<String>,
48    /// 默认值表达式(字符串形式),如果指定。
49    pub default_value: Option<String>,
50    /// 此字段是否必填。
51    pub required: bool,
52    /// 是否为嵌套子模型。
53    pub nested: bool,
54}
55
56impl PdfFieldDescriptor {
57    /// 使用最少的必需字段创建新的字段描述符。
58    #[must_use]
59    pub fn new(field_name: impl Into<String>, rust_field_name: impl Into<String>) -> Self {
60        Self {
61            field_name: field_name.into(),
62            rust_field_name: rust_field_name.into(),
63            order: u32::MAX,
64            format: None,
65            default_value: None,
66            required: false,
67            nested: false,
68        }
69    }
70}
71
72/// 与 `PdfModel` 关联的元数据(通常来自 `#[pdf(...)]` 属性)。
73#[derive(Debug, Clone)]
74pub struct PdfModelMetadata {
75    /// 模型的页面尺寸。
76    pub page_size: crate::enums::PageSize,
77    /// 页面方向。
78    pub orientation: crate::enums::Orientation,
79    /// 页面边距(点)。
80    pub margins: f64,
81}
82
83impl Default for PdfModelMetadata {
84    fn default() -> Self {
85        Self {
86            page_size: crate::enums::PageSize::A4,
87            orientation: crate::enums::Orientation::default(),
88            margins: 72.0,
89        }
90    }
91}
92
93/// 由 [`PdfModel`] 生成的渲染元素。
94///
95/// 这是 `PdfModel::render()` 的输出,表示 PDF 页面上
96/// 一个已定位的内容片段。
97#[derive(Debug, Clone)]
98pub enum RenderedElement {
99    /// 位于给定 (x, y) 位置的文本元素。
100    Text {
101        /// 从左下角算起的 x 坐标(PDF 点)。
102        x: f64,
103        /// 从左下角算起的 y 坐标(PDF 点)。
104        y: f64,
105        /// 带格式的文本内容。
106        text: crate::content::PdfText,
107    },
108    /// 位于给定 (x, y) 位置的表格。
109    Table {
110        /// 表格左上角的 x 坐标。
111        x: f64,
112        /// 表格左上角的 y 坐标。
113        y: f64,
114        /// 表格数据和配置。
115        table: crate::content::PdfTable,
116    },
117    /// 位于给定 (x, y) 位置的图片。
118    Image {
119        /// 从左下角算起的 x 坐标。
120        x: f64,
121        /// 从左下角算起的 y 坐标。
122        y: f64,
123        /// 图片数据。
124        image: crate::content::PdfImage,
125    },
126}
127
128// --- PdfReadListener ---
129
130/// PDF 读取操作的事件驱动监听器。
131///
132/// 类似于 easyexcel-rs 中的 `ReadListener<T>`。在读取过程中
133/// 遇到每个页面或文本块时被调用。
134///
135/// 此 trait 要求 `Send`,以便监听器可在异步或并行读取管道中
136/// 跨线程边界使用。持有非 `Send` 状态的实现者应将其包装在
137/// `Mutex` 或类似结构中。
138pub trait PdfReadListener: Send {
139    /// 页面开始处理时调用。
140    ///
141    /// # Errors
142    ///
143    /// 实现可通过返回错误来停止处理。
144    fn on_page_start(&mut self, page_number: usize) -> Result<()> {
145        let _ = page_number;
146        Ok(())
147    }
148
149    /// 从页面提取的每个文本块被调用。
150    ///
151    /// # Errors
152    ///
153    /// 实现可通过返回错误来停止处理。
154    fn on_text(&mut self, page_number: usize, text: &str) -> Result<()>;
155
156    /// 页面处理完成时调用。
157    ///
158    /// # Errors
159    ///
160    /// 实现可通过返回错误来停止处理。
161    fn on_page_end(&mut self, page_number: usize) -> Result<()> {
162        let _ = page_number;
163        Ok(())
164    }
165
166    /// 所有页面处理完成后调用。
167    ///
168    /// # Errors
169    ///
170    /// 实现可报告终结化失败。
171    fn on_document_end(&mut self) -> Result<()> {
172        Ok(())
173    }
174}
175
176// --- PdfWriteHandler ---
177
178/// PDF 写入操作的生命周期钩子。
179///
180/// 类似于 easyexcel-rs 中的 `WriteHandler`。处理程序在文档创建的
181/// 每个阶段按优先级顺序被调用。
182pub trait PdfWriteHandler: Send {
183    /// 文档创建前调用。
184    ///
185    /// # Errors
186    ///
187    /// 实现可中止文档创建。
188    fn before_document(&mut self) -> Result<()> {
189        Ok(())
190    }
191
192    /// 新页面开始前调用。
193    ///
194    /// # Errors
195    ///
196    /// 实现可中止页面创建。
197    fn before_page(&mut self, page_number: usize) -> Result<()> {
198        let _ = page_number;
199        Ok(())
200    }
201
202    /// 页面完成后调用。
203    ///
204    /// # Errors
205    ///
206    /// 实现可报告页面终结化失败。
207    fn after_page(&mut self, page_number: usize) -> Result<()> {
208        let _ = page_number;
209        Ok(())
210    }
211
212    /// 文档终结化后调用。
213    ///
214    /// # Errors
215    ///
216    /// 实现可报告文档终结化失败。
217    fn after_document(&mut self) -> Result<()> {
218        Ok(())
219    }
220}
221
222// --- PdfConverter ---
223
224/// Rust 类型 `T` 与 PDF 字符串表示之间的双向转换器。
225///
226/// 类似于 easyexcel-rs 中的 `Converter<T>`。
227pub trait PdfConverter<T>: Send {
228    /// 将 Rust 值转换为其 PDF 字符串表示。
229    ///
230    /// # Errors
231    ///
232    /// 值无法表示时返回错误。
233    fn to_pdf_string(&self, value: &T) -> Result<String>;
234
235    /// 将 PDF 字符串表示转换回 Rust 值。
236    ///
237    /// # Errors
238    ///
239    /// 字符串无法解析时返回错误。
240    #[allow(clippy::wrong_self_convention)]
241    fn from_pdf_string(&self, s: &str) -> Result<T>;
242}
243
244/// 全面实现,使 `Box<dyn PdfConverter<T>>` 可在任何期望
245/// `PdfConverter<T>` 的地方使用(如 `ConverterRegistry::register`)。
246impl<T> PdfConverter<T> for Box<dyn PdfConverter<T>> {
247    fn to_pdf_string(&self, value: &T) -> Result<String> {
248        (**self).to_pdf_string(value)
249    }
250
251    fn from_pdf_string(&self, s: &str) -> Result<T> {
252        (**self).from_pdf_string(s)
253    }
254}
255
256// --- PdfEngine (C1) ---
257
258/// 用于后端切换的抽象 PDF 引擎接口。
259///
260/// 允许不同的 PDF 后端(lopdf、printpdf、justpdf)互换使用。
261/// 目前处于实验阶段——完整的抽象等待第二个成熟引擎实现。
262pub trait PdfEngine: Send + Sync {
263    /// 人类可读的引擎名称。
264    fn name(&self) -> &str;
265
266    /// 此引擎支持的功能。
267    fn capabilities(&self) -> EngineCapabilities;
268}
269
270/// 描述 PDF 引擎支持哪些操作。
271#[derive(Debug, Clone, Copy, Default)]
272#[allow(clippy::struct_excessive_bools)]
273pub struct EngineCapabilities {
274    /// 可以从零创建新的 PDF 文档。
275    pub create: bool,
276    /// 可以读取和解析现有 PDF 文档。
277    pub read: bool,
278    /// 可以操作(合并、拆分、旋转)现有 PDF。
279    pub manipulate: bool,
280    /// 可以填写 PDF 模板中的表单字段。
281    pub fill_forms: bool,
282    /// 支持加密。
283    pub encrypt: bool,
284    /// 支持数字签名。
285    pub sign: bool,
286    /// 支持 PDF/A 验证。
287    pub pdfa: bool,
288}
289
290impl EngineCapabilities {
291    /// lopdf 的能力集。
292    #[must_use]
293    pub const fn lopdf() -> Self {
294        Self {
295            create: false,
296            read: true,
297            manipulate: true,
298            fill_forms: true,
299            encrypt: false,
300            sign: false,
301            pdfa: false,
302        }
303    }
304
305    /// printpdf 的能力集。
306    #[must_use]
307    pub const fn printpdf() -> Self {
308        Self {
309            create: true,
310            read: false,
311            manipulate: false,
312            fill_forms: false,
313            encrypt: false,
314            sign: false,
315            pdfa: false,
316        }
317    }
318
319    /// 转换为使用 [`CapabilityLevel`] 值的 [`DetailedEngineCapabilities`]。
320    ///
321    /// 布尔值 `true` 映射到 [`CapabilityLevel::Structural`];
322    /// `false` 映射到 [`CapabilityLevel::None`]。
323    #[must_use]
324    pub const fn to_detailed(&self) -> DetailedEngineCapabilities {
325        DetailedEngineCapabilities {
326            text_extraction: if self.read {
327                CapabilityLevel::Structural
328            } else {
329                CapabilityLevel::None
330            },
331            metadata: if self.read {
332                CapabilityLevel::Structural
333            } else {
334                CapabilityLevel::None
335            },
336            image_extraction: if self.read {
337                CapabilityLevel::Heuristic
338            } else {
339                CapabilityLevel::None
340            },
341            table_detection: if self.read {
342                CapabilityLevel::Heuristic
343            } else {
344                CapabilityLevel::None
345            },
346            rendering: if self.create {
347                CapabilityLevel::Structural
348            } else {
349                CapabilityLevel::None
350            },
351        }
352    }
353}
354
355/// 用于细粒度引擎功能报告的渐进式能力级别。
356///
357/// 与布尔值不同,这传达了引擎对某项功能的支持*程度*,
358/// 使下游代码(如 markdown 处理器链)能选择最佳可用引擎
359/// 或优雅降级。
360#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
361#[non_exhaustive]
362pub enum CapabilityLevel {
363    /// 不支持该功能。
364    #[default]
365    None,
366    /// 通过启发式或近似方法支持该功能。
367    Heuristic,
368    /// 以完整的结构精度支持该功能。
369    Structural,
370    /// 通过云端加速或 AI 辅助处理支持该功能。
371    Cloud,
372}
373
374impl CapabilityLevel {
375    /// 返回此级别是否代表任何程度的支持(非 `None`)。
376    #[must_use]
377    pub const fn is_supported(&self) -> bool {
378        !matches!(self, Self::None)
379    }
380}
381
382/// 使用渐进式 [`CapabilityLevel`] 值的详细引擎能力。
383///
384/// 这补充了基于布尔值的 [`EngineCapabilities`],
385/// 适用于支持*质量*很重要的场景(如在 markdown 处理器链中
386/// 选择提取策略)。
387#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
388pub struct DetailedEngineCapabilities {
389    /// 文本提取支持的质量。
390    pub text_extraction: CapabilityLevel,
391    /// 元数据(XMP、文档信息)提取的质量。
392    pub metadata: CapabilityLevel,
393    /// 图片提取支持的质量。
394    pub image_extraction: CapabilityLevel,
395    /// 表格检测和提取的质量。
396    pub table_detection: CapabilityLevel,
397    /// 渲染(文档创建)支持的质量。
398    pub rendering: CapabilityLevel,
399}
400
401#[cfg(test)]
402#[allow(clippy::uninlined_format_args, clippy::float_cmp)]
403mod tests {
404    use super::*;
405
406    #[test]
407    fn capability_level_ordering() {
408        assert!(CapabilityLevel::None < CapabilityLevel::Heuristic);
409        assert!(CapabilityLevel::Heuristic < CapabilityLevel::Structural);
410        assert!(CapabilityLevel::Structural < CapabilityLevel::Cloud);
411    }
412
413    #[test]
414    fn capability_level_default_is_none() {
415        assert_eq!(CapabilityLevel::default(), CapabilityLevel::None);
416    }
417
418    #[test]
419    fn capability_level_is_supported() {
420        assert!(!CapabilityLevel::None.is_supported());
421        assert!(CapabilityLevel::Heuristic.is_supported());
422        assert!(CapabilityLevel::Structural.is_supported());
423        assert!(CapabilityLevel::Cloud.is_supported());
424    }
425
426    #[test]
427    fn capability_level_debug() {
428        assert_eq!(format!("{:?}", CapabilityLevel::Cloud), "Cloud");
429    }
430
431    #[test]
432    fn detailed_capabilities_default() {
433        let dc = DetailedEngineCapabilities::default();
434        assert_eq!(dc.text_extraction, CapabilityLevel::None);
435        assert_eq!(dc.metadata, CapabilityLevel::None);
436        assert_eq!(dc.image_extraction, CapabilityLevel::None);
437        assert_eq!(dc.table_detection, CapabilityLevel::None);
438        assert_eq!(dc.rendering, CapabilityLevel::None);
439    }
440
441    #[test]
442    fn engine_capabilities_to_detailed_lopdf() {
443        let caps = EngineCapabilities::lopdf();
444        let detailed = caps.to_detailed();
445        assert_eq!(detailed.text_extraction, CapabilityLevel::Structural);
446        assert_eq!(detailed.metadata, CapabilityLevel::Structural);
447        assert_eq!(detailed.image_extraction, CapabilityLevel::Heuristic);
448        assert_eq!(detailed.table_detection, CapabilityLevel::Heuristic);
449        assert_eq!(detailed.rendering, CapabilityLevel::None);
450    }
451
452    #[test]
453    fn engine_capabilities_to_detailed_printpdf() {
454        let caps = EngineCapabilities::printpdf();
455        let detailed = caps.to_detailed();
456        assert_eq!(detailed.text_extraction, CapabilityLevel::None);
457        assert_eq!(detailed.metadata, CapabilityLevel::None);
458        assert_eq!(detailed.image_extraction, CapabilityLevel::None);
459        assert_eq!(detailed.table_detection, CapabilityLevel::None);
460        assert_eq!(detailed.rendering, CapabilityLevel::Structural);
461    }
462
463    #[test]
464    fn detailed_capabilities_equality() {
465        let a = DetailedEngineCapabilities {
466            text_extraction: CapabilityLevel::Structural,
467            ..Default::default()
468        };
469        let b = DetailedEngineCapabilities {
470            text_extraction: CapabilityLevel::Structural,
471            ..Default::default()
472        };
473        assert_eq!(a, b);
474    }
475
476    #[test]
477    fn detailed_capabilities_inequality() {
478        let a = DetailedEngineCapabilities {
479            text_extraction: CapabilityLevel::Structural,
480            ..Default::default()
481        };
482        let b = DetailedEngineCapabilities {
483            text_extraction: CapabilityLevel::Heuristic,
484            ..Default::default()
485        };
486        assert_ne!(a, b);
487    }
488
489    #[test]
490    fn detailed_capabilities_debug() {
491        let dc = DetailedEngineCapabilities::default();
492        let dbg = format!("{:?}", dc);
493        assert!(dbg.contains("DetailedEngineCapabilities"));
494    }
495
496    #[test]
497    fn detailed_capabilities_clone() {
498        let dc = DetailedEngineCapabilities {
499            text_extraction: CapabilityLevel::Cloud,
500            metadata: CapabilityLevel::Structural,
501            image_extraction: CapabilityLevel::Heuristic,
502            table_detection: CapabilityLevel::None,
503            rendering: CapabilityLevel::Structural,
504        };
505        let cloned = dc;
506        assert_eq!(dc, cloned);
507    }
508
509    #[test]
510    fn engine_capabilities_default() {
511        let caps = EngineCapabilities::default();
512        assert!(!caps.create);
513        assert!(!caps.read);
514        assert!(!caps.manipulate);
515        assert!(!caps.fill_forms);
516        assert!(!caps.encrypt);
517        assert!(!caps.sign);
518        assert!(!caps.pdfa);
519    }
520
521    #[test]
522    fn engine_capabilities_debug() {
523        let caps = EngineCapabilities::lopdf();
524        let dbg = format!("{:?}", caps);
525        assert!(dbg.contains("EngineCapabilities"));
526    }
527
528    #[test]
529    fn engine_capabilities_clone() {
530        let caps = EngineCapabilities::printpdf();
531        let cloned = caps;
532        assert_eq!(caps.create, cloned.create);
533    }
534
535    #[test]
536    fn pdf_field_descriptor_new() {
537        let desc = PdfFieldDescriptor::new("pdf_name", "rust_name");
538        assert_eq!(desc.field_name, "pdf_name");
539        assert_eq!(desc.rust_field_name, "rust_name");
540        assert_eq!(desc.order, u32::MAX);
541        assert!(desc.format.is_none());
542        assert!(desc.default_value.is_none());
543        assert!(!desc.required);
544        assert!(!desc.nested);
545    }
546
547    #[test]
548    fn pdf_field_descriptor_debug() {
549        let desc = PdfFieldDescriptor::new("f", "r");
550        let dbg = format!("{:?}", desc);
551        assert!(dbg.contains("PdfFieldDescriptor"));
552    }
553
554    #[test]
555    fn pdf_field_descriptor_clone() {
556        let desc = PdfFieldDescriptor::new("f", "r");
557        let cloned = desc.clone();
558        assert_eq!(desc.field_name, cloned.field_name);
559    }
560
561    #[test]
562    fn pdf_model_metadata_default() {
563        let meta = PdfModelMetadata::default();
564        assert_eq!(meta.page_size, crate::enums::PageSize::A4);
565        assert_eq!(meta.margins, 72.0);
566    }
567
568    #[test]
569    fn pdf_model_metadata_debug() {
570        let meta = PdfModelMetadata::default();
571        let dbg = format!("{:?}", meta);
572        assert!(dbg.contains("PdfModelMetadata"));
573    }
574
575    #[test]
576    fn pdf_model_metadata_clone() {
577        let meta = PdfModelMetadata::default();
578        let cloned = meta.clone();
579        assert_eq!(meta.margins, cloned.margins);
580    }
581
582    #[test]
583    fn capability_level_clone_copy() {
584        let level = CapabilityLevel::Structural;
585        let copied = level;
586        assert_eq!(level, copied);
587    }
588
589    #[test]
590    fn capability_level_hash() {
591        use std::collections::HashSet;
592        let mut set = HashSet::new();
593        set.insert(CapabilityLevel::None);
594        set.insert(CapabilityLevel::None);
595        set.insert(CapabilityLevel::Heuristic);
596        assert_eq!(set.len(), 2);
597    }
598
599    #[test]
600    fn engine_capabilities_to_detailed_custom() {
601        let caps = EngineCapabilities {
602            create: true,
603            read: true,
604            manipulate: false,
605            fill_forms: false,
606            encrypt: false,
607            sign: false,
608            pdfa: false,
609        };
610        let detailed = caps.to_detailed();
611        assert_eq!(detailed.text_extraction, CapabilityLevel::Structural);
612        assert_eq!(detailed.metadata, CapabilityLevel::Structural);
613        assert_eq!(detailed.image_extraction, CapabilityLevel::Heuristic);
614        assert_eq!(detailed.table_detection, CapabilityLevel::Heuristic);
615        assert_eq!(detailed.rendering, CapabilityLevel::Structural);
616    }
617
618    #[test]
619    fn engine_capabilities_to_detailed_none() {
620        let caps = EngineCapabilities::default();
621        let detailed = caps.to_detailed();
622        assert_eq!(detailed.text_extraction, CapabilityLevel::None);
623        assert_eq!(detailed.metadata, CapabilityLevel::None);
624        assert_eq!(detailed.image_extraction, CapabilityLevel::None);
625        assert_eq!(detailed.table_detection, CapabilityLevel::None);
626        assert_eq!(detailed.rendering, CapabilityLevel::None);
627    }
628}