Skip to main content

superbook_pdf/
pdf_writer.rs

1//! PDF Writer module
2//!
3//! Provides functionality to create PDF files from images.
4//!
5//! # Features
6//!
7//! - Create PDFs from image files (PNG, JPEG, etc.)
8//! - Configurable DPI and JPEG quality
9//! - Optional OCR text layer for searchable PDFs
10//! - Metadata embedding
11//! - Multiple page size modes
12//!
13//! # Example
14//!
15//! ```rust,no_run
16//! use superbook_pdf::{PdfWriterOptions, PrintPdfWriter};
17//!
18//! let options = PdfWriterOptions::builder()
19//!     .dpi(300)
20//!     .jpeg_quality(90)
21//!     .build();
22//!
23//! let images = vec!["page1.png".into(), "page2.png".into()];
24//! PrintPdfWriter::create_from_images(&images, std::path::Path::new("output.pdf"), &options).unwrap();
25//! ```
26
27use crate::pdf_reader::PdfMetadata;
28use std::fs::File;
29use std::io::BufWriter;
30use std::path::{Path, PathBuf};
31use thiserror::Error;
32
33// ============================================================
34// Constants
35// ============================================================
36
37/// Conversion factor: 1 point = 0.352_778 mm (1/72 inch ≈ 25.4/72)
38const POINTS_TO_MM: f32 = 0.352_778;
39
40/// Millimeters per inch
41const MM_PER_INCH: f32 = 25.4;
42
43/// Points per inch (PDF standard)
44const POINTS_PER_INCH: f32 = 72.0;
45
46/// Default DPI for standard quality
47const DEFAULT_DPI: u32 = 300;
48
49/// High quality DPI for archival output
50const HIGH_QUALITY_DPI: u32 = 600;
51
52/// Compact DPI for smaller file sizes
53const COMPACT_DPI: u32 = 150;
54
55/// Default JPEG quality
56const DEFAULT_JPEG_QUALITY: u8 = 90;
57
58/// High quality JPEG setting
59const HIGH_QUALITY_JPEG: u8 = 95;
60
61/// Compact JPEG quality for smaller files
62const COMPACT_JPEG_QUALITY: u8 = 75;
63
64/// Minimum JPEG quality
65const MIN_JPEG_QUALITY: u8 = 1;
66
67/// Maximum JPEG quality
68const MAX_JPEG_QUALITY: u8 = 100;
69
70/// PDF writing error types
71#[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/// PDF generation options
92#[derive(Debug, Clone)]
93pub struct PdfWriterOptions {
94    /// Output DPI
95    pub dpi: u32,
96    /// JPEG quality (1-100)
97    pub jpeg_quality: u8,
98    /// Image compression method
99    pub compression: ImageCompression,
100    /// Page size unification mode
101    pub page_size_mode: PageSizeMode,
102    /// PDF metadata
103    pub metadata: Option<PdfMetadata>,
104    /// OCR text layer
105    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    /// Create a new options builder
123    pub fn builder() -> PdfWriterOptionsBuilder {
124        PdfWriterOptionsBuilder::default()
125    }
126
127    /// Create options optimized for high quality output
128    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    /// Create options optimized for smaller file size
138    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/// Builder for PdfWriterOptions
149#[derive(Debug, Default)]
150pub struct PdfWriterOptionsBuilder {
151    options: PdfWriterOptions,
152}
153
154impl PdfWriterOptionsBuilder {
155    /// Set output DPI
156    #[must_use]
157    pub fn dpi(mut self, dpi: u32) -> Self {
158        self.options.dpi = dpi;
159        self
160    }
161
162    /// Set JPEG quality (1-100)
163    #[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    /// Set compression method
170    #[must_use]
171    pub fn compression(mut self, compression: ImageCompression) -> Self {
172        self.options.compression = compression;
173        self
174    }
175
176    /// Set page size mode
177    #[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    /// Set metadata
184    #[must_use]
185    pub fn metadata(mut self, metadata: PdfMetadata) -> Self {
186        self.options.metadata = Some(metadata);
187        self
188    }
189
190    /// Set OCR layer
191    #[must_use]
192    pub fn ocr_layer(mut self, layer: OcrLayer) -> Self {
193        self.options.ocr_layer = Some(layer);
194        self
195    }
196
197    /// Build the options
198    #[must_use]
199    pub fn build(self) -> PdfWriterOptions {
200        self.options
201    }
202}
203
204/// Image compression methods
205#[derive(Debug, Clone, Copy, Default)]
206pub enum ImageCompression {
207    #[default]
208    Jpeg,
209    JpegLossless,
210    Flate,
211    None,
212}
213
214/// Page size unification modes
215#[derive(Debug, Clone, Copy, Default)]
216pub enum PageSizeMode {
217    /// Match first page size
218    #[default]
219    FirstPage,
220    /// Use maximum dimensions
221    MaxSize,
222    /// Fixed size in points
223    Fixed { width_pt: f64, height_pt: f64 },
224    /// Keep original size for each page
225    Original,
226}
227
228/// OCR text layer
229#[derive(Debug, Clone)]
230pub struct OcrLayer {
231    pub pages: Vec<OcrPageText>,
232}
233
234/// OCR text for a single page
235#[derive(Debug, Clone)]
236pub struct OcrPageText {
237    pub page_index: usize,
238    pub blocks: Vec<TextBlock>,
239}
240
241/// Text block with position
242#[derive(Debug, Clone)]
243pub struct TextBlock {
244    /// X coordinate in points (left-bottom origin)
245    pub x: f64,
246    /// Y coordinate in points
247    pub y: f64,
248    /// Width in points
249    pub width: f64,
250    /// Height in points
251    pub height: f64,
252    /// Text content
253    pub text: String,
254    /// Font size in points
255    pub font_size: f64,
256    /// Vertical text flag
257    pub vertical: bool,
258}
259
260/// PDF Writer trait
261pub trait PdfWriter {
262    /// Create PDF from images
263    fn create_from_images(
264        images: &[PathBuf],
265        output: &Path,
266        options: &PdfWriterOptions,
267    ) -> Result<()>;
268
269    /// Create PDF using streaming (memory efficient)
270    fn create_streaming(
271        images: impl Iterator<Item = PathBuf>,
272        output: &Path,
273        options: &PdfWriterOptions,
274    ) -> Result<()>;
275}
276
277/// printpdf-based PDF writer implementation
278pub struct PrintPdfWriter;
279
280impl PrintPdfWriter {
281    /// Create a new PDF writer
282    pub fn new() -> Self {
283        Self
284    }
285
286    /// Create PDF from images
287    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        // Validate all images exist
297        for img_path in images {
298            if !img_path.exists() {
299                return Err(PdfWriterError::ImageNotFound(img_path.clone()));
300            }
301        }
302
303        // Load first image to determine initial page size
304        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        // Convert pixels to millimeters for printpdf
311        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        // Create PDF document
315        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        // Add first image to first page
329        Self::add_image_to_layer(&doc, page1, layer1, &first_img, width_mm, height_mm)?;
330
331        // Add OCR text layer for first page if available
332        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        // Add remaining images
340        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                    // Convert points to pixels at specified DPI
354                    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            // Add OCR text layer if available
368            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        // Save PDF
377        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    /// Add image to a PDF layer
386    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        // Convert image to RGB8 format
397        let rgb_img = img.to_rgb8();
398        let (img_width, img_height) = rgb_img.dimensions();
399
400        // Create printpdf Image from raw RGB data
401        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        // Get layer reference
416        let layer_ref = doc.get_page(page).get_layer(layer);
417
418        // Calculate transform to fit image to page
419        // printpdf uses points (72 per inch) for scaling
420        // We need to convert from pixels to points, then scale to fit the page
421        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        // Target size in points (mm to points: mm * 72 / 25.4)
426        let width_pt = width_mm * 72.0 / MM_PER_INCH;
427        let height_pt = height_mm * 72.0 / MM_PER_INCH;
428
429        // Scale factors to fit image to page
430        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    /// Add OCR text layer to a PDF page
448    ///
449    /// The text is rendered as invisible (searchable) text over the image.
450    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        // Load built-in font for OCR text
459        // Using a CJK font for Japanese text support
460        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            // Convert points to mm
466            let x_mm = block.x as f32 * POINTS_TO_MM;
467            // PDF coordinate system has origin at bottom-left, so flip y
468            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            // Set text rendering mode to invisible (mode 3)
474            // This makes text searchable but not visible
475            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    /// Create PDF from image iterator (streaming mode for memory efficiency)
484    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    // TC-PDW-003: 空の画像リストエラー
506    #[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    // TC-PDW-004: 存在しない画像エラー
517    #[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    // Image fixture tests
543
544    // TC-PDW-001: 単一画像からPDF生成
545    #[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    // TC-PDW-002: 複数画像からPDF生成
559    #[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    // TC-PDW-005: メタデータ設定
576    #[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        // Metadata verification would require reading back the PDF
594        assert!(output.exists());
595    }
596
597    // TC-PDW-006: JPEG品質設定
598    #[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        // High quality
604        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        // Low quality
612        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        // Both should exist (size comparison may not work with placeholder implementation)
620        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        // Quality should be clamped to 1-100
645        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    // TC-PDW-007: Streaming generation (memory efficient)
674    #[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    // TC-PDW-008: OCR layer embedding
690    #[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    // TC-PDW-009: Vertical OCR support
716    #[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    // TC-PDW-010: Different size images processing
733    #[test]
734    fn test_different_size_images() {
735        let temp_dir = tempdir().unwrap();
736        let output = temp_dir.path().join("output.pdf");
737
738        // Use different sized images from fixtures
739        let images = vec![
740            PathBuf::from("tests/fixtures/book_page_1.png"),
741            PathBuf::from("tests/fixtures/sample_page.png"),
742        ];
743
744        // Test with PageSizeMode::Original to preserve different sizes
745        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    // Additional structure tests
757
758    #[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            // Verify compression is set
770            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        // Test FirstPage mode
783        let options = PdfWriterOptions::builder()
784            .page_size_mode(PageSizeMode::FirstPage)
785            .build();
786        assert!(matches!(options.page_size_mode, PageSizeMode::FirstPage));
787
788        // Test MaxSize mode
789        let options = PdfWriterOptions::builder()
790            .page_size_mode(PageSizeMode::MaxSize)
791            .build();
792        assert!(matches!(options.page_size_mode, PageSizeMode::MaxSize));
793
794        // Test Original mode
795        let options = PdfWriterOptions::builder()
796            .page_size_mode(PageSizeMode::Original)
797            .build();
798        assert!(matches!(options.page_size_mode, PageSizeMode::Original));
799
800        // Test Fixed mode
801        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        // Test all error variants can be constructed
880        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        // Just verify it can be constructed
892        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    // TC-PDW-008: OCRレイヤー埋め込みテスト拡張
921    #[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    // TC-PDW-009: 縦書きOCR対応テスト
958    #[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    // TC-PDW-010: 異なるサイズの画像処理テスト
988    #[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            // Verify mode is set correctly
1003            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    // Additional comprehensive tests
1101
1102    #[test]
1103    fn test_dpi_boundary_values() {
1104        // Low DPI
1105        let options_low = PdfWriterOptions::builder().dpi(72).build();
1106        assert_eq!(options_low.dpi, 72);
1107
1108        // Standard DPI
1109        let options_std = PdfWriterOptions::builder().dpi(300).build();
1110        assert_eq!(options_std.dpi, 300);
1111
1112        // High DPI
1113        let options_high = PdfWriterOptions::builder().dpi(1200).build();
1114        assert_eq!(options_high.dpi, 1200);
1115
1116        // Very high DPI
1117        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        // Minimum (clamped to 1)
1124        let options_min = PdfWriterOptions::builder().jpeg_quality(1).build();
1125        assert_eq!(options_min.jpeg_quality, 1);
1126
1127        // Maximum
1128        let options_max = PdfWriterOptions::builder().jpeg_quality(100).build();
1129        assert_eq!(options_max.jpeg_quality, 100);
1130
1131        // Typical values
1132        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        // Small block
1141        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        // Full page width block
1154        let full_width = TextBlock {
1155            x: 0.0,
1156            y: 700.0,
1157            width: 595.0, // A4 width in points
1158            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        // Tall vertical block
1166        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        // A4 (595 x 842 pt)
1322        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        // US Letter (612 x 792 pt)
1336        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        // US Legal (612 x 1008 pt)
1350        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    // ============================================================
1490    // Additional Error handling tests
1491    // ============================================================
1492
1493    #[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    // ============ Concurrency Tests ============
1612
1613    #[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    // ============ Boundary Value Tests ============
1732
1733    #[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); // Should clamp to 1
1749    }
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); // Should clamp to 100
1755    }
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        // A4 in points: 595.28 x 841.89
1834        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}