Skip to main content

easypdf_markdown/render/
mod.rs

1//! PDF 页面渲染为光栅图像。
2//!
3//! 本模块提供基于 trait 的抽象([`PdfRenderer`])用于将 PDF 页面渲染为
4//! 光栅图像(PNG、JPEG)。两个可用后端:
5//!
6//! - **`TextRenderer`**(默认,纯 Rust)——通过 [`easypdf_reader::PdfReader`]
7//!   提取文本并渲染为简单位图图像。质量较低但足以满足 OCR 流水线需求,无外部依赖。
8//!
9//! - **`PdfiumRenderer`**(feature `pdfium`)——使用 Google `PDFium` 进行
10//!   高质量渲染,运行时需要 `libpdfium` 动态库。
11//!
12//! # 快速开始
13//!
14//! ```no_run
15//! use easypdf_markdown::render::{render_page_to_png, render_all_pages_to_dir};
16//!
17//! // 以 150 DPI 渲染第 0 页:
18//! render_page_to_png("input.pdf".as_ref(), 0, "page_0.png".as_ref(), 150)?;
19//!
20//! // 将全部页面渲染到目录:
21//! let paths = render_all_pages_to_dir("input.pdf".as_ref(), "output/".as_ref(), 150)?;
22//! # Ok::<(), easypdf_markdown::render::RenderError>(())
23//! ```
24//!
25//! # 选择后端
26//!
27//! 使用 [`RenderBackend::default_backend`] 自动选择最佳可用后端,或手动指定:
28//!
29//! ```no_run
30//! use easypdf_markdown::render::RenderBackend;
31//!
32//! let renderer = RenderBackend::default_backend()
33//!     .build_renderer("document.pdf".as_ref())?;
34//! # Ok::<(), easypdf_markdown::render::RenderError>(())
35//! ```
36
37pub mod backend;
38pub mod backends;
39pub mod config;
40mod convenience;
41pub mod error;
42pub mod traits;
43
44// --- 公共类型重导出 ---
45pub use backend::RenderBackend;
46pub use config::{Background, ImageFormat, RenderConfig};
47pub use convenience::{render_all_pages_to_dir, render_page, render_page_to_png};
48pub use error::{RenderError, Result};
49pub use traits::{PdfRenderer, RenderedImage};
50
51#[cfg(test)]
52mod tests {
53    #![allow(clippy::similar_names, clippy::float_cmp)]
54    use super::*;
55
56    /// 辅助函数:在内存中构建一个最小有效 PDF。
57    fn make_test_pdf_bytes() -> Vec<u8> {
58        let mut doc = lopdf::Document::new();
59        let content_id = doc.add_object(lopdf::Object::Stream(lopdf::Stream::new(
60            lopdf::Dictionary::new(),
61            b"BT /F1 12 Tf 72 700 Td (Hello) Tj ET".to_vec(),
62        )));
63        let mut font_dict = lopdf::Dictionary::new();
64        font_dict.set("Type", lopdf::Object::Name(b"Font".to_vec()));
65        font_dict.set("Subtype", lopdf::Object::Name(b"Type1".to_vec()));
66        font_dict.set("BaseFont", lopdf::Object::Name(b"Helvetica".to_vec()));
67        let font_id = doc.add_object(lopdf::Object::Dictionary(font_dict));
68        let mut resources = lopdf::Dictionary::new();
69        let mut fonts = lopdf::Dictionary::new();
70        fonts.set("F1", lopdf::Object::Reference(font_id));
71        resources.set("Font", lopdf::Object::Dictionary(fonts));
72        let resources_id = doc.add_object(lopdf::Object::Dictionary(resources));
73        let mut page_dict = lopdf::Dictionary::new();
74        page_dict.set("Type", lopdf::Object::Name(b"Page".to_vec()));
75        page_dict.set(
76            "MediaBox",
77            lopdf::Object::Array(vec![0.into(), 0.into(), 595.into(), 842.into()]),
78        );
79        page_dict.set("Contents", lopdf::Object::Reference(content_id));
80        page_dict.set("Resources", lopdf::Object::Reference(resources_id));
81        let page_id = doc.add_object(lopdf::Object::Dictionary(page_dict));
82        let mut pages_dict = lopdf::Dictionary::new();
83        pages_dict.set("Type", lopdf::Object::Name(b"Pages".to_vec()));
84        pages_dict.set(
85            "Kids",
86            lopdf::Object::Array(vec![lopdf::Object::Reference(page_id)]),
87        );
88        pages_dict.set("Count", lopdf::Object::Integer(1));
89        let pages_id = doc.add_object(lopdf::Object::Dictionary(pages_dict));
90        let mut catalog = lopdf::Dictionary::new();
91        catalog.set("Type", lopdf::Object::Name(b"Catalog".to_vec()));
92        catalog.set("Pages", lopdf::Object::Reference(pages_id));
93        let catalog_id = doc.add_object(lopdf::Object::Dictionary(catalog));
94        doc.trailer
95            .set("Root", lopdf::Object::Reference(catalog_id));
96        let mut buf = Vec::new();
97        doc.save_to(&mut buf).unwrap();
98        buf
99    }
100
101    #[test]
102    fn test_render_page_to_png_convenience() {
103        let bytes = make_test_pdf_bytes();
104        let dir = std::env::temp_dir().join("easypdf_render_lib_test");
105        let _ = std::fs::remove_dir_all(&dir);
106        // 将 PDF 写入临时文件(便捷 API 需要路径)。
107        let pdf_path = dir.join("test.pdf");
108        std::fs::create_dir_all(&dir).unwrap();
109        std::fs::write(&pdf_path, &bytes).unwrap();
110
111        let output = dir.join("page_0.png");
112        render_page_to_png(&pdf_path, 0, &output, 72).unwrap();
113        assert!(output.exists());
114
115        let _ = std::fs::remove_dir_all(&dir);
116    }
117
118    #[test]
119    fn test_render_all_pages_to_dir() {
120        let bytes = make_test_pdf_bytes();
121        let dir = std::env::temp_dir().join("easypdf_render_all_test");
122        let _ = std::fs::remove_dir_all(&dir);
123        std::fs::create_dir_all(&dir).unwrap();
124        let pdf_path = dir.join("test.pdf");
125        std::fs::write(&pdf_path, &bytes).unwrap();
126
127        let out_dir = dir.join("output");
128        let paths = render_all_pages_to_dir(&pdf_path, &out_dir, 72).unwrap();
129        assert_eq!(paths.len(), 1);
130        assert!(paths[0].exists());
131
132        let _ = std::fs::remove_dir_all(&dir);
133    }
134
135    #[test]
136    fn test_render_page_in_memory() {
137        let bytes = make_test_pdf_bytes();
138        let dir = std::env::temp_dir().join("easypdf_render_mem_test");
139        let _ = std::fs::remove_dir_all(&dir);
140        std::fs::create_dir_all(&dir).unwrap();
141        let pdf_path = dir.join("test.pdf");
142        std::fs::write(&pdf_path, &bytes).unwrap();
143
144        let config = RenderConfig {
145            dpi: 72,
146            ..RenderConfig::default()
147        };
148        let img = render_page(&pdf_path, 0, &config).unwrap();
149        assert!(img.width > 0);
150        assert!(img.height > 0);
151
152        let _ = std::fs::remove_dir_all(&dir);
153    }
154
155    #[test]
156    fn test_default_backend_is_text() {
157        let backend = RenderBackend::default_backend();
158        // 未启用 pdfium feature 时,默认应为 Text。
159        assert_eq!(backend, RenderBackend::Text);
160    }
161
162    #[test]
163    fn test_backend_display() {
164        assert_eq!(RenderBackend::Text.to_string(), "text");
165        assert_eq!(RenderBackend::Pdfium.to_string(), "pdfium");
166    }
167
168    #[test]
169    fn test_text_backend_is_available() {
170        assert!(RenderBackend::Text.is_available());
171    }
172
173    #[test]
174    fn test_pdfium_backend_not_available_without_feature() {
175        // 未启用 pdfium feature 时,Pdfium 应不可用。
176        #[cfg(not(feature = "pdfium"))]
177        assert!(!RenderBackend::Pdfium.is_available());
178    }
179}