Skip to main content

easypdf_core/
lib.rs

1#![doc = "`easypdf-rust` 的核心类型、trait、枚举、转换器与错误定义。"]
2#![warn(missing_docs)]
3#![warn(clippy::pedantic)]
4#![deny(unsafe_code)]
5#![cfg_attr(test, allow(clippy::similar_names))]
6
7// --- 模块 ---
8pub mod content;
9pub mod converter_registry;
10pub mod enums;
11pub mod error;
12pub mod event;
13pub mod handler_chain;
14pub mod logging;
15pub mod metadata;
16pub mod page_index;
17pub mod page_number;
18pub mod page_range;
19pub mod style;
20pub mod traits;
21
22// --- 加密(加密 + 签名)---
23pub mod crypto;
24
25// --- 合并的子 crate(第一波)---
26pub mod io;
27pub mod layout;
28pub mod model;
29
30// --- 便捷重导出 ---
31pub use content::{PdfImage, PdfLine, PdfRect, PdfTable, PdfTableCell, PdfText};
32pub use enums::{Orientation, PageSize, Rotation, TextAlignment, VerticalAlignment};
33pub use error::{PdfError, PdfErrorCode, Result};
34pub use event::PdfReadListener;
35pub use logging::{init_logging, init_logging_json};
36pub use metadata::{PdfBookmark, PdfMetadata};
37pub use page_index::PageIndex;
38pub use page_number::PageNumber;
39pub use page_range::PageRange;
40pub use style::{BuiltInFont, FontFamily, FontStyle, PdfColor, PdfFont, TableBorder, TableStyle};
41pub use traits::{
42    CapabilityLevel, DetailedEngineCapabilities, EngineCapabilities, PdfConverter, PdfEngine,
43    PdfFieldDescriptor, PdfModel, PdfModelMetadata, PdfWriteHandler, RenderedElement,
44};
45
46// --- 从合并的 model crate 重导出 ---
47pub use model::{
48    ImageData, ImageFormat, ListItem, PdfBlock, PdfBlockType, PdfDocumentModel, PdfPageModel,
49    SourceLocation,
50};
51
52// --- 从合并的 io crate 重导出(扁平类型)---
53pub use io::{AtomicFileOutput, PdfInput, ResourceLimits};
54
55// --- 从合并的 layout crate 重导出 ---
56pub use layout::{Direction, FlowLayout, LayoutSink};
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    /// Approximate equality for f64 in tests.
63    fn assert_f64_eq(actual: f64, expected: f64) {
64        assert!(
65            (actual - expected).abs() < f64::EPSILON,
66            "expected {expected}, got {actual}"
67        );
68    }
69
70    // --- Enums ---
71
72    #[test]
73    fn test_page_size_dimensions() {
74        assert_eq!(PageSize::A4.dimensions(), (595.0, 842.0));
75        assert_eq!(PageSize::A0.dimensions(), (2384.0, 3370.0));
76        assert_eq!(PageSize::A1.dimensions(), (1684.0, 2384.0));
77        assert_eq!(PageSize::A2.dimensions(), (1191.0, 1684.0));
78        assert_eq!(PageSize::A3.dimensions(), (842.0, 1191.0));
79        assert_eq!(PageSize::Letter.dimensions(), (612.0, 792.0));
80        assert_eq!(PageSize::Legal.dimensions(), (612.0, 1008.0));
81        assert_eq!(PageSize::A5.dimensions(), (420.0, 595.0));
82        assert_eq!(PageSize::Custom(100.0, 200.0).dimensions(), (100.0, 200.0));
83    }
84
85    #[test]
86    fn test_orientation_default() {
87        assert_eq!(Orientation::default(), Orientation::Portrait);
88    }
89
90    #[test]
91    fn test_text_alignment_default() {
92        assert_eq!(TextAlignment::default(), TextAlignment::Left);
93    }
94
95    #[test]
96    fn test_vertical_alignment_default() {
97        assert_eq!(VerticalAlignment::default(), VerticalAlignment::Top);
98    }
99
100    // --- Error ---
101
102    #[test]
103    fn test_pdf_error_display() {
104        let err = PdfError::InvalidPage(5);
105        assert!(format!("{err}").contains('5'));
106        let err = PdfError::Parse("bad data".into());
107        assert!(format!("{err}").contains("bad data"));
108        let err = PdfError::UnsupportedFeature("tables".into());
109        assert!(format!("{err}").contains("tables"));
110    }
111
112    #[test]
113    fn test_pdf_error_from_io() {
114        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
115        let pdf_err: PdfError = io_err.into();
116        assert!(matches!(pdf_err, PdfError::Io(_)));
117    }
118
119    // --- Style ---
120
121    #[test]
122    fn test_pdf_color_rgb_u8() {
123        let color = PdfColor::rgb_u8(255, 0, 128);
124        match color {
125            PdfColor::Rgb(r, g, b) => {
126                assert!((r - 1.0).abs() < 0.001);
127                assert!((g - 0.0).abs() < 0.001);
128                assert!((b - 0.5).abs() < 0.01);
129            }
130            _ => panic!("expected Rgb"),
131        }
132    }
133
134    #[test]
135    fn test_pdf_color_constants() {
136        assert_eq!(PdfColor::black(), PdfColor::Rgb(0.0, 0.0, 0.0));
137        assert_eq!(PdfColor::white(), PdfColor::Rgb(1.0, 1.0, 1.0));
138        assert_eq!(PdfColor::red(), PdfColor::Rgb(1.0, 0.0, 0.0));
139        assert_eq!(PdfColor::green(), PdfColor::Rgb(0.0, 1.0, 0.0));
140        assert_eq!(PdfColor::blue(), PdfColor::Rgb(0.0, 0.0, 1.0));
141    }
142
143    #[test]
144    fn test_pdf_color_cmyk() {
145        let c = PdfColor::Cmyk(0.1, 0.2, 0.3, 0.4);
146        if let PdfColor::Cmyk(c1, m, y, k) = c {
147            assert!((c1 - 0.1).abs() < 0.001);
148            assert!((m - 0.2).abs() < 0.001);
149            assert!((y - 0.3).abs() < 0.001);
150            assert!((k - 0.4).abs() < 0.001);
151        } else {
152            panic!("expected Cmyk");
153        }
154    }
155
156    #[test]
157    fn test_pdf_color_gray() {
158        assert_eq!(PdfColor::Gray(0.5), PdfColor::Gray(0.5));
159        assert_eq!(PdfColor::light_gray(), PdfColor::Gray(0.8));
160        assert_eq!(PdfColor::gray(), PdfColor::Gray(0.5));
161    }
162
163    #[test]
164    fn test_pdf_font_constructors() {
165        let f = PdfFont::helvetica(12.0);
166        assert_f64_eq(f.size, 12.0);
167        assert!(matches!(
168            f.family,
169            FontFamily::BuiltIn(BuiltInFont::Helvetica)
170        ));
171
172        let f = PdfFont::times_roman(14.0);
173        assert_f64_eq(f.size, 14.0);
174
175        let f = PdfFont::courier(10.0);
176        assert_f64_eq(f.size, 10.0);
177    }
178
179    #[test]
180    fn test_pdf_font_builder() {
181        let f = PdfFont::helvetica(12.0).bold().italic().with_size(16.0);
182        assert_f64_eq(f.size, 16.0);
183        assert!(f.style.bold);
184        assert!(f.style.italic);
185    }
186
187    #[test]
188    fn test_pdf_font_default() {
189        let f = PdfFont::default();
190        assert_f64_eq(f.size, 12.0);
191        assert!(!f.style.bold);
192        assert!(!f.style.italic);
193    }
194
195    #[test]
196    fn test_table_style_defaults() {
197        let s = TableStyle::default();
198        assert!(s.header_bg.is_some());
199        assert!(s.header_font.style.bold);
200        assert!(!s.striped);
201    }
202
203    #[test]
204    fn test_table_style_simple() {
205        let s = TableStyle::simple();
206        assert!(s.header_bg.is_none());
207        assert!(!s.striped);
208    }
209
210    #[test]
211    fn test_table_style_striped() {
212        let s = TableStyle::striped();
213        assert!(s.header_bg.is_some());
214        assert!(s.striped);
215    }
216
217    #[test]
218    fn test_table_border_default() {
219        let b = TableBorder::default();
220        assert_f64_eq(b.width, 0.5);
221        assert_eq!(b.color, PdfColor::black());
222    }
223
224    // --- Content ---
225
226    #[test]
227    fn test_pdf_text_new() {
228        let t = PdfText::new("hello");
229        assert_eq!(t.content, "hello");
230        assert_eq!(t.alignment, TextAlignment::Left);
231    }
232
233    #[test]
234    fn test_pdf_text_builder() {
235        let t = PdfText::new("hi")
236            .font(PdfFont::courier(10.0))
237            .alignment(TextAlignment::Center)
238            .color(PdfColor::red());
239        assert_eq!(t.content, "hi");
240        assert_eq!(t.alignment, TextAlignment::Center);
241        assert_eq!(t.color, PdfColor::red());
242    }
243
244    #[test]
245    fn test_pdf_table_builder() {
246        let t = PdfTable::new(vec!["A".into(), "B".into()])
247            .row(vec!["1".into(), "2".into()])
248            .rows(vec![vec!["3".into(), "4".into()]])
249            .width(500.0);
250        assert_eq!(t.headers.len(), 2);
251        assert_eq!(t.rows.len(), 2);
252        assert_f64_eq(t.width, 500.0);
253    }
254
255    #[test]
256    fn test_pdf_table_cell_default() {
257        let c = PdfTableCell::default();
258        assert!(c.content.is_empty());
259        assert_eq!(c.h_alignment, TextAlignment::Left);
260        assert_eq!(c.v_alignment, VerticalAlignment::Top);
261    }
262
263    #[test]
264    fn test_pdf_image_from_bytes() {
265        let img = PdfImage::from_bytes(vec![1, 2, 3]);
266        assert_eq!(img.data, vec![1, 2, 3]);
267        assert_f64_eq(img.width, 0.0);
268        assert_f64_eq(img.height, 0.0);
269    }
270
271    #[test]
272    fn test_pdf_image_from_path_nonexistent() {
273        let result = PdfImage::from_path("/nonexistent/image.png");
274        assert!(result.is_err());
275    }
276
277    #[test]
278    fn test_pdf_image_from_path_temp() {
279        let dir = std::env::temp_dir();
280        let path = dir.join("easypdf_test_real.png");
281        // Create a minimal PNG
282        let png = vec![
283            0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48,
284            0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00,
285            0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08,
286            0xD7, 0x63, 0xF8, 0xFF, 0xFF, 0x3F, 0x00, 0x05, 0xFE, 0x02, 0xFE, 0xDC, 0xCC, 0x59,
287            0xE7, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
288        ];
289        std::fs::write(&path, &png).unwrap();
290        let result = PdfImage::from_path(&path);
291        assert!(result.is_ok());
292        let _ = std::fs::remove_file(&path);
293    }
294
295    // --- Metadata ---
296
297    #[test]
298    fn test_pdf_metadata_builder() {
299        let m = PdfMetadata::new()
300            .title("T")
301            .author("A")
302            .subject("S")
303            .keywords("K");
304        assert_eq!(m.title.as_deref(), Some("T"));
305        assert_eq!(m.author.as_deref(), Some("A"));
306        assert_eq!(m.subject.as_deref(), Some("S"));
307        assert_eq!(m.keywords.as_deref(), Some("K"));
308    }
309
310    #[test]
311    fn test_pdf_bookmark_new() {
312        let b = PdfBookmark::new("Chapter 1", 1);
313        assert_eq!(b.title, "Chapter 1");
314        assert_eq!(b.page, 1);
315        assert!(b.children.is_empty());
316    }
317
318    #[test]
319    fn test_pdf_bookmark_child() {
320        let b = PdfBookmark::new("Ch1", 1).child(PdfBookmark::new("1.1", 2));
321        assert_eq!(b.children.len(), 1);
322        assert_eq!(b.children[0].title, "1.1");
323    }
324
325    // --- Traits ---
326
327    #[test]
328    fn test_pdf_model_metadata_default() {
329        let m = PdfModelMetadata::default();
330        assert_eq!(m.page_size, PageSize::A4);
331        assert_eq!(m.orientation, Orientation::Portrait);
332        assert_f64_eq(m.margins, 72.0);
333    }
334
335    struct TestListener {
336        pages: Vec<String>,
337    }
338
339    impl PdfReadListener for TestListener {
340        fn on_text(&mut self, _page: usize, text: &str) -> Result<()> {
341            self.pages.push(text.to_string());
342            Ok(())
343        }
344    }
345
346    #[test]
347    fn test_pdf_read_listener_defaults() {
348        let mut listener = TestListener { pages: vec![] };
349        // Default no-ops should not panic
350        assert!(listener.on_page_start(1).is_ok());
351        assert!(listener.on_page_end(1).is_ok());
352        assert!(listener.on_document_end().is_ok());
353    }
354
355    struct TestHandler;
356
357    impl PdfWriteHandler for TestHandler {}
358
359    #[test]
360    fn test_pdf_write_handler_defaults() {
361        let mut handler = TestHandler;
362        assert!(handler.before_document().is_ok());
363        assert!(handler.before_page(1).is_ok());
364        assert!(handler.after_page(1).is_ok());
365        assert!(handler.after_document().is_ok());
366    }
367
368    struct TestConverter;
369
370    impl PdfConverter<i32> for TestConverter {
371        fn to_pdf_string(&self, value: &i32) -> Result<String> {
372            Ok(value.to_string())
373        }
374        fn from_pdf_string(&self, s: &str) -> Result<i32> {
375            s.parse::<i32>().map_err(|e| PdfError::Other(e.to_string()))
376        }
377    }
378
379    #[test]
380    fn test_pdf_converter_roundtrip() {
381        let c = TestConverter;
382        assert_eq!(c.to_pdf_string(&42).unwrap(), "42");
383        assert_eq!(c.from_pdf_string("99").unwrap(), 99);
384        assert!(c.from_pdf_string("abc").is_err());
385    }
386
387    #[test]
388    fn test_rendered_element_debug() {
389        let e = RenderedElement::Text {
390            x: 1.0,
391            y: 2.0,
392            text: PdfText::new("hi"),
393        };
394        assert!(format!("{e:?}").contains("Text"));
395    }
396
397    #[test]
398    fn test_xmp_metadata() {
399        let xmp = PdfMetadata::new()
400            .title("Test Title")
401            .author("Author Name")
402            .to_xmp();
403        assert!(xmp.contains("Test Title"));
404        assert!(xmp.contains("Author Name"));
405        assert!(xmp.contains("xmpmeta"));
406    }
407
408    #[test]
409    fn test_engine_capabilities_lopdf() {
410        let c = EngineCapabilities::lopdf();
411        assert!(c.read);
412        assert!(c.manipulate);
413        assert!(c.fill_forms);
414        assert!(!c.create);
415    }
416
417    #[test]
418    fn test_engine_capabilities_printpdf() {
419        let c = EngineCapabilities::printpdf();
420        assert!(c.create);
421        assert!(!c.read);
422    }
423}