1use crate::pdf_reader::PdfMetadata;
28use std::fs::File;
29use std::io::BufWriter;
30use std::path::{Path, PathBuf};
31use thiserror::Error;
32
33const POINTS_TO_MM: f32 = 0.352_778;
39
40const MM_PER_INCH: f32 = 25.4;
42
43const POINTS_PER_INCH: f32 = 72.0;
45
46const DEFAULT_DPI: u32 = 300;
48
49const HIGH_QUALITY_DPI: u32 = 600;
51
52const COMPACT_DPI: u32 = 150;
54
55const DEFAULT_JPEG_QUALITY: u8 = 90;
57
58const HIGH_QUALITY_JPEG: u8 = 95;
60
61const COMPACT_JPEG_QUALITY: u8 = 75;
63
64const MIN_JPEG_QUALITY: u8 = 1;
66
67const MAX_JPEG_QUALITY: u8 = 100;
69
70#[derive(Debug, Error)]
72pub enum PdfWriterError {
73 #[error("No images provided")]
74 NoImages,
75
76 #[error("Image not found: {0}")]
77 ImageNotFound(PathBuf),
78
79 #[error("Unsupported image format: {0}")]
80 UnsupportedFormat(String),
81
82 #[error("IO error: {0}")]
83 IoError(#[from] std::io::Error),
84
85 #[error("PDF generation error: {0}")]
86 GenerationError(String),
87}
88
89pub type Result<T> = std::result::Result<T, PdfWriterError>;
90
91#[derive(Debug, Clone)]
93pub struct PdfWriterOptions {
94 pub dpi: u32,
96 pub jpeg_quality: u8,
98 pub compression: ImageCompression,
100 pub page_size_mode: PageSizeMode,
102 pub metadata: Option<PdfMetadata>,
104 pub ocr_layer: Option<OcrLayer>,
106}
107
108impl Default for PdfWriterOptions {
109 fn default() -> Self {
110 Self {
111 dpi: DEFAULT_DPI,
112 jpeg_quality: DEFAULT_JPEG_QUALITY,
113 compression: ImageCompression::Jpeg,
114 page_size_mode: PageSizeMode::FirstPage,
115 metadata: None,
116 ocr_layer: None,
117 }
118 }
119}
120
121impl PdfWriterOptions {
122 pub fn builder() -> PdfWriterOptionsBuilder {
124 PdfWriterOptionsBuilder::default()
125 }
126
127 pub fn high_quality() -> Self {
129 Self {
130 dpi: HIGH_QUALITY_DPI,
131 jpeg_quality: HIGH_QUALITY_JPEG,
132 compression: ImageCompression::JpegLossless,
133 ..Default::default()
134 }
135 }
136
137 pub fn compact() -> Self {
139 Self {
140 dpi: COMPACT_DPI,
141 jpeg_quality: COMPACT_JPEG_QUALITY,
142 compression: ImageCompression::Jpeg,
143 ..Default::default()
144 }
145 }
146}
147
148#[derive(Debug, Default)]
150pub struct PdfWriterOptionsBuilder {
151 options: PdfWriterOptions,
152}
153
154impl PdfWriterOptionsBuilder {
155 #[must_use]
157 pub fn dpi(mut self, dpi: u32) -> Self {
158 self.options.dpi = dpi;
159 self
160 }
161
162 #[must_use]
164 pub fn jpeg_quality(mut self, quality: u8) -> Self {
165 self.options.jpeg_quality = quality.clamp(MIN_JPEG_QUALITY, MAX_JPEG_QUALITY);
166 self
167 }
168
169 #[must_use]
171 pub fn compression(mut self, compression: ImageCompression) -> Self {
172 self.options.compression = compression;
173 self
174 }
175
176 #[must_use]
178 pub fn page_size_mode(mut self, mode: PageSizeMode) -> Self {
179 self.options.page_size_mode = mode;
180 self
181 }
182
183 #[must_use]
185 pub fn metadata(mut self, metadata: PdfMetadata) -> Self {
186 self.options.metadata = Some(metadata);
187 self
188 }
189
190 #[must_use]
192 pub fn ocr_layer(mut self, layer: OcrLayer) -> Self {
193 self.options.ocr_layer = Some(layer);
194 self
195 }
196
197 #[must_use]
199 pub fn build(self) -> PdfWriterOptions {
200 self.options
201 }
202}
203
204#[derive(Debug, Clone, Copy, Default)]
206pub enum ImageCompression {
207 #[default]
208 Jpeg,
209 JpegLossless,
210 Flate,
211 None,
212}
213
214#[derive(Debug, Clone, Copy, Default)]
216pub enum PageSizeMode {
217 #[default]
219 FirstPage,
220 MaxSize,
222 Fixed { width_pt: f64, height_pt: f64 },
224 Original,
226}
227
228#[derive(Debug, Clone)]
230pub struct OcrLayer {
231 pub pages: Vec<OcrPageText>,
232}
233
234#[derive(Debug, Clone)]
236pub struct OcrPageText {
237 pub page_index: usize,
238 pub blocks: Vec<TextBlock>,
239}
240
241#[derive(Debug, Clone)]
243pub struct TextBlock {
244 pub x: f64,
246 pub y: f64,
248 pub width: f64,
250 pub height: f64,
252 pub text: String,
254 pub font_size: f64,
256 pub vertical: bool,
258}
259
260pub trait PdfWriter {
262 fn create_from_images(
264 images: &[PathBuf],
265 output: &Path,
266 options: &PdfWriterOptions,
267 ) -> Result<()>;
268
269 fn create_streaming(
271 images: impl Iterator<Item = PathBuf>,
272 output: &Path,
273 options: &PdfWriterOptions,
274 ) -> Result<()>;
275}
276
277pub struct PrintPdfWriter;
279
280impl PrintPdfWriter {
281 pub fn new() -> Self {
283 Self
284 }
285
286 pub fn create_from_images(
288 images: &[PathBuf],
289 output: &Path,
290 options: &PdfWriterOptions,
291 ) -> Result<()> {
292 if images.is_empty() {
293 return Err(PdfWriterError::NoImages);
294 }
295
296 for img_path in images {
298 if !img_path.exists() {
299 return Err(PdfWriterError::ImageNotFound(img_path.clone()));
300 }
301 }
302
303 let first_img =
305 image::open(&images[0]).map_err(|e| PdfWriterError::GenerationError(e.to_string()))?;
306
307 let (width_px, height_px) = (first_img.width(), first_img.height());
308 let dpi = options.dpi as f64;
309
310 let width_mm = (width_px as f32 / dpi as f32) * MM_PER_INCH;
312 let height_mm = (height_px as f32 / dpi as f32) * MM_PER_INCH;
313
314 let title = options
316 .metadata
317 .as_ref()
318 .and_then(|m| m.title.as_deref())
319 .unwrap_or("Document");
320
321 let (doc, page1, layer1) = printpdf::PdfDocument::new(
322 title,
323 printpdf::Mm(width_mm),
324 printpdf::Mm(height_mm),
325 "Layer 1",
326 );
327
328 Self::add_image_to_layer(&doc, page1, layer1, &first_img, width_mm, height_mm)?;
330
331 if let Some(ref ocr_layer) = options.ocr_layer {
333 if let Some(page_text) = ocr_layer.pages.iter().find(|p| p.page_index == 0) {
334 let text_layer = doc.get_page(page1).add_layer("OCR Text");
335 Self::add_ocr_text(&doc, text_layer, page_text, height_mm)?;
336 }
337 }
338
339 for (img_idx, img_path) in images.iter().enumerate().skip(1) {
341 let img = image::open(img_path)
342 .map_err(|e| PdfWriterError::GenerationError(e.to_string()))?;
343
344 let dpi_f32 = options.dpi as f32;
345 let (w_px, h_px) = match options.page_size_mode {
346 PageSizeMode::FirstPage => (width_px, height_px),
347 PageSizeMode::Original => (img.width(), img.height()),
348 PageSizeMode::MaxSize => (width_px.max(img.width()), height_px.max(img.height())),
349 PageSizeMode::Fixed {
350 width_pt,
351 height_pt,
352 } => {
353 let w = (width_pt as f32 * dpi_f32 / POINTS_PER_INCH) as u32;
355 let h = (height_pt as f32 * dpi_f32 / POINTS_PER_INCH) as u32;
356 (w, h)
357 }
358 };
359
360 let w_mm = (w_px as f32 / dpi_f32) * MM_PER_INCH;
361 let h_mm = (h_px as f32 / dpi_f32) * MM_PER_INCH;
362
363 let (page, layer) = doc.add_page(printpdf::Mm(w_mm), printpdf::Mm(h_mm), "Layer 1");
364
365 Self::add_image_to_layer(&doc, page, layer, &img, w_mm, h_mm)?;
366
367 if let Some(ref ocr_layer) = options.ocr_layer {
369 if let Some(page_text) = ocr_layer.pages.iter().find(|p| p.page_index == img_idx) {
370 let text_layer = doc.get_page(page).add_layer("OCR Text");
371 Self::add_ocr_text(&doc, text_layer, page_text, h_mm)?;
372 }
373 }
374 }
375
376 let file = File::create(output)?;
378 let mut writer = BufWriter::new(file);
379 doc.save(&mut writer)
380 .map_err(|e| PdfWriterError::GenerationError(e.to_string()))?;
381
382 Ok(())
383 }
384
385 fn add_image_to_layer(
387 doc: &printpdf::PdfDocumentReference,
388 page: printpdf::PdfPageIndex,
389 layer: printpdf::PdfLayerIndex,
390 img: &image::DynamicImage,
391 width_mm: f32,
392 height_mm: f32,
393 ) -> Result<()> {
394 use printpdf::{Image, ImageTransform, Mm, Px};
395
396 let rgb_img = img.to_rgb8();
398 let (img_width, img_height) = rgb_img.dimensions();
399
400 let image_data = printpdf::ImageXObject {
402 width: Px(img_width as usize),
403 height: Px(img_height as usize),
404 color_space: printpdf::ColorSpace::Rgb,
405 bits_per_component: printpdf::ColorBits::Bit8,
406 interpolate: true,
407 image_data: rgb_img.into_raw(),
408 image_filter: None,
409 clipping_bbox: None,
410 smask: None,
411 };
412
413 let image = Image::from(image_data);
414
415 let layer_ref = doc.get_page(page).get_layer(layer);
417
418 let dpi = 300.0_f32;
422 let img_width_pt = img_width as f32 * 72.0 / dpi;
423 let img_height_pt = img_height as f32 * 72.0 / dpi;
424
425 let width_pt = width_mm * 72.0 / MM_PER_INCH;
427 let height_pt = height_mm * 72.0 / MM_PER_INCH;
428
429 let scale_x = width_pt / img_width_pt;
431 let scale_y = height_pt / img_height_pt;
432
433 let transform = ImageTransform {
434 translate_x: Some(Mm(0.0)),
435 translate_y: Some(Mm(0.0)),
436 scale_x: Some(scale_x),
437 scale_y: Some(scale_y),
438 rotate: None,
439 dpi: Some(dpi),
440 };
441
442 image.add_to_layer(layer_ref, transform);
443
444 Ok(())
445 }
446
447 fn add_ocr_text(
451 doc: &printpdf::PdfDocumentReference,
452 layer: printpdf::PdfLayerReference,
453 page_text: &OcrPageText,
454 page_height_mm: f32,
455 ) -> Result<()> {
456 use printpdf::Mm;
457
458 let font = doc
461 .add_builtin_font(printpdf::BuiltinFont::Helvetica)
462 .map_err(|e| PdfWriterError::GenerationError(e.to_string()))?;
463
464 for block in &page_text.blocks {
465 let x_mm = block.x as f32 * POINTS_TO_MM;
467 let y_mm = page_height_mm
469 - (block.y as f32 * POINTS_TO_MM)
470 - (block.height as f32 * POINTS_TO_MM);
471 let font_size_pt = block.font_size as f32;
472
473 layer.set_text_rendering_mode(printpdf::TextRenderingMode::Invisible);
476
477 layer.use_text(&block.text, font_size_pt, Mm(x_mm), Mm(y_mm), &font);
478 }
479
480 Ok(())
481 }
482
483 pub fn create_streaming(
485 images: impl Iterator<Item = PathBuf>,
486 output: &Path,
487 options: &PdfWriterOptions,
488 ) -> Result<()> {
489 let images_vec: Vec<PathBuf> = images.collect();
490 Self::create_from_images(&images_vec, output, options)
491 }
492}
493
494impl Default for PrintPdfWriter {
495 fn default() -> Self {
496 Self::new()
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503 use tempfile::tempdir;
504
505 #[test]
507 fn test_empty_images_error() {
508 let temp_dir = tempdir().unwrap();
509 let output = temp_dir.path().join("output.pdf");
510
511 let result = PrintPdfWriter::create_from_images(&[], &output, &PdfWriterOptions::default());
512
513 assert!(matches!(result, Err(PdfWriterError::NoImages)));
514 }
515
516 #[test]
518 fn test_nonexistent_image_error() {
519 let temp_dir = tempdir().unwrap();
520 let output = temp_dir.path().join("output.pdf");
521
522 let images = vec![PathBuf::from("/nonexistent/image.jpg")];
523
524 let result =
525 PrintPdfWriter::create_from_images(&images, &output, &PdfWriterOptions::default());
526
527 assert!(matches!(result, Err(PdfWriterError::ImageNotFound(_))));
528 }
529
530 #[test]
531 fn test_default_options() {
532 let opts = PdfWriterOptions::default();
533
534 assert_eq!(opts.dpi, 300);
535 assert_eq!(opts.jpeg_quality, 90);
536 assert!(matches!(opts.compression, ImageCompression::Jpeg));
537 assert!(matches!(opts.page_size_mode, PageSizeMode::FirstPage));
538 assert!(opts.metadata.is_none());
539 assert!(opts.ocr_layer.is_none());
540 }
541
542 #[test]
546 fn test_single_image_to_pdf() {
547 let temp_dir = tempdir().unwrap();
548 let output = temp_dir.path().join("output.pdf");
549
550 let images = vec![PathBuf::from("tests/fixtures/book_page_1.png")];
551 let options = PdfWriterOptions::default();
552
553 PrintPdfWriter::create_from_images(&images, &output, &options).unwrap();
554
555 assert!(output.exists());
556 }
557
558 #[test]
560 fn test_multiple_images_to_pdf() {
561 let temp_dir = tempdir().unwrap();
562 let output = temp_dir.path().join("output.pdf");
563
564 let images: Vec<_> = (1..=10)
565 .map(|i| PathBuf::from(format!("tests/fixtures/book_page_{}.png", i)))
566 .collect();
567 let options = PdfWriterOptions::default();
568
569 PrintPdfWriter::create_from_images(&images, &output, &options).unwrap();
570
571 let doc = lopdf::Document::load(&output).unwrap();
572 assert_eq!(doc.get_pages().len(), 10);
573 }
574
575 #[test]
577 fn test_metadata_setting() {
578 let temp_dir = tempdir().unwrap();
579 let output = temp_dir.path().join("output.pdf");
580
581 let images = vec![PathBuf::from("tests/fixtures/book_page_1.png")];
582 let options = PdfWriterOptions {
583 metadata: Some(PdfMetadata {
584 title: Some("Test Document".to_string()),
585 author: Some("Test Author".to_string()),
586 ..Default::default()
587 }),
588 ..Default::default()
589 };
590
591 PrintPdfWriter::create_from_images(&images, &output, &options).unwrap();
592
593 assert!(output.exists());
595 }
596
597 #[test]
599 fn test_jpeg_quality() {
600 let temp_dir = tempdir().unwrap();
601 let images = vec![PathBuf::from("tests/fixtures/book_page_1.png")];
602
603 let output_high = temp_dir.path().join("high.pdf");
605 let options_high = PdfWriterOptions {
606 jpeg_quality: 95,
607 ..Default::default()
608 };
609 PrintPdfWriter::create_from_images(&images, &output_high, &options_high).unwrap();
610
611 let output_low = temp_dir.path().join("low.pdf");
613 let options_low = PdfWriterOptions {
614 jpeg_quality: 50,
615 ..Default::default()
616 };
617 PrintPdfWriter::create_from_images(&images, &output_low, &options_low).unwrap();
618
619 assert!(output_high.exists());
621 assert!(output_low.exists());
622 }
623
624 #[test]
625 fn test_builder_pattern() {
626 let options = PdfWriterOptions::builder()
627 .dpi(600)
628 .jpeg_quality(95)
629 .compression(ImageCompression::JpegLossless)
630 .page_size_mode(PageSizeMode::MaxSize)
631 .build();
632
633 assert_eq!(options.dpi, 600);
634 assert_eq!(options.jpeg_quality, 95);
635 assert!(matches!(
636 options.compression,
637 ImageCompression::JpegLossless
638 ));
639 assert!(matches!(options.page_size_mode, PageSizeMode::MaxSize));
640 }
641
642 #[test]
643 fn test_builder_quality_clamping() {
644 let options = PdfWriterOptions::builder().jpeg_quality(150).build();
646 assert_eq!(options.jpeg_quality, 100);
647
648 let options = PdfWriterOptions::builder().jpeg_quality(0).build();
649 assert_eq!(options.jpeg_quality, 1);
650 }
651
652 #[test]
653 fn test_high_quality_preset() {
654 let options = PdfWriterOptions::high_quality();
655
656 assert_eq!(options.dpi, 600);
657 assert_eq!(options.jpeg_quality, 95);
658 assert!(matches!(
659 options.compression,
660 ImageCompression::JpegLossless
661 ));
662 }
663
664 #[test]
665 fn test_compact_preset() {
666 let options = PdfWriterOptions::compact();
667
668 assert_eq!(options.dpi, 150);
669 assert_eq!(options.jpeg_quality, 75);
670 assert!(matches!(options.compression, ImageCompression::Jpeg));
671 }
672
673 #[test]
675 fn test_streaming_generation() {
676 let temp_dir = tempdir().unwrap();
677 let output = temp_dir.path().join("output.pdf");
678
679 let images = (1..=5).map(|i| PathBuf::from(format!("tests/fixtures/book_page_{}.png", i)));
680 let options = PdfWriterOptions::default();
681
682 PrintPdfWriter::create_streaming(images, &output, &options).unwrap();
683
684 assert!(output.exists());
685 let doc = lopdf::Document::load(&output).unwrap();
686 assert_eq!(doc.get_pages().len(), 5);
687 }
688
689 #[test]
691 fn test_ocr_layer_option() {
692 let ocr_layer = OcrLayer {
693 pages: vec![OcrPageText {
694 page_index: 0,
695 blocks: vec![TextBlock {
696 x: 100.0,
697 y: 100.0,
698 width: 200.0,
699 height: 20.0,
700 text: "Test OCR Text".to_string(),
701 font_size: 12.0,
702 vertical: false,
703 }],
704 }],
705 };
706
707 let options = PdfWriterOptions::builder()
708 .ocr_layer(ocr_layer.clone())
709 .build();
710
711 assert!(options.ocr_layer.is_some());
712 assert_eq!(options.ocr_layer.unwrap().pages.len(), 1);
713 }
714
715 #[test]
717 fn test_vertical_ocr_text() {
718 let vertical_block = TextBlock {
719 x: 50.0,
720 y: 100.0,
721 width: 20.0,
722 height: 200.0,
723 text: "縦書きテスト".to_string(),
724 font_size: 14.0,
725 vertical: true,
726 };
727
728 assert!(vertical_block.vertical);
729 assert_eq!(vertical_block.text, "縦書きテスト");
730 }
731
732 #[test]
734 fn test_different_size_images() {
735 let temp_dir = tempdir().unwrap();
736 let output = temp_dir.path().join("output.pdf");
737
738 let images = vec![
740 PathBuf::from("tests/fixtures/book_page_1.png"),
741 PathBuf::from("tests/fixtures/sample_page.png"),
742 ];
743
744 let options = PdfWriterOptions::builder()
746 .page_size_mode(PageSizeMode::Original)
747 .build();
748
749 PrintPdfWriter::create_from_images(&images, &output, &options).unwrap();
750
751 assert!(output.exists());
752 let doc = lopdf::Document::load(&output).unwrap();
753 assert_eq!(doc.get_pages().len(), 2);
754 }
755
756 #[test]
759 fn test_all_compression_types() {
760 let compressions = vec![
761 ImageCompression::Jpeg,
762 ImageCompression::JpegLossless,
763 ImageCompression::Flate,
764 ImageCompression::None,
765 ];
766
767 for comp in compressions {
768 let options = PdfWriterOptions::builder().compression(comp).build();
769 match (comp, options.compression) {
771 (ImageCompression::Jpeg, ImageCompression::Jpeg) => {}
772 (ImageCompression::JpegLossless, ImageCompression::JpegLossless) => {}
773 (ImageCompression::Flate, ImageCompression::Flate) => {}
774 (ImageCompression::None, ImageCompression::None) => {}
775 _ => panic!("Compression mismatch"),
776 }
777 }
778 }
779
780 #[test]
781 fn test_all_page_size_modes() {
782 let options = PdfWriterOptions::builder()
784 .page_size_mode(PageSizeMode::FirstPage)
785 .build();
786 assert!(matches!(options.page_size_mode, PageSizeMode::FirstPage));
787
788 let options = PdfWriterOptions::builder()
790 .page_size_mode(PageSizeMode::MaxSize)
791 .build();
792 assert!(matches!(options.page_size_mode, PageSizeMode::MaxSize));
793
794 let options = PdfWriterOptions::builder()
796 .page_size_mode(PageSizeMode::Original)
797 .build();
798 assert!(matches!(options.page_size_mode, PageSizeMode::Original));
799
800 let options = PdfWriterOptions::builder()
802 .page_size_mode(PageSizeMode::Fixed {
803 width_pt: 612.0,
804 height_pt: 792.0,
805 })
806 .build();
807 if let PageSizeMode::Fixed {
808 width_pt,
809 height_pt,
810 } = options.page_size_mode
811 {
812 assert_eq!(width_pt, 612.0);
813 assert_eq!(height_pt, 792.0);
814 } else {
815 panic!("Expected Fixed page size mode");
816 }
817 }
818
819 #[test]
820 fn test_text_block_construction() {
821 let block = TextBlock {
822 x: 100.0,
823 y: 200.0,
824 width: 300.0,
825 height: 50.0,
826 text: "Sample text".to_string(),
827 font_size: 12.0,
828 vertical: false,
829 };
830
831 assert_eq!(block.x, 100.0);
832 assert_eq!(block.y, 200.0);
833 assert_eq!(block.width, 300.0);
834 assert_eq!(block.height, 50.0);
835 assert_eq!(block.text, "Sample text");
836 assert_eq!(block.font_size, 12.0);
837 assert!(!block.vertical);
838 }
839
840 #[test]
841 fn test_ocr_page_text_construction() {
842 let page_text = OcrPageText {
843 page_index: 5,
844 blocks: vec![TextBlock {
845 x: 0.0,
846 y: 0.0,
847 width: 100.0,
848 height: 20.0,
849 text: "Test".to_string(),
850 font_size: 10.0,
851 vertical: false,
852 }],
853 };
854
855 assert_eq!(page_text.page_index, 5);
856 assert_eq!(page_text.blocks.len(), 1);
857 }
858
859 #[test]
860 fn test_ocr_layer_construction() {
861 let layer = OcrLayer {
862 pages: vec![
863 OcrPageText {
864 page_index: 0,
865 blocks: vec![],
866 },
867 OcrPageText {
868 page_index: 1,
869 blocks: vec![],
870 },
871 ],
872 };
873
874 assert_eq!(layer.pages.len(), 2);
875 }
876
877 #[test]
878 fn test_error_types() {
879 let _err1 = PdfWriterError::NoImages;
881 let _err2 = PdfWriterError::ImageNotFound(PathBuf::from("/test/path"));
882 let _err3 = PdfWriterError::UnsupportedFormat("test".to_string());
883 let _err4 = PdfWriterError::GenerationError("gen error".to_string());
884 let _err5: PdfWriterError =
885 std::io::Error::new(std::io::ErrorKind::NotFound, "test").into();
886 }
887
888 #[test]
889 fn test_print_pdf_writer_default() {
890 let writer = PrintPdfWriter;
891 let _ = writer;
893 }
894
895 #[test]
896 fn test_builder_with_metadata() {
897 let metadata = PdfMetadata {
898 title: Some("Test Doc".to_string()),
899 author: Some("Test Author".to_string()),
900 ..Default::default()
901 };
902
903 let options = PdfWriterOptions::builder().metadata(metadata).build();
904
905 assert!(options.metadata.is_some());
906 let meta = options.metadata.unwrap();
907 assert_eq!(meta.title, Some("Test Doc".to_string()));
908 assert_eq!(meta.author, Some("Test Author".to_string()));
909 }
910
911 #[test]
912 fn test_dpi_setting() {
913 let options = PdfWriterOptions::builder().dpi(600).build();
914 assert_eq!(options.dpi, 600);
915
916 let options = PdfWriterOptions::builder().dpi(150).build();
917 assert_eq!(options.dpi, 150);
918 }
919
920 #[test]
922 fn test_ocr_layer_with_multiple_blocks() {
923 let layer = OcrLayer {
924 pages: vec![OcrPageText {
925 page_index: 0,
926 blocks: vec![
927 TextBlock {
928 x: 100.0,
929 y: 700.0,
930 width: 400.0,
931 height: 20.0,
932 text: "First line".to_string(),
933 font_size: 12.0,
934 vertical: false,
935 },
936 TextBlock {
937 x: 100.0,
938 y: 680.0,
939 width: 300.0,
940 height: 20.0,
941 text: "Second line".to_string(),
942 font_size: 12.0,
943 vertical: false,
944 },
945 ],
946 }],
947 };
948
949 let options = PdfWriterOptions::builder().ocr_layer(layer).build();
950
951 assert!(options.ocr_layer.is_some());
952 let ocr = options.ocr_layer.unwrap();
953 assert_eq!(ocr.pages.len(), 1);
954 assert_eq!(ocr.pages[0].blocks.len(), 2);
955 }
956
957 #[test]
959 fn test_vertical_text_block() {
960 let vertical_block = TextBlock {
961 x: 500.0,
962 y: 100.0,
963 width: 20.0,
964 height: 600.0,
965 text: "縦書きテスト".to_string(),
966 font_size: 12.0,
967 vertical: true,
968 };
969
970 assert!(vertical_block.vertical);
971 assert!(vertical_block.height > vertical_block.width);
972
973 let horizontal_block = TextBlock {
974 x: 100.0,
975 y: 700.0,
976 width: 400.0,
977 height: 20.0,
978 text: "横書きテスト".to_string(),
979 font_size: 12.0,
980 vertical: false,
981 };
982
983 assert!(!horizontal_block.vertical);
984 assert!(horizontal_block.width > horizontal_block.height);
985 }
986
987 #[test]
989 fn test_page_size_modes() {
990 let modes = [
991 PageSizeMode::FirstPage,
992 PageSizeMode::MaxSize,
993 PageSizeMode::Original,
994 PageSizeMode::Fixed {
995 width_pt: 595.0,
996 height_pt: 842.0,
997 },
998 ];
999
1000 for mode in modes {
1001 let options = PdfWriterOptions::builder().page_size_mode(mode).build();
1002 match (mode, options.page_size_mode) {
1004 (PageSizeMode::FirstPage, PageSizeMode::FirstPage) => {}
1005 (PageSizeMode::MaxSize, PageSizeMode::MaxSize) => {}
1006 (PageSizeMode::Original, PageSizeMode::Original) => {}
1007 (
1008 PageSizeMode::Fixed {
1009 width_pt: w1,
1010 height_pt: h1,
1011 },
1012 PageSizeMode::Fixed {
1013 width_pt: w2,
1014 height_pt: h2,
1015 },
1016 ) => {
1017 assert_eq!(w1, w2);
1018 assert_eq!(h1, h2);
1019 }
1020 _ => panic!("Page size mode mismatch"),
1021 }
1022 }
1023 }
1024
1025 #[test]
1026 fn test_compression_types_in_builder() {
1027 let compressions = [
1028 ImageCompression::Jpeg,
1029 ImageCompression::JpegLossless,
1030 ImageCompression::Flate,
1031 ImageCompression::None,
1032 ];
1033
1034 for compression in compressions {
1035 let options = PdfWriterOptions::builder().compression(compression).build();
1036 match (compression, options.compression) {
1037 (ImageCompression::Jpeg, ImageCompression::Jpeg) => {}
1038 (ImageCompression::JpegLossless, ImageCompression::JpegLossless) => {}
1039 (ImageCompression::Flate, ImageCompression::Flate) => {}
1040 (ImageCompression::None, ImageCompression::None) => {}
1041 _ => panic!("Compression type mismatch"),
1042 }
1043 }
1044 }
1045
1046 #[test]
1047 fn test_metadata_all_fields() {
1048 let metadata = PdfMetadata {
1049 title: Some("Test Title".to_string()),
1050 author: Some("Test Author".to_string()),
1051 subject: Some("Test Subject".to_string()),
1052 keywords: Some("keyword1, keyword2".to_string()),
1053 creator: Some("Test Creator".to_string()),
1054 producer: Some("superbook-pdf".to_string()),
1055 creation_date: Some("2024-01-01".to_string()),
1056 modification_date: Some("2024-01-02".to_string()),
1057 };
1058
1059 assert_eq!(metadata.title, Some("Test Title".to_string()));
1060 assert_eq!(metadata.author, Some("Test Author".to_string()));
1061 assert_eq!(metadata.subject, Some("Test Subject".to_string()));
1062 assert!(metadata.keywords.is_some());
1063 assert!(metadata.keywords.as_ref().unwrap().contains("keyword1"));
1064 assert_eq!(metadata.creator, Some("Test Creator".to_string()));
1065 assert_eq!(metadata.producer, Some("superbook-pdf".to_string()));
1066 assert!(metadata.creation_date.is_some());
1067 assert!(metadata.modification_date.is_some());
1068 }
1069
1070 #[test]
1071 fn test_error_display_messages() {
1072 let err = PdfWriterError::NoImages;
1073 assert!(!err.to_string().is_empty());
1074
1075 let err = PdfWriterError::ImageNotFound(PathBuf::from("/path/to/image.png"));
1076 assert!(err.to_string().contains("/path/to/image.png"));
1077
1078 let err = PdfWriterError::UnsupportedFormat("BMP".to_string());
1079 assert!(err.to_string().contains("BMP"));
1080
1081 let err = PdfWriterError::GenerationError("PDF creation failed".to_string());
1082 assert!(err.to_string().contains("PDF creation failed"));
1083 }
1084
1085 #[test]
1086 fn test_builder_chaining() {
1087 let options = PdfWriterOptions::builder()
1088 .dpi(300)
1089 .jpeg_quality(90)
1090 .compression(ImageCompression::Jpeg)
1091 .page_size_mode(PageSizeMode::FirstPage)
1092 .build();
1093
1094 assert_eq!(options.dpi, 300);
1095 assert_eq!(options.jpeg_quality, 90);
1096 assert!(matches!(options.compression, ImageCompression::Jpeg));
1097 assert!(matches!(options.page_size_mode, PageSizeMode::FirstPage));
1098 }
1099
1100 #[test]
1103 fn test_dpi_boundary_values() {
1104 let options_low = PdfWriterOptions::builder().dpi(72).build();
1106 assert_eq!(options_low.dpi, 72);
1107
1108 let options_std = PdfWriterOptions::builder().dpi(300).build();
1110 assert_eq!(options_std.dpi, 300);
1111
1112 let options_high = PdfWriterOptions::builder().dpi(1200).build();
1114 assert_eq!(options_high.dpi, 1200);
1115
1116 let options_very_high = PdfWriterOptions::builder().dpi(2400).build();
1118 assert_eq!(options_very_high.dpi, 2400);
1119 }
1120
1121 #[test]
1122 fn test_jpeg_quality_boundary_values() {
1123 let options_min = PdfWriterOptions::builder().jpeg_quality(1).build();
1125 assert_eq!(options_min.jpeg_quality, 1);
1126
1127 let options_max = PdfWriterOptions::builder().jpeg_quality(100).build();
1129 assert_eq!(options_max.jpeg_quality, 100);
1130
1131 for quality in [25, 50, 75, 85, 95] {
1133 let opts = PdfWriterOptions::builder().jpeg_quality(quality).build();
1134 assert_eq!(opts.jpeg_quality, quality);
1135 }
1136 }
1137
1138 #[test]
1139 fn test_text_block_various_dimensions() {
1140 let small = TextBlock {
1142 x: 0.0,
1143 y: 0.0,
1144 width: 10.0,
1145 height: 5.0,
1146 text: "X".to_string(),
1147 font_size: 8.0,
1148 vertical: false,
1149 };
1150 assert!(small.width > 0.0);
1151 assert!(small.height > 0.0);
1152
1153 let full_width = TextBlock {
1155 x: 0.0,
1156 y: 700.0,
1157 width: 595.0, height: 14.0,
1159 text: "Full width text line".to_string(),
1160 font_size: 12.0,
1161 vertical: false,
1162 };
1163 assert_eq!(full_width.width, 595.0);
1164
1165 let tall_vertical = TextBlock {
1167 x: 500.0,
1168 y: 50.0,
1169 width: 14.0,
1170 height: 700.0,
1171 text: "縦書き長文".to_string(),
1172 font_size: 12.0,
1173 vertical: true,
1174 };
1175 assert!(tall_vertical.height > tall_vertical.width);
1176 assert!(tall_vertical.vertical);
1177 }
1178
1179 #[test]
1180 fn test_ocr_layer_many_pages() {
1181 let pages: Vec<OcrPageText> = (0..100)
1182 .map(|i| OcrPageText {
1183 page_index: i,
1184 blocks: vec![TextBlock {
1185 x: 100.0,
1186 y: 700.0,
1187 width: 400.0,
1188 height: 20.0,
1189 text: format!("Page {} text", i + 1),
1190 font_size: 12.0,
1191 vertical: false,
1192 }],
1193 })
1194 .collect();
1195
1196 let layer = OcrLayer { pages };
1197 assert_eq!(layer.pages.len(), 100);
1198 assert_eq!(layer.pages[0].page_index, 0);
1199 assert_eq!(layer.pages[99].page_index, 99);
1200 }
1201
1202 #[test]
1203 fn test_ocr_page_text_many_blocks() {
1204 let blocks: Vec<TextBlock> = (0..50)
1205 .map(|i| TextBlock {
1206 x: 50.0,
1207 y: 800.0 - (i as f64 * 15.0),
1208 width: 500.0,
1209 height: 12.0,
1210 text: format!("Line {}", i + 1),
1211 font_size: 10.0,
1212 vertical: false,
1213 })
1214 .collect();
1215
1216 let page = OcrPageText {
1217 page_index: 0,
1218 blocks,
1219 };
1220
1221 assert_eq!(page.blocks.len(), 50);
1222 }
1223
1224 #[test]
1225 fn test_error_from_io_conversion() {
1226 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
1227 let pdf_err: PdfWriterError = io_err.into();
1228 let msg = pdf_err.to_string();
1229 assert!(msg.contains("access denied") || msg.contains("IO error"));
1230 }
1231
1232 #[test]
1233 fn test_options_clone() {
1234 let original = PdfWriterOptions::builder()
1235 .dpi(400)
1236 .jpeg_quality(88)
1237 .compression(ImageCompression::Flate)
1238 .build();
1239
1240 let cloned = original.clone();
1241 assert_eq!(cloned.dpi, original.dpi);
1242 assert_eq!(cloned.jpeg_quality, original.jpeg_quality);
1243 }
1244
1245 #[test]
1246 fn test_text_block_clone() {
1247 let original = TextBlock {
1248 x: 100.0,
1249 y: 200.0,
1250 width: 300.0,
1251 height: 50.0,
1252 text: "Clone test".to_string(),
1253 font_size: 14.0,
1254 vertical: true,
1255 };
1256
1257 let cloned = original.clone();
1258 assert_eq!(cloned.x, original.x);
1259 assert_eq!(cloned.y, original.y);
1260 assert_eq!(cloned.text, original.text);
1261 assert_eq!(cloned.vertical, original.vertical);
1262 }
1263
1264 #[test]
1265 fn test_ocr_layer_clone() {
1266 let original = OcrLayer {
1267 pages: vec![OcrPageText {
1268 page_index: 0,
1269 blocks: vec![],
1270 }],
1271 };
1272
1273 let cloned = original.clone();
1274 assert_eq!(cloned.pages.len(), original.pages.len());
1275 }
1276
1277 #[test]
1278 fn test_options_debug_impl() {
1279 let options = PdfWriterOptions::builder()
1280 .dpi(300)
1281 .jpeg_quality(90)
1282 .build();
1283
1284 let debug_str = format!("{:?}", options);
1285 assert!(debug_str.contains("PdfWriterOptions"));
1286 assert!(debug_str.contains("300"));
1287 }
1288
1289 #[test]
1290 fn test_compression_debug_impl() {
1291 let comp = ImageCompression::Flate;
1292 let debug_str = format!("{:?}", comp);
1293 assert!(debug_str.contains("Flate"));
1294 }
1295
1296 #[test]
1297 fn test_page_size_mode_debug_impl() {
1298 let mode = PageSizeMode::Fixed {
1299 width_pt: 595.0,
1300 height_pt: 842.0,
1301 };
1302 let debug_str = format!("{:?}", mode);
1303 assert!(debug_str.contains("Fixed"));
1304 assert!(debug_str.contains("595"));
1305 }
1306
1307 #[test]
1308 fn test_compression_default() {
1309 let comp: ImageCompression = Default::default();
1310 assert!(matches!(comp, ImageCompression::Jpeg));
1311 }
1312
1313 #[test]
1314 fn test_page_size_mode_default() {
1315 let mode: PageSizeMode = Default::default();
1316 assert!(matches!(mode, PageSizeMode::FirstPage));
1317 }
1318
1319 #[test]
1320 fn test_fixed_page_size_various_standards() {
1321 let a4 = PageSizeMode::Fixed {
1323 width_pt: 595.0,
1324 height_pt: 842.0,
1325 };
1326 if let PageSizeMode::Fixed {
1327 width_pt,
1328 height_pt,
1329 } = a4
1330 {
1331 assert_eq!(width_pt, 595.0);
1332 assert_eq!(height_pt, 842.0);
1333 }
1334
1335 let letter = PageSizeMode::Fixed {
1337 width_pt: 612.0,
1338 height_pt: 792.0,
1339 };
1340 if let PageSizeMode::Fixed {
1341 width_pt,
1342 height_pt,
1343 } = letter
1344 {
1345 assert_eq!(width_pt, 612.0);
1346 assert_eq!(height_pt, 792.0);
1347 }
1348
1349 let legal = PageSizeMode::Fixed {
1351 width_pt: 612.0,
1352 height_pt: 1008.0,
1353 };
1354 if let PageSizeMode::Fixed {
1355 width_pt,
1356 height_pt,
1357 } = legal
1358 {
1359 assert_eq!(width_pt, 612.0);
1360 assert_eq!(height_pt, 1008.0);
1361 }
1362 }
1363
1364 #[test]
1365 fn test_metadata_default() {
1366 let meta = PdfMetadata::default();
1367 assert!(meta.title.is_none());
1368 assert!(meta.author.is_none());
1369 assert!(meta.subject.is_none());
1370 }
1371
1372 #[test]
1373 fn test_metadata_partial_fill() {
1374 let meta = PdfMetadata {
1375 title: Some("Only Title".to_string()),
1376 ..Default::default()
1377 };
1378
1379 assert!(meta.title.is_some());
1380 assert!(meta.author.is_none());
1381 assert!(meta.subject.is_none());
1382 }
1383
1384 #[test]
1385 fn test_multiple_images_pdf_verify_pages() {
1386 let temp_dir = tempdir().unwrap();
1387 let output = temp_dir.path().join("multi.pdf");
1388
1389 let images: Vec<_> = (1..=3)
1390 .map(|i| PathBuf::from(format!("tests/fixtures/book_page_{}.png", i)))
1391 .collect();
1392
1393 PrintPdfWriter::create_from_images(&images, &output, &PdfWriterOptions::default()).unwrap();
1394
1395 let doc = lopdf::Document::load(&output).unwrap();
1396 assert_eq!(doc.get_pages().len(), 3);
1397 }
1398
1399 #[test]
1400 fn test_text_block_unicode() {
1401 let unicode_block = TextBlock {
1402 x: 100.0,
1403 y: 700.0,
1404 width: 400.0,
1405 height: 20.0,
1406 text: "日本語テキスト 中文 한글 🎉".to_string(),
1407 font_size: 12.0,
1408 vertical: false,
1409 };
1410
1411 assert!(unicode_block.text.contains("日本語"));
1412 assert!(unicode_block.text.contains("中文"));
1413 assert!(unicode_block.text.contains("한글"));
1414 }
1415
1416 #[test]
1417 fn test_text_block_empty_text() {
1418 let empty_block = TextBlock {
1419 x: 100.0,
1420 y: 700.0,
1421 width: 400.0,
1422 height: 20.0,
1423 text: "".to_string(),
1424 font_size: 12.0,
1425 vertical: false,
1426 };
1427
1428 assert!(empty_block.text.is_empty());
1429 }
1430
1431 #[test]
1432 fn test_font_size_variations() {
1433 let sizes = [6.0, 8.0, 10.0, 12.0, 14.0, 18.0, 24.0, 36.0, 72.0];
1434
1435 for size in sizes {
1436 let block = TextBlock {
1437 x: 0.0,
1438 y: 0.0,
1439 width: 100.0,
1440 height: size * 1.2,
1441 text: "Test".to_string(),
1442 font_size: size,
1443 vertical: false,
1444 };
1445 assert_eq!(block.font_size, size);
1446 }
1447 }
1448
1449 #[test]
1450 fn test_builder_full_chain_with_all_options() {
1451 let metadata = PdfMetadata {
1452 title: Some("Full Test".to_string()),
1453 author: Some("Tester".to_string()),
1454 ..Default::default()
1455 };
1456
1457 let ocr_layer = OcrLayer { pages: vec![] };
1458
1459 let options = PdfWriterOptions::builder()
1460 .dpi(600)
1461 .jpeg_quality(95)
1462 .compression(ImageCompression::JpegLossless)
1463 .page_size_mode(PageSizeMode::MaxSize)
1464 .metadata(metadata)
1465 .ocr_layer(ocr_layer)
1466 .build();
1467
1468 assert_eq!(options.dpi, 600);
1469 assert_eq!(options.jpeg_quality, 95);
1470 assert!(options.metadata.is_some());
1471 assert!(options.ocr_layer.is_some());
1472 }
1473
1474 #[test]
1475 fn test_error_variants_display() {
1476 let errors = [
1477 PdfWriterError::NoImages,
1478 PdfWriterError::ImageNotFound(PathBuf::from("/test.png")),
1479 PdfWriterError::UnsupportedFormat("RAW".to_string()),
1480 PdfWriterError::GenerationError("Failed".to_string()),
1481 ];
1482
1483 for err in &errors {
1484 let display = format!("{}", err);
1485 assert!(!display.is_empty());
1486 }
1487 }
1488
1489 #[test]
1494 fn test_error_no_images_display() {
1495 let err = PdfWriterError::NoImages;
1496 let msg = format!("{}", err);
1497 assert!(msg.contains("No images provided"));
1498 }
1499
1500 #[test]
1501 fn test_error_no_images_debug() {
1502 let err = PdfWriterError::NoImages;
1503 let debug = format!("{:?}", err);
1504 assert!(debug.contains("NoImages"));
1505 }
1506
1507 #[test]
1508 fn test_error_image_not_found_display() {
1509 let path = PathBuf::from("/test/missing_image.png");
1510 let err = PdfWriterError::ImageNotFound(path);
1511 let msg = format!("{}", err);
1512 assert!(msg.contains("Image not found"));
1513 assert!(msg.contains("missing_image.png"));
1514 }
1515
1516 #[test]
1517 fn test_error_image_not_found_debug() {
1518 let path = PathBuf::from("/test/missing.png");
1519 let err = PdfWriterError::ImageNotFound(path);
1520 let debug = format!("{:?}", err);
1521 assert!(debug.contains("ImageNotFound"));
1522 }
1523
1524 #[test]
1525 fn test_error_unsupported_format_display() {
1526 let err = PdfWriterError::UnsupportedFormat("HEIC".to_string());
1527 let msg = format!("{}", err);
1528 assert!(msg.contains("Unsupported image format"));
1529 assert!(msg.contains("HEIC"));
1530 }
1531
1532 #[test]
1533 fn test_error_unsupported_format_debug() {
1534 let err = PdfWriterError::UnsupportedFormat("WEBP".to_string());
1535 let debug = format!("{:?}", err);
1536 assert!(debug.contains("UnsupportedFormat"));
1537 }
1538
1539 #[test]
1540 fn test_error_io_error_display() {
1541 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
1542 let err = PdfWriterError::IoError(io_err);
1543 let msg = format!("{}", err);
1544 assert!(msg.contains("IO error"));
1545 }
1546
1547 #[test]
1548 fn test_error_io_error_debug() {
1549 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "output not found");
1550 let err = PdfWriterError::IoError(io_err);
1551 let debug = format!("{:?}", err);
1552 assert!(debug.contains("IoError"));
1553 }
1554
1555 #[test]
1556 fn test_error_from_io_error() {
1557 let io_err = std::io::Error::new(std::io::ErrorKind::WriteZero, "disk full");
1558 let writer_err: PdfWriterError = io_err.into();
1559 let msg = format!("{}", writer_err);
1560 assert!(msg.contains("IO error"));
1561 }
1562
1563 #[test]
1564 fn test_error_generation_error_display() {
1565 let err = PdfWriterError::GenerationError("font embedding failed".to_string());
1566 let msg = format!("{}", err);
1567 assert!(msg.contains("PDF generation error"));
1568 assert!(msg.contains("font embedding failed"));
1569 }
1570
1571 #[test]
1572 fn test_error_generation_error_debug() {
1573 let err = PdfWriterError::GenerationError("image compression failed".to_string());
1574 let debug = format!("{:?}", err);
1575 assert!(debug.contains("GenerationError"));
1576 }
1577
1578 #[test]
1579 fn test_error_all_variants_debug_impl() {
1580 let errors: Vec<PdfWriterError> = vec![
1581 PdfWriterError::NoImages,
1582 PdfWriterError::ImageNotFound(PathBuf::from("/test.png")),
1583 PdfWriterError::UnsupportedFormat("SVG".to_string()),
1584 PdfWriterError::IoError(std::io::Error::other("io")),
1585 PdfWriterError::GenerationError("gen fail".to_string()),
1586 ];
1587
1588 for err in &errors {
1589 let debug = format!("{:?}", err);
1590 assert!(!debug.is_empty());
1591 let display = format!("{}", err);
1592 assert!(!display.is_empty());
1593 }
1594 }
1595
1596 #[test]
1597 fn test_error_unsupported_format_empty() {
1598 let err = PdfWriterError::UnsupportedFormat(String::new());
1599 let msg = format!("{}", err);
1600 assert!(msg.contains("Unsupported image format"));
1601 }
1602
1603 #[test]
1604 fn test_error_generation_error_with_details() {
1605 let err = PdfWriterError::GenerationError("page 5: overflow at 0x1234".to_string());
1606 let msg = format!("{}", err);
1607 assert!(msg.contains("page 5"));
1608 assert!(msg.contains("0x1234"));
1609 }
1610
1611 #[test]
1614 fn test_pdf_writer_types_send_sync() {
1615 fn assert_send_sync<T: Send + Sync>() {}
1616 assert_send_sync::<PdfWriterOptions>();
1617 assert_send_sync::<ImageCompression>();
1618 assert_send_sync::<PageSizeMode>();
1619 assert_send_sync::<OcrLayer>();
1620 assert_send_sync::<OcrPageText>();
1621 assert_send_sync::<TextBlock>();
1622 }
1623
1624 #[test]
1625 fn test_concurrent_options_building() {
1626 use std::thread;
1627 let handles: Vec<_> = (0..8)
1628 .map(|i| {
1629 thread::spawn(move || {
1630 PdfWriterOptions::builder()
1631 .dpi(300 + (i as u32 * 100))
1632 .jpeg_quality(80 + (i as u8 * 2))
1633 .compression(if i % 2 == 0 {
1634 ImageCompression::Jpeg
1635 } else {
1636 ImageCompression::Flate
1637 })
1638 .build()
1639 })
1640 })
1641 .collect();
1642
1643 let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
1644 assert_eq!(results.len(), 8);
1645 for (i, opt) in results.iter().enumerate() {
1646 assert_eq!(opt.dpi, 300 + (i as u32 * 100));
1647 }
1648 }
1649
1650 #[test]
1651 fn test_parallel_text_block_creation() {
1652 use rayon::prelude::*;
1653
1654 let blocks: Vec<_> = (0..100)
1655 .into_par_iter()
1656 .map(|i| TextBlock {
1657 x: i as f64 * 10.0,
1658 y: i as f64 * 20.0,
1659 width: 100.0,
1660 height: 20.0,
1661 text: format!("Text block {}", i),
1662 font_size: 12.0,
1663 vertical: i % 2 == 0,
1664 })
1665 .collect();
1666
1667 assert_eq!(blocks.len(), 100);
1668 for (i, block) in blocks.iter().enumerate() {
1669 assert_eq!(block.x, i as f64 * 10.0);
1670 assert!(block.text.contains(&i.to_string()));
1671 }
1672 }
1673
1674 #[test]
1675 fn test_ocr_layer_thread_transfer() {
1676 use std::thread;
1677
1678 let layer = OcrLayer {
1679 pages: vec![OcrPageText {
1680 page_index: 0,
1681 blocks: vec![TextBlock {
1682 x: 0.0,
1683 y: 0.0,
1684 width: 100.0,
1685 height: 20.0,
1686 text: "Test".to_string(),
1687 font_size: 12.0,
1688 vertical: false,
1689 }],
1690 }],
1691 };
1692
1693 let handle = thread::spawn(move || {
1694 assert_eq!(layer.pages.len(), 1);
1695 assert_eq!(layer.pages[0].blocks.len(), 1);
1696 layer.pages[0].blocks[0].text.clone()
1697 });
1698
1699 let result = handle.join().unwrap();
1700 assert_eq!(result, "Test");
1701 }
1702
1703 #[test]
1704 fn test_options_shared_across_threads() {
1705 use std::sync::Arc;
1706 use std::thread;
1707
1708 let options = Arc::new(
1709 PdfWriterOptions::builder()
1710 .dpi(600)
1711 .jpeg_quality(95)
1712 .build(),
1713 );
1714
1715 let handles: Vec<_> = (0..4)
1716 .map(|_| {
1717 let opts = Arc::clone(&options);
1718 thread::spawn(move || {
1719 assert_eq!(opts.dpi, 600);
1720 assert_eq!(opts.jpeg_quality, 95);
1721 opts.dpi
1722 })
1723 })
1724 .collect();
1725
1726 for handle in handles {
1727 assert_eq!(handle.join().unwrap(), 600);
1728 }
1729 }
1730
1731 #[test]
1734 fn test_dpi_boundary_minimum() {
1735 let opts = PdfWriterOptions::builder().dpi(1).build();
1736 assert_eq!(opts.dpi, 1);
1737 }
1738
1739 #[test]
1740 fn test_dpi_boundary_maximum() {
1741 let opts = PdfWriterOptions::builder().dpi(2400).build();
1742 assert_eq!(opts.dpi, 2400);
1743 }
1744
1745 #[test]
1746 fn test_jpeg_quality_clamped_to_min() {
1747 let opts = PdfWriterOptions::builder().jpeg_quality(0).build();
1748 assert_eq!(opts.jpeg_quality, MIN_JPEG_QUALITY); }
1750
1751 #[test]
1752 fn test_jpeg_quality_clamped_to_max() {
1753 let opts = PdfWriterOptions::builder().jpeg_quality(255).build();
1754 assert_eq!(opts.jpeg_quality, MAX_JPEG_QUALITY); }
1756
1757 #[test]
1758 fn test_text_block_zero_dimensions() {
1759 let block = TextBlock {
1760 x: 0.0,
1761 y: 0.0,
1762 width: 0.0,
1763 height: 0.0,
1764 text: String::new(),
1765 font_size: 0.0,
1766 vertical: false,
1767 };
1768 assert_eq!(block.width, 0.0);
1769 assert_eq!(block.height, 0.0);
1770 }
1771
1772 #[test]
1773 fn test_text_block_large_dimensions() {
1774 let block = TextBlock {
1775 x: 10000.0,
1776 y: 10000.0,
1777 width: 5000.0,
1778 height: 1000.0,
1779 text: "Large page".to_string(),
1780 font_size: 144.0,
1781 vertical: true,
1782 };
1783 assert_eq!(block.width, 5000.0);
1784 assert!(block.vertical);
1785 }
1786
1787 #[test]
1788 fn test_font_size_boundary_small() {
1789 let block = TextBlock {
1790 x: 0.0,
1791 y: 0.0,
1792 width: 100.0,
1793 height: 10.0,
1794 text: "Tiny".to_string(),
1795 font_size: 1.0,
1796 vertical: false,
1797 };
1798 assert_eq!(block.font_size, 1.0);
1799 }
1800
1801 #[test]
1802 fn test_font_size_boundary_large() {
1803 let block = TextBlock {
1804 x: 0.0,
1805 y: 0.0,
1806 width: 1000.0,
1807 height: 500.0,
1808 text: "HUGE".to_string(),
1809 font_size: 500.0,
1810 vertical: false,
1811 };
1812 assert_eq!(block.font_size, 500.0);
1813 }
1814
1815 #[test]
1816 fn test_fixed_page_size_zero() {
1817 let mode = PageSizeMode::Fixed {
1818 width_pt: 0.0,
1819 height_pt: 0.0,
1820 };
1821 if let PageSizeMode::Fixed {
1822 width_pt,
1823 height_pt,
1824 } = mode
1825 {
1826 assert_eq!(width_pt, 0.0);
1827 assert_eq!(height_pt, 0.0);
1828 }
1829 }
1830
1831 #[test]
1832 fn test_fixed_page_size_a4() {
1833 let mode = PageSizeMode::Fixed {
1835 width_pt: 595.28,
1836 height_pt: 841.89,
1837 };
1838 if let PageSizeMode::Fixed {
1839 width_pt,
1840 height_pt,
1841 } = mode
1842 {
1843 assert!((width_pt - 595.28).abs() < 0.01);
1844 assert!((height_pt - 841.89).abs() < 0.01);
1845 }
1846 }
1847
1848 #[test]
1849 fn test_ocr_page_text_empty_blocks() {
1850 let page = OcrPageText {
1851 page_index: 0,
1852 blocks: vec![],
1853 };
1854 assert!(page.blocks.is_empty());
1855 }
1856
1857 #[test]
1858 fn test_ocr_layer_many_pages_boundary() {
1859 let pages: Vec<OcrPageText> = (0..1000)
1860 .map(|i| OcrPageText {
1861 page_index: i,
1862 blocks: vec![],
1863 })
1864 .collect();
1865
1866 let layer = OcrLayer { pages };
1867 assert_eq!(layer.pages.len(), 1000);
1868 }
1869}