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
7pub 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
22pub mod crypto;
24
25pub mod io;
27pub mod layout;
28pub mod model;
29
30pub 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
46pub use model::{
48 ImageData, ImageFormat, ListItem, PdfBlock, PdfBlockType, PdfDocumentModel, PdfPageModel,
49 SourceLocation,
50};
51
52pub use io::{AtomicFileOutput, PdfInput, ResourceLimits};
54
55pub use layout::{Direction, FlowLayout, LayoutSink};
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 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 #[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 #[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 #[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 #[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 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 #[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 #[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 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}