Skip to main content

superbook_pdf/
lib.rs

1//! superbook-pdf - High-quality PDF converter for scanned books
2//!
3//! A complete Rust implementation for converting scanned book PDFs into
4//! high-quality digital books with AI enhancement.
5//!
6//! # Features
7//!
8//! - **PDF Reading** ([`pdf_reader`]) - Extract metadata, pages, and images from PDFs
9//! - **PDF Writing** ([`pdf_writer`]) - Generate PDFs from images with optional OCR layer
10//! - **Image Extraction** ([`image_extract`]) - Extract page images using `ImageMagick`
11//! - **AI Enhancement** ([`realesrgan`]) - Upscale images using `RealESRGAN`
12//! - **Deskew Correction** ([`deskew`]) - Detect and correct page skew
13//! - **Margin Detection** ([`margin`]) - Detect and trim page margins
14//! - **Page Number Detection** ([`page_number`]) - OCR-based page number recognition
15//! - **AI Bridge** ([`ai_bridge`]) - Python subprocess bridge for AI tools
16//! - **`YomiToku` OCR** ([`yomitoku`]) - Japanese AI-OCR for searchable PDFs
17//!
18//! # Quick Start
19//!
20//! ## Reading a PDF
21//!
22//! ```rust,no_run
23//! use superbook_pdf::{LopdfReader, PdfWriterOptions, PrintPdfWriter};
24//!
25//! // Read a PDF
26//! let reader = LopdfReader::new("input.pdf").unwrap();
27//! println!("Pages: {}", reader.info.page_count);
28//! ```
29//!
30//! ## Using Builder Patterns
31//!
32//! All option structs support fluent builder patterns:
33//!
34//! ```rust
35//! use superbook_pdf::{PdfWriterOptions, DeskewOptions, RealEsrganOptions};
36//!
37//! // PDF Writer options
38//! let pdf_opts = PdfWriterOptions::builder()
39//!     .dpi(600)
40//!     .jpeg_quality(95)
41//!     .build();
42//!
43//! // Or use presets
44//! let high_quality = PdfWriterOptions::high_quality();
45//! let compact = PdfWriterOptions::compact();
46//!
47//! // Deskew options
48//! let deskew_opts = DeskewOptions::builder()
49//!     .max_angle(15.0)
50//!     .build();
51//!
52//! // RealESRGAN options
53//! let upscale_opts = RealEsrganOptions::builder()
54//!     .scale(4)
55//!     .tile_size(256)
56//!     .build();
57//! ```
58//!
59//! # Architecture
60//!
61//! The library is organized into independent modules that can be used separately:
62//!
63//! ```text
64//! PDF Input -> Image Extraction -> Deskew -> Margin Detection
65//!                                    |
66//!                            AI Upscaling (RealESRGAN)
67//!                                    |
68//!                         Page Number Detection -> OCR -> PDF Output
69//! ```
70//!
71//! # Error Handling
72//!
73//! Each module has its own error type that can be matched for specific handling:
74//!
75//! - [`PdfReaderError`] - PDF reading errors
76//! - [`PdfWriterError`] - PDF writing errors
77//! - [`ExtractError`] - Image extraction errors
78//! - [`DeskewError`] - Deskew processing errors
79//! - [`MarginError`] - Margin detection errors
80//! - [`PageNumberError`] - Page number detection errors
81//! - [`AiBridgeError`] - AI tool communication errors
82//! - [`RealEsrganError`] - `RealESRGAN` upscaling errors
83//! - [`YomiTokuError`] - `YomiToku` OCR errors
84//!
85//! # CLI Exit Codes
86//!
87//! Use [`ExitCode`] for type-safe exit code handling:
88//!
89//! ```rust
90//! use superbook_pdf::ExitCode;
91//!
92//! let code = ExitCode::Success;
93//! assert_eq!(code.code(), 0);
94//! assert_eq!(code.description(), "Success");
95//! ```
96//!
97//! # Error Handling Example
98//!
99//! ```rust
100//! use superbook_pdf::{PdfReaderError, MarginError, DeskewError};
101//! use std::path::PathBuf;
102//!
103//! fn handle_pdf_error(err: PdfReaderError) -> String {
104//!     match err {
105//!         PdfReaderError::FileNotFound(path) => format!("File not found: {}", path.display()),
106//!         PdfReaderError::InvalidFormat(msg) => format!("Invalid PDF: {}", msg),
107//!         PdfReaderError::EncryptedPdf => "Encrypted PDFs are not supported".to_string(),
108//!         _ => format!("Other error: {}", err),
109//!     }
110//! }
111//!
112//! let err = PdfReaderError::FileNotFound(PathBuf::from("/test.pdf"));
113//! assert!(handle_pdf_error(err).contains("/test.pdf"));
114//! ```
115//!
116//! # License
117//!
118//! AGPL-3.0
119
120pub mod ai_bridge;
121pub mod cache;
122pub mod cli;
123pub mod config;
124pub mod color_stats;
125pub mod deskew;
126pub mod finalize;
127pub mod image_extract;
128pub mod margin;
129pub mod normalize;
130pub mod page_number;
131pub mod parallel;
132pub mod pdf_reader;
133pub mod pipeline;
134pub mod progress;
135pub mod pdf_writer;
136pub mod realesrgan;
137pub mod reprocess;
138pub mod util;
139pub mod vertical_detect;
140#[cfg(feature = "web")]
141pub mod web;
142pub mod yomitoku;
143
144// Re-exports for convenience
145pub use ai_bridge::{
146    AiBridgeConfig, AiBridgeConfigBuilder, AiBridgeError, AiTool, SubprocessBridge,
147};
148pub use cli::{
149    create_page_progress_bar, create_progress_bar, create_spinner, CacheInfoArgs, Cli, Commands,
150    ConvertArgs, ExitCode, ReprocessArgs,
151};
152#[cfg(feature = "web")]
153pub use cli::ServeArgs;
154pub use config::{
155    AdvancedConfig, CliOverrides, Config, ConfigError, GeneralConfig, OcrConfig, OutputConfig,
156    ProcessingConfig,
157};
158pub use deskew::{
159    DeskewAlgorithm, DeskewError, DeskewOptions, DeskewOptionsBuilder, DeskewResult,
160    ImageProcDeskewer, QualityMode, SkewDetection,
161};
162pub use image_extract::{
163    ColorSpace, ExtractError, ExtractOptions, ExtractOptionsBuilder, ExtractedPage, ImageFormat,
164    LopdfExtractor, MagickExtractor,
165};
166pub use margin::{
167    ContentDetectionMode, ContentRect, GroupCropAnalyzer, GroupCropRegion, ImageMarginDetector,
168    MarginDetection, MarginError, MarginOptions, MarginOptionsBuilder, Margins, PageBoundingBox,
169    TrimResult, UnifiedCropRegions, UnifiedMargins,
170};
171pub use page_number::{
172    calc_group_reference_position, calc_overlap_center, find_page_number_with_fallback,
173    find_page_numbers_batch, BookOffsetAnalysis, DetectedPageNumber, FallbackMatchStats,
174    MatchStage, OffsetCorrection, PageNumberAnalysis, PageNumberCandidate, PageNumberError,
175    PageNumberMatch, PageNumberOptions, PageNumberOptionsBuilder, PageNumberPosition,
176    PageNumberRect, PageOffsetAnalyzer, PageOffsetResult, Point, Rectangle, TesseractPageDetector,
177};
178pub use pdf_reader::{LopdfReader, PdfDocument, PdfMetadata, PdfPage, PdfReaderError};
179pub use pdf_writer::{PdfWriterError, PdfWriterOptions, PdfWriterOptionsBuilder, PrintPdfWriter};
180pub use realesrgan::{RealEsrgan, RealEsrganError, RealEsrganOptions, RealEsrganOptionsBuilder};
181pub use reprocess::{
182    PageStatus, ReprocessError, ReprocessOptions, ReprocessResult, ReprocessState,
183};
184pub use util::{
185    clamp, ensure_dir_writable, ensure_file_exists, format_duration, format_file_size, load_image,
186    mm_to_pixels, mm_to_points, percentage, pixels_to_mm, points_to_mm,
187};
188pub use yomitoku::{
189    BatchOcrResult, OcrResult, TextBlock, TextDirection, YomiToku, YomiTokuError, YomiTokuOptions,
190    YomiTokuOptionsBuilder,
191};
192
193// Phase 1-6: Advanced processing modules
194pub use color_stats::{ColorAnalyzer, ColorStats, ColorStatsError, GlobalColorParam};
195pub use finalize::{
196    FinalizeError, FinalizeOptions, FinalizeOptionsBuilder, FinalizeResult, PageFinalizer,
197};
198pub use normalize::{
199    ImageNormalizer, NormalizeError, NormalizeOptions, NormalizeOptionsBuilder, NormalizeResult,
200    PaddingMode, PaperColor, Resampler,
201};
202pub use vertical_detect::{
203    detect_book_vertical_writing, detect_vertical_probability, BookVerticalResult,
204    VerticalDetectError, VerticalDetectOptions, VerticalDetectResult,
205};
206pub use parallel::{
207    parallel_map, parallel_process, ParallelError, ParallelOptions, ParallelProcessor,
208    ParallelResult,
209};
210pub use progress::{build_progress_bar, OutputMode, ProcessingStage, ProgressTracker};
211pub use cache::{
212    should_skip_processing, CacheDigest, ProcessingCache, ProcessingResult, CACHE_EXTENSION,
213    CACHE_VERSION,
214};
215pub use pipeline::{
216    calculate_optimal_chunk_size, process_in_chunks, PdfPipeline, PipelineConfig, PipelineError,
217    PipelineResult, ProcessingContext, ProgressCallback, SilentProgress,
218};
219
220// Web server (optional feature)
221#[cfg(feature = "web")]
222pub use web::{
223    generate_preview_base64, preview_stage, ApiKey, AuthConfig, AuthError, AuthManager,
224    AuthResult, AuthStatusResponse, BatchJob, BatchProgress, BatchQueue, BatchStatistics,
225    BatchStatus, ConvertOptions as WebConvertOptions, CorsConfig, HistoryQuery, HistoryResponse,
226    Job, JobQueue, JobStatistics, JobStatus, JobStore, JsonJobStore, MetricsCollector,
227    PersistenceConfig, Priority, Progress as WebProgress, RateLimitConfig, RateLimitError,
228    RateLimitResult, RateLimiter, RateLimitStatus, RecoveryManager, RecoveryResult, RetryResponse,
229    Scope, ServerConfig, ServerInfo, ShutdownConfig, ShutdownCoordinator, ShutdownResult,
230    ShutdownSignal, StatsResponse, StorageBackend, StoreError, SystemMetrics, WebServer,
231    WsBroadcaster, WsMessage, extract_api_key, graceful_shutdown, wait_for_shutdown_signal,
232    PREVIEW_WIDTH,
233};
234
235/// Exit codes for CLI (deprecated: prefer using `ExitCode` enum)
236///
237/// These constants are provided for backward compatibility.
238/// The `ExitCode` enum provides a more type-safe alternative.
239pub mod exit_codes {
240    use super::ExitCode;
241
242    pub const SUCCESS: i32 = ExitCode::Success as i32;
243    pub const GENERAL_ERROR: i32 = ExitCode::GeneralError as i32;
244    pub const INVALID_ARGS: i32 = ExitCode::InvalidArgs as i32;
245    pub const INPUT_NOT_FOUND: i32 = ExitCode::InputNotFound as i32;
246    pub const OUTPUT_ERROR: i32 = ExitCode::OutputError as i32;
247    pub const PROCESSING_ERROR: i32 = ExitCode::ProcessingError as i32;
248    pub const GPU_ERROR: i32 = ExitCode::GpuError as i32;
249    pub const EXTERNAL_TOOL_ERROR: i32 = ExitCode::ExternalToolError as i32;
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use std::path::PathBuf;
256
257    // ============ Re-export Verification Tests ============
258
259    #[test]
260    fn test_all_public_types_accessible() {
261        // Verify all public types are accessible via re-exports
262
263        // PDF Reader types
264        let _reader_err: Option<PdfReaderError> = None;
265        let _doc: Option<PdfDocument> = None;
266        let _meta: PdfMetadata = Default::default();
267        let _page: Option<PdfPage> = None;
268
269        // PDF Writer types
270        let _writer_err: Option<PdfWriterError> = None;
271        let _opts = PdfWriterOptions::default();
272        let _builder = PdfWriterOptions::builder();
273
274        // Image Extract types
275        let _ext_err: Option<ExtractError> = None;
276        let _ext_opts = ExtractOptions::default();
277        let _format = ImageFormat::Png;
278        let _colorspace = ColorSpace::Rgb;
279
280        // Deskew types
281        let _dsk_err: Option<DeskewError> = None;
282        let _dsk_opts = DeskewOptions::default();
283        let _algo = DeskewAlgorithm::HoughLines;
284        let _quality = QualityMode::Standard;
285
286        // Margin types
287        let _mrg_err: Option<MarginError> = None;
288        let _mrg_opts = MarginOptions::default();
289        let _detection_mode = ContentDetectionMode::BackgroundColor;
290        let _margins = Margins::default();
291
292        // Page Number types
293        let _pgn_err: Option<PageNumberError> = None;
294        let _pgn_opts = PageNumberOptions::default();
295        let _position = PageNumberPosition::BottomCenter;
296
297        // AI Bridge types
298        let _ai_err: Option<AiBridgeError> = None;
299        let _ai_config = AiBridgeConfig::default();
300
301        // RealESRGAN types
302        let _res_err: Option<RealEsrganError> = None;
303        let _res_opts = RealEsrganOptions::default();
304
305        // YomiToku types
306        let _yomi_err: Option<YomiTokuError> = None;
307        let _yomi_opts = YomiTokuOptions::default();
308        let _direction = TextDirection::Horizontal;
309
310        // CLI types
311        let code = ExitCode::Success;
312        assert_eq!(code.code(), 0);
313    }
314
315    #[test]
316    fn test_exit_codes_module() {
317        assert_eq!(exit_codes::SUCCESS, 0);
318        assert_ne!(exit_codes::GENERAL_ERROR, 0);
319        assert_ne!(exit_codes::INVALID_ARGS, 0);
320        assert_ne!(exit_codes::INPUT_NOT_FOUND, 0);
321        assert_ne!(exit_codes::OUTPUT_ERROR, 0);
322        assert_ne!(exit_codes::PROCESSING_ERROR, 0);
323        assert_ne!(exit_codes::GPU_ERROR, 0);
324        assert_ne!(exit_codes::EXTERNAL_TOOL_ERROR, 0);
325    }
326
327    #[test]
328    fn test_exit_codes_match_enum() {
329        assert_eq!(exit_codes::SUCCESS, ExitCode::Success.code());
330        assert_eq!(exit_codes::GENERAL_ERROR, ExitCode::GeneralError.code());
331        assert_eq!(exit_codes::INVALID_ARGS, ExitCode::InvalidArgs.code());
332        assert_eq!(exit_codes::INPUT_NOT_FOUND, ExitCode::InputNotFound.code());
333        assert_eq!(exit_codes::OUTPUT_ERROR, ExitCode::OutputError.code());
334        assert_eq!(
335            exit_codes::PROCESSING_ERROR,
336            ExitCode::ProcessingError.code()
337        );
338        assert_eq!(exit_codes::GPU_ERROR, ExitCode::GpuError.code());
339        assert_eq!(
340            exit_codes::EXTERNAL_TOOL_ERROR,
341            ExitCode::ExternalToolError.code()
342        );
343    }
344
345    // ============ Builder Pattern Consistency Tests ============
346
347    #[test]
348    fn test_all_builders_follow_same_pattern() {
349        // All builders should work with build() method
350        let _pdf = PdfWriterOptions::builder().dpi(300).build();
351        let _dsk = DeskewOptions::builder().max_angle(10.0).build();
352        let _ext = ExtractOptions::builder().dpi(300).build();
353        let _mrg = MarginOptions::builder().min_margin(5).build();
354        let _pgn = PageNumberOptions::builder().min_confidence(70.0).build();
355        let _res = RealEsrganOptions::builder().scale(2).build();
356        let _yomi = YomiTokuOptions::builder()
357            .language(yomitoku::Language::Japanese)
358            .build();
359        let _ai = AiBridgeConfig::builder().max_retries(3).build();
360    }
361
362    #[test]
363    fn test_all_defaults_are_valid() {
364        // All default options should be usable
365        let pdf = PdfWriterOptions::default();
366        assert!(pdf.dpi > 0);
367        assert!(pdf.jpeg_quality > 0 && pdf.jpeg_quality <= 100);
368
369        let dsk = DeskewOptions::default();
370        assert!(dsk.max_angle > 0.0);
371        assert!(dsk.threshold_angle >= 0.0);
372
373        let ext = ExtractOptions::default();
374        assert!(ext.dpi > 0);
375
376        let mrg = MarginOptions::default();
377        assert!(mrg.background_threshold > 0);
378
379        let pgn = PageNumberOptions::default();
380        assert!(pgn.min_confidence > 0.0);
381        assert!(!pgn.ocr_language.is_empty());
382
383        let res = RealEsrganOptions::default();
384        assert!(res.scale > 0);
385
386        let yomi = YomiTokuOptions::default();
387        // Language enum has a default value (Japanese)
388        assert!(matches!(yomi.language, yomitoku::Language::Japanese));
389    }
390
391    // ============ Preset Consistency Tests ============
392
393    #[test]
394    fn test_pdf_writer_presets() {
395        let high = PdfWriterOptions::high_quality();
396        let compact = PdfWriterOptions::compact();
397
398        // High quality should have higher DPI and quality
399        assert!(high.dpi >= compact.dpi);
400        assert!(high.jpeg_quality >= compact.jpeg_quality);
401    }
402
403    #[test]
404    fn test_deskew_presets() {
405        let high = DeskewOptions::high_quality();
406        let fast = DeskewOptions::fast();
407
408        // High quality should use better interpolation
409        assert!(matches!(high.quality_mode, QualityMode::HighQuality));
410        assert!(matches!(fast.quality_mode, QualityMode::Fast));
411    }
412
413    #[test]
414    fn test_page_number_presets() {
415        let jpn = PageNumberOptions::japanese();
416        let eng = PageNumberOptions::english();
417        let strict = PageNumberOptions::strict();
418
419        assert_eq!(jpn.ocr_language, "jpn");
420        assert_eq!(eng.ocr_language, "eng");
421        assert!(strict.min_confidence > PageNumberOptions::default().min_confidence);
422    }
423
424    #[test]
425    fn test_margin_presets() {
426        let dark = MarginOptions::for_dark_background();
427        let precise = MarginOptions::precise();
428
429        // Dark background should have lower threshold
430        assert!(dark.background_threshold < MarginOptions::default().background_threshold);
431        // Precise should use combined detection
432        assert!(matches!(
433            precise.detection_mode,
434            ContentDetectionMode::Combined
435        ));
436    }
437
438    #[test]
439    fn test_ai_bridge_presets() {
440        let cpu = AiBridgeConfig::cpu_only();
441        let low_vram = AiBridgeConfig::low_vram();
442
443        // CPU only should have GPU disabled
444        assert!(!cpu.gpu_config.enabled);
445        // Low VRAM should have memory limits
446        assert!(low_vram.gpu_config.max_vram_mb.is_some());
447    }
448
449    #[test]
450    fn test_realesrgan_presets() {
451        let quality = RealEsrganOptions::x4_high_quality();
452        let anime = RealEsrganOptions::anime();
453
454        // High quality should be 4x
455        assert_eq!(quality.scale, 4);
456        // Anime preset should have anime model
457        assert!(matches!(
458            anime.model,
459            realesrgan::RealEsrganModel::X4PlusAnime
460        ));
461    }
462
463    // ============ Util Function Tests ============
464
465    #[test]
466    fn test_clamp_function() {
467        assert_eq!(clamp(5, 0, 10), 5);
468        assert_eq!(clamp(-5, 0, 10), 0);
469        assert_eq!(clamp(15, 0, 10), 10);
470        assert_eq!(clamp(0, 0, 10), 0);
471        assert_eq!(clamp(10, 0, 10), 10);
472    }
473
474    #[test]
475    fn test_format_file_size() {
476        assert!(format_file_size(500).contains("B"));
477        assert!(format_file_size(1024).contains("K") || format_file_size(1024).contains("1"));
478        assert!(
479            format_file_size(1024 * 1024).contains("M")
480                || format_file_size(1024 * 1024).contains("1")
481        );
482    }
483
484    #[test]
485    fn test_format_duration() {
486        let duration = std::time::Duration::from_secs(65);
487        let formatted = format_duration(duration);
488        assert!(formatted.contains("1") || formatted.contains("min") || formatted.contains(":"));
489    }
490
491    #[test]
492    fn test_mm_points_conversion() {
493        // 1 inch = 72 points = 25.4 mm
494        let mm = 25.4;
495        let points = mm_to_points(mm);
496        assert!((points - 72.0).abs() < 0.1);
497
498        let back_to_mm = points_to_mm(points);
499        assert!((back_to_mm - mm).abs() < 0.1);
500    }
501
502    #[test]
503    fn test_mm_pixels_conversion() {
504        // At 300 DPI: 25.4mm = 1 inch = 300 pixels
505        let mm = 25.4;
506        let dpi = 300;
507        let pixels = mm_to_pixels(mm, dpi);
508        assert!((pixels as i32 - 300).abs() < 2);
509
510        let back_to_mm = pixels_to_mm(pixels, dpi);
511        assert!((back_to_mm - mm).abs() < 1.0);
512    }
513
514    #[test]
515    fn test_percentage_function() {
516        assert_eq!(percentage(50, 100), 50.0);
517        assert_eq!(percentage(25, 100), 25.0);
518        assert_eq!(percentage(100, 100), 100.0);
519        assert_eq!(percentage(0, 100), 0.0);
520    }
521
522    #[test]
523    fn test_ensure_file_exists_nonexistent() {
524        let result = ensure_file_exists(PathBuf::from("/nonexistent/file.pdf"));
525        assert!(result.is_err());
526    }
527
528    #[test]
529    fn test_ensure_dir_writable_nonexistent() {
530        let result = ensure_dir_writable(PathBuf::from("/nonexistent/directory"));
531        assert!(result.is_err());
532    }
533
534    // ============ Error Type Conversion Tests ============
535
536    #[test]
537    fn test_io_error_conversions() {
538        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "test");
539
540        // All error types should be convertible from io::Error
541        let _pdf_err: PdfReaderError = io_err.into();
542
543        let io_err2 = std::io::Error::new(std::io::ErrorKind::NotFound, "test");
544        let _writer_err: PdfWriterError = io_err2.into();
545
546        let io_err3 = std::io::Error::new(std::io::ErrorKind::NotFound, "test");
547        let _ext_err: ExtractError = io_err3.into();
548
549        let io_err4 = std::io::Error::new(std::io::ErrorKind::NotFound, "test");
550        let _dsk_err: DeskewError = io_err4.into();
551
552        let io_err5 = std::io::Error::new(std::io::ErrorKind::NotFound, "test");
553        let _mrg_err: MarginError = io_err5.into();
554
555        let io_err6 = std::io::Error::new(std::io::ErrorKind::NotFound, "test");
556        let _pgn_err: PageNumberError = io_err6.into();
557    }
558
559    // ============ Cross-Module Integration Tests ============
560
561    #[test]
562    fn test_margins_with_content_rect() {
563        let margins = Margins {
564            top: 50,
565            bottom: 50,
566            left: 30,
567            right: 30,
568        };
569
570        let content = ContentRect {
571            x: margins.left,
572            y: margins.top,
573            width: 1000 - margins.total_horizontal(),
574            height: 1500 - margins.total_vertical(),
575        };
576
577        assert_eq!(content.x, 30);
578        assert_eq!(content.y, 50);
579        assert_eq!(content.width, 940);
580        assert_eq!(content.height, 1400);
581    }
582
583    #[test]
584    fn test_page_metadata_from_reader_to_writer() {
585        let metadata = PdfMetadata {
586            title: Some("Test Book".to_string()),
587            author: Some("Test Author".to_string()),
588            subject: Some("Test Subject".to_string()),
589            keywords: Some("rust, pdf, test".to_string()),
590            creator: Some("superbook-pdf".to_string()),
591            producer: Some("superbook-pdf".to_string()),
592            creation_date: None,
593            modification_date: None,
594        };
595
596        // Create writer options with metadata
597        let opts = PdfWriterOptions::builder()
598            .metadata(metadata.clone())
599            .build();
600
601        assert!(opts.metadata.is_some());
602        let writer_meta = opts.metadata.unwrap();
603        assert_eq!(writer_meta.title, metadata.title);
604        assert_eq!(writer_meta.author, metadata.author);
605    }
606
607    #[test]
608    fn test_exit_code_covers_all_error_types() {
609        // Each error type should map to an appropriate exit code
610
611        // File not found errors -> INPUT_NOT_FOUND
612        let input_not_found = ExitCode::InputNotFound;
613        assert_ne!(input_not_found.code(), 0);
614
615        // Processing errors -> PROCESSING_ERROR
616        let processing_error = ExitCode::ProcessingError;
617        assert_ne!(processing_error.code(), 0);
618
619        // GPU errors -> GPU_ERROR
620        let gpu_error = ExitCode::GpuError;
621        assert_ne!(gpu_error.code(), 0);
622
623        // External tool errors -> EXTERNAL_TOOL_ERROR
624        let tool_error = ExitCode::ExternalToolError;
625        assert_ne!(tool_error.code(), 0);
626    }
627
628    #[test]
629    fn test_image_format_and_colorspace_combinations() {
630        let formats = [
631            ImageFormat::Png,
632            ImageFormat::Jpeg { quality: 90 },
633            ImageFormat::Tiff,
634        ];
635
636        let colorspaces = [ColorSpace::Rgb, ColorSpace::Grayscale, ColorSpace::Cmyk];
637
638        // All combinations should be valid
639        for format in &formats {
640            for colorspace in &colorspaces {
641                let opts = ExtractOptions::builder()
642                    .format(*format)
643                    .colorspace(*colorspace)
644                    .build();
645
646                // Just verify they can be combined
647                let _ = format!("{:?} + {:?}", opts.format, opts.colorspace);
648            }
649        }
650    }
651
652    #[test]
653    fn test_text_direction_with_text_block() {
654        let horizontal = TextBlock {
655            text: "Horizontal text".to_string(),
656            bbox: (0, 0, 200, 20),
657            confidence: 0.95,
658            direction: TextDirection::Horizontal,
659            font_size: Some(12.0),
660        };
661
662        let vertical = TextBlock {
663            text: "縦書きテキスト".to_string(),
664            bbox: (0, 0, 20, 200),
665            confidence: 0.95,
666            direction: TextDirection::Vertical,
667            font_size: Some(12.0),
668        };
669
670        assert!(matches!(horizontal.direction, TextDirection::Horizontal));
671        assert!(matches!(vertical.direction, TextDirection::Vertical));
672
673        // Aspect ratio check
674        let h_width = horizontal.bbox.2 - horizontal.bbox.0;
675        let h_height = horizontal.bbox.3 - horizontal.bbox.1;
676        assert!(h_width > h_height);
677
678        let v_width = vertical.bbox.2 - vertical.bbox.0;
679        let v_height = vertical.bbox.3 - vertical.bbox.1;
680        assert!(v_height > v_width);
681    }
682
683    #[test]
684    fn test_deskew_result_with_detection() {
685        let detection = SkewDetection {
686            angle: 2.5,
687            confidence: 0.92,
688            feature_count: 150,
689        };
690
691        let result = DeskewResult {
692            detection: detection.clone(),
693            corrected: true,
694            output_path: PathBuf::from("/output/corrected.png"),
695            original_size: (2480, 3508),
696            corrected_size: (2500, 3530),
697        };
698
699        assert_eq!(result.detection.angle, 2.5);
700        assert!(result.corrected);
701        // Corrected size may be slightly different due to rotation
702        assert!(result.corrected_size.0 >= result.original_size.0 - 50);
703    }
704
705    #[test]
706    fn test_upscale_result_dimensions() {
707        // When upscaling 2x, dimensions should double
708        let original = (1000u32, 1500u32);
709        let scale = 2u32;
710        let expected = (original.0 * scale, original.1 * scale);
711
712        assert_eq!(expected, (2000, 3000));
713
714        // 4x upscale
715        let scale4 = 4u32;
716        let expected4 = (original.0 * scale4, original.1 * scale4);
717        assert_eq!(expected4, (4000, 6000));
718    }
719
720    #[test]
721    fn test_progress_bar_creation() {
722        let pb = create_progress_bar(100);
723        assert_eq!(pb.length(), Some(100));
724
725        let page_pb = create_page_progress_bar(50);
726        assert_eq!(page_pb.length(), Some(50));
727
728        let spinner = create_spinner("Processing...");
729        assert_eq!(spinner.message(), "Processing...");
730    }
731
732    // ==================== Additional API Tests ====================
733
734    #[test]
735    fn test_all_presets_have_valid_defaults() {
736        // PDF Writer presets
737        let pdf_default = PdfWriterOptions::default();
738        let pdf_high = PdfWriterOptions::high_quality();
739        let pdf_compact = PdfWriterOptions::compact();
740        assert!(pdf_default.dpi > 0);
741        assert!(pdf_high.dpi >= pdf_default.dpi);
742        assert!(pdf_compact.dpi <= pdf_default.dpi);
743
744        // Deskew presets
745        let dsk_default = DeskewOptions::default();
746        let dsk_high = DeskewOptions::high_quality();
747        let dsk_fast = DeskewOptions::fast();
748        assert!(dsk_default.max_angle > 0.0);
749        assert!(dsk_high.max_angle > 0.0);
750        assert!(dsk_fast.max_angle > 0.0);
751
752        // Margin presets
753        let mrg_default = MarginOptions::default();
754        let mrg_dark = MarginOptions::for_dark_background();
755        let mrg_precise = MarginOptions::precise();
756        assert!(mrg_default.background_threshold > 0);
757        assert!(mrg_dark.background_threshold < mrg_default.background_threshold);
758        assert!(mrg_precise.edge_sensitivity > mrg_default.edge_sensitivity);
759
760        // RealESRGAN presets
761        let res_default = RealEsrganOptions::default();
762        let res_x4 = RealEsrganOptions::x4_high_quality();
763        let res_anime = RealEsrganOptions::anime();
764        assert!(res_default.scale > 0);
765        assert_eq!(res_x4.scale, 4);
766        assert!(res_anime.scale > 0);
767    }
768
769    #[test]
770    fn test_builder_chain_immutability() {
771        // Verify builder methods return new instances
772        let builder1 = PdfWriterOptions::builder();
773        let builder2 = builder1.dpi(300);
774        let opts = builder2.build();
775        assert_eq!(opts.dpi, 300);
776
777        // Another chain
778        let opts2 = PdfWriterOptions::builder()
779            .dpi(600)
780            .jpeg_quality(95)
781            .build();
782        assert_eq!(opts2.dpi, 600);
783        assert_eq!(opts2.jpeg_quality, 95);
784    }
785
786    #[test]
787    fn test_error_messages_are_descriptive() {
788        // PDF Reader errors
789        let pdf_err = PdfReaderError::FileNotFound(PathBuf::from("/test.pdf"));
790        let msg = format!("{}", pdf_err);
791        assert!(msg.contains("test.pdf"));
792
793        // Extract errors
794        let ext_err = ExtractError::PdfNotFound(PathBuf::from("/input.pdf"));
795        let msg = format!("{}", ext_err);
796        assert!(msg.contains("input.pdf"));
797
798        // Deskew errors
799        let dsk_err = DeskewError::ImageNotFound(PathBuf::from("/image.png"));
800        let msg = format!("{}", dsk_err);
801        assert!(msg.contains("image.png"));
802    }
803
804    #[test]
805    fn test_exit_code_uniqueness() {
806        // All exit codes should be unique
807        let codes = [
808            ExitCode::Success,
809            ExitCode::GeneralError,
810            ExitCode::InvalidArgs,
811            ExitCode::InputNotFound,
812            ExitCode::OutputError,
813            ExitCode::ProcessingError,
814            ExitCode::GpuError,
815            ExitCode::ExternalToolError,
816        ];
817
818        let code_values: Vec<i32> = codes.iter().map(|c| c.code()).collect();
819        let mut unique_values = code_values.clone();
820        unique_values.sort();
821        unique_values.dedup();
822
823        assert_eq!(
824            code_values.len(),
825            unique_values.len(),
826            "Exit codes must be unique"
827        );
828    }
829
830    #[test]
831    fn test_margins_arithmetic() {
832        let margins = Margins {
833            top: 100,
834            bottom: 150,
835            left: 50,
836            right: 75,
837        };
838
839        assert_eq!(margins.total_horizontal(), 125);
840        assert_eq!(margins.total_vertical(), 250);
841
842        // Test with zero margins
843        let zero_margins = Margins::default();
844        assert_eq!(zero_margins.total_horizontal(), 0);
845        assert_eq!(zero_margins.total_vertical(), 0);
846    }
847
848    #[test]
849    fn test_content_rect_calculations() {
850        let rect = ContentRect {
851            x: 50,
852            y: 100,
853            width: 800,
854            height: 1000,
855        };
856
857        // Verify coordinates are stored correctly
858        assert_eq!(rect.x, 50);
859        assert_eq!(rect.y, 100);
860        assert_eq!(rect.width, 800);
861        assert_eq!(rect.height, 1000);
862
863        // Calculate right and bottom
864        let right = rect.x + rect.width;
865        let bottom = rect.y + rect.height;
866        assert_eq!(right, 850);
867        assert_eq!(bottom, 1100);
868    }
869
870    #[test]
871    fn test_page_number_positions() {
872        let positions = [
873            PageNumberPosition::BottomCenter,
874            PageNumberPosition::BottomOutside,
875            PageNumberPosition::BottomInside,
876            PageNumberPosition::TopCenter,
877            PageNumberPosition::TopOutside,
878        ];
879
880        // All positions should be distinct
881        for (i, pos1) in positions.iter().enumerate() {
882            for (j, pos2) in positions.iter().enumerate() {
883                if i != j {
884                    assert_ne!(std::mem::discriminant(pos1), std::mem::discriminant(pos2));
885                }
886            }
887        }
888    }
889
890    #[test]
891    fn test_yomitoku_language_options() {
892        let jpn = YomiTokuOptions::builder()
893            .language(yomitoku::Language::Japanese)
894            .build();
895        assert!(matches!(jpn.language, yomitoku::Language::Japanese));
896
897        let eng = YomiTokuOptions::builder()
898            .language(yomitoku::Language::English)
899            .build();
900        assert!(matches!(eng.language, yomitoku::Language::English));
901    }
902
903    #[test]
904    fn test_deskew_algorithms() {
905        let algorithms = [
906            DeskewAlgorithm::HoughLines,
907            DeskewAlgorithm::ProjectionProfile,
908            DeskewAlgorithm::TextLineDetection,
909            DeskewAlgorithm::Combined,
910        ];
911
912        // All algorithms should be usable in options
913        for algo in algorithms {
914            let opts = DeskewOptions::builder().algorithm(algo).build();
915            assert!(matches!(
916                opts.algorithm,
917                DeskewAlgorithm::HoughLines
918                    | DeskewAlgorithm::ProjectionProfile
919                    | DeskewAlgorithm::TextLineDetection
920                    | DeskewAlgorithm::Combined
921            ));
922        }
923    }
924
925    // ============ Concurrency Tests for Re-exported Types ============
926
927    #[test]
928    fn test_all_options_types_send_sync() {
929        fn assert_send_sync<T: Send + Sync>() {}
930        assert_send_sync::<PdfWriterOptions>();
931        assert_send_sync::<DeskewOptions>();
932        assert_send_sync::<ExtractOptions>();
933        assert_send_sync::<MarginOptions>();
934        assert_send_sync::<PageNumberOptions>();
935        assert_send_sync::<RealEsrganOptions>();
936        assert_send_sync::<YomiTokuOptions>();
937        assert_send_sync::<AiBridgeConfig>();
938    }
939
940    #[test]
941    fn test_all_error_types_send_sync() {
942        fn assert_send_sync<T: Send + Sync>() {}
943        assert_send_sync::<PdfReaderError>();
944        assert_send_sync::<PdfWriterError>();
945        assert_send_sync::<ExtractError>();
946        assert_send_sync::<DeskewError>();
947        assert_send_sync::<MarginError>();
948        assert_send_sync::<PageNumberError>();
949        assert_send_sync::<RealEsrganError>();
950        assert_send_sync::<YomiTokuError>();
951        assert_send_sync::<AiBridgeError>();
952    }
953
954    #[test]
955    fn test_all_data_types_send_sync() {
956        fn assert_send_sync<T: Send + Sync>() {}
957        assert_send_sync::<PdfDocument>();
958        assert_send_sync::<PdfMetadata>();
959        assert_send_sync::<PdfPage>();
960        assert_send_sync::<Margins>();
961        assert_send_sync::<ContentRect>();
962        assert_send_sync::<TextBlock>();
963        assert_send_sync::<ExitCode>();
964    }
965
966    #[test]
967    fn test_concurrent_options_creation() {
968        use std::thread;
969
970        let handles: Vec<_> = (0..8)
971            .map(|i| {
972                thread::spawn(move || {
973                    let pdf = PdfWriterOptions::builder().dpi(150 + i * 50).build();
974                    let dsk = DeskewOptions::builder().max_angle(5.0 + i as f64).build();
975                    let ext = ExtractOptions::builder().dpi(150 + i * 50).build();
976                    (pdf.dpi, dsk.max_angle, ext.dpi)
977                })
978            })
979            .collect();
980
981        let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
982        assert_eq!(results.len(), 8);
983        for (i, (pdf_dpi, dsk_angle, ext_dpi)) in results.iter().enumerate() {
984            assert_eq!(*pdf_dpi, 150 + (i as u32) * 50);
985            assert!((dsk_angle - (5.0 + i as f64)).abs() < 0.01);
986            assert_eq!(*ext_dpi, 150 + (i as u32) * 50);
987        }
988    }
989
990    #[test]
991    fn test_concurrent_preset_creation() {
992        use rayon::prelude::*;
993
994        let results: Vec<_> = (0..100)
995            .into_par_iter()
996            .map(|i| {
997                let preset = match i % 6 {
998                    0 => PdfWriterOptions::default(),
999                    1 => PdfWriterOptions::high_quality(),
1000                    2 => PdfWriterOptions::compact(),
1001                    3 => PdfWriterOptions::builder()
1002                        .dpi(300)
1003                        .jpeg_quality(90)
1004                        .build(),
1005                    4 => PdfWriterOptions::builder()
1006                        .dpi(600)
1007                        .jpeg_quality(95)
1008                        .build(),
1009                    _ => PdfWriterOptions::builder().dpi(150).build(),
1010                };
1011                preset.dpi
1012            })
1013            .collect();
1014
1015        assert_eq!(results.len(), 100);
1016    }
1017
1018    #[test]
1019    fn test_concurrent_exit_code_handling() {
1020        use std::thread;
1021
1022        let codes = vec![
1023            ExitCode::Success,
1024            ExitCode::GeneralError,
1025            ExitCode::InvalidArgs,
1026            ExitCode::InputNotFound,
1027            ExitCode::OutputError,
1028            ExitCode::ProcessingError,
1029            ExitCode::GpuError,
1030            ExitCode::ExternalToolError,
1031        ];
1032
1033        let handles: Vec<_> = codes
1034            .into_iter()
1035            .map(|code| {
1036                thread::spawn(move || {
1037                    let value = code.code();
1038                    let desc = code.description();
1039                    (value, desc.to_string())
1040                })
1041            })
1042            .collect();
1043
1044        let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
1045        assert_eq!(results.len(), 8);
1046
1047        // Success should be 0
1048        assert!(results.iter().any(|(code, _)| *code == 0));
1049    }
1050
1051    #[test]
1052    fn test_concurrent_margins_operations() {
1053        use rayon::prelude::*;
1054
1055        let results: Vec<_> = (0..100)
1056            .into_par_iter()
1057            .map(|i| {
1058                let margins = Margins {
1059                    top: i as u32 * 10,
1060                    bottom: i as u32 * 10,
1061                    left: i as u32 * 5,
1062                    right: i as u32 * 5,
1063                };
1064                (margins.total_vertical(), margins.total_horizontal())
1065            })
1066            .collect();
1067
1068        assert_eq!(results.len(), 100);
1069        for (i, (vert, horiz)) in results.iter().enumerate() {
1070            assert_eq!(*vert, (i as u32) * 20);
1071            assert_eq!(*horiz, (i as u32) * 10);
1072        }
1073    }
1074
1075    // ============ Additional Integration Tests ============
1076
1077    #[test]
1078    fn test_all_modules_have_consistent_builder_pattern() {
1079        // All builders should support build() without any required parameters
1080        let _pdf = PdfWriterOptions::builder().build();
1081        let _dsk = DeskewOptions::builder().build();
1082        let _ext = ExtractOptions::builder().build();
1083        let _mrg = MarginOptions::builder().build();
1084        let _pgn = PageNumberOptions::builder().build();
1085        let _res = RealEsrganOptions::builder().build();
1086        let _yomi = YomiTokuOptions::builder().build();
1087        let _ai = AiBridgeConfig::builder().build();
1088    }
1089
1090    #[test]
1091    fn test_all_defaults_consistent_with_docs() {
1092        // Verify documented defaults match actual defaults
1093
1094        // DPI defaults should be reasonable (150-600)
1095        let ext = ExtractOptions::default();
1096        assert!(ext.dpi >= 72 && ext.dpi <= 1200);
1097
1098        let pdf = PdfWriterOptions::default();
1099        assert!(pdf.dpi >= 72 && pdf.dpi <= 1200);
1100
1101        // JPEG quality should be valid (1-100)
1102        assert!(pdf.jpeg_quality >= 1 && pdf.jpeg_quality <= 100);
1103
1104        // Deskew max angle should be reasonable (1-45 degrees)
1105        let dsk = DeskewOptions::default();
1106        assert!(dsk.max_angle >= 1.0 && dsk.max_angle <= 45.0);
1107
1108        // Confidence thresholds should be 0-100
1109        let pgn = PageNumberOptions::default();
1110        assert!(pgn.min_confidence >= 0.0 && pgn.min_confidence <= 100.0);
1111    }
1112
1113    #[test]
1114    fn test_colorspace_image_format_all_combinations() {
1115        let formats = [
1116            ImageFormat::Png,
1117            ImageFormat::Jpeg { quality: 90 },
1118            ImageFormat::Tiff,
1119            ImageFormat::Bmp,
1120        ];
1121
1122        let colorspaces = [ColorSpace::Rgb, ColorSpace::Grayscale, ColorSpace::Cmyk];
1123
1124        // All 12 combinations should be valid
1125        let mut count = 0;
1126        for format in &formats {
1127            for colorspace in &colorspaces {
1128                let _ = ExtractOptions::builder()
1129                    .format(*format)
1130                    .colorspace(*colorspace)
1131                    .build();
1132                count += 1;
1133            }
1134        }
1135        assert_eq!(count, 12);
1136    }
1137
1138    #[test]
1139    fn test_unified_margins_from_margins() {
1140        let pages_margins = [
1141            Margins {
1142                top: 50,
1143                bottom: 60,
1144                left: 30,
1145                right: 40,
1146            },
1147            Margins {
1148                top: 55,
1149                bottom: 65,
1150                left: 35,
1151                right: 45,
1152            },
1153            Margins {
1154                top: 45,
1155                bottom: 55,
1156                left: 25,
1157                right: 35,
1158            },
1159        ];
1160
1161        // Find unified margins (minimum of each)
1162        let unified = Margins {
1163            top: pages_margins.iter().map(|m| m.top).min().unwrap(),
1164            bottom: pages_margins.iter().map(|m| m.bottom).min().unwrap(),
1165            left: pages_margins.iter().map(|m| m.left).min().unwrap(),
1166            right: pages_margins.iter().map(|m| m.right).min().unwrap(),
1167        };
1168
1169        assert_eq!(unified.top, 45);
1170        assert_eq!(unified.bottom, 55);
1171        assert_eq!(unified.left, 25);
1172        assert_eq!(unified.right, 35);
1173    }
1174
1175    #[test]
1176    fn test_exit_code_all_descriptions_non_empty() {
1177        let codes = [
1178            ExitCode::Success,
1179            ExitCode::GeneralError,
1180            ExitCode::InvalidArgs,
1181            ExitCode::InputNotFound,
1182            ExitCode::OutputError,
1183            ExitCode::ProcessingError,
1184            ExitCode::GpuError,
1185            ExitCode::ExternalToolError,
1186        ];
1187
1188        for code in codes {
1189            let desc = code.description();
1190            assert!(
1191                !desc.is_empty(),
1192                "Description for {:?} should not be empty",
1193                code
1194            );
1195        }
1196    }
1197}