1pub 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
144pub 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
193pub 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#[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
235pub 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 #[test]
260 fn test_all_public_types_accessible() {
261 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 let _writer_err: Option<PdfWriterError> = None;
271 let _opts = PdfWriterOptions::default();
272 let _builder = PdfWriterOptions::builder();
273
274 let _ext_err: Option<ExtractError> = None;
276 let _ext_opts = ExtractOptions::default();
277 let _format = ImageFormat::Png;
278 let _colorspace = ColorSpace::Rgb;
279
280 let _dsk_err: Option<DeskewError> = None;
282 let _dsk_opts = DeskewOptions::default();
283 let _algo = DeskewAlgorithm::HoughLines;
284 let _quality = QualityMode::Standard;
285
286 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 let _pgn_err: Option<PageNumberError> = None;
294 let _pgn_opts = PageNumberOptions::default();
295 let _position = PageNumberPosition::BottomCenter;
296
297 let _ai_err: Option<AiBridgeError> = None;
299 let _ai_config = AiBridgeConfig::default();
300
301 let _res_err: Option<RealEsrganError> = None;
303 let _res_opts = RealEsrganOptions::default();
304
305 let _yomi_err: Option<YomiTokuError> = None;
307 let _yomi_opts = YomiTokuOptions::default();
308 let _direction = TextDirection::Horizontal;
309
310 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 #[test]
348 fn test_all_builders_follow_same_pattern() {
349 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 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 assert!(matches!(yomi.language, yomitoku::Language::Japanese));
389 }
390
391 #[test]
394 fn test_pdf_writer_presets() {
395 let high = PdfWriterOptions::high_quality();
396 let compact = PdfWriterOptions::compact();
397
398 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 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 assert!(dark.background_threshold < MarginOptions::default().background_threshold);
431 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 assert!(!cpu.gpu_config.enabled);
445 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 assert_eq!(quality.scale, 4);
456 assert!(matches!(
458 anime.model,
459 realesrgan::RealEsrganModel::X4PlusAnime
460 ));
461 }
462
463 #[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 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 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 #[test]
537 fn test_io_error_conversions() {
538 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "test");
539
540 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 #[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 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 let input_not_found = ExitCode::InputNotFound;
613 assert_ne!(input_not_found.code(), 0);
614
615 let processing_error = ExitCode::ProcessingError;
617 assert_ne!(processing_error.code(), 0);
618
619 let gpu_error = ExitCode::GpuError;
621 assert_ne!(gpu_error.code(), 0);
622
623 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 for format in &formats {
640 for colorspace in &colorspaces {
641 let opts = ExtractOptions::builder()
642 .format(*format)
643 .colorspace(*colorspace)
644 .build();
645
646 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 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 assert!(result.corrected_size.0 >= result.original_size.0 - 50);
703 }
704
705 #[test]
706 fn test_upscale_result_dimensions() {
707 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 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 #[test]
735 fn test_all_presets_have_valid_defaults() {
736 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 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 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 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 let builder1 = PdfWriterOptions::builder();
773 let builder2 = builder1.dpi(300);
774 let opts = builder2.build();
775 assert_eq!(opts.dpi, 300);
776
777 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 let pdf_err = PdfReaderError::FileNotFound(PathBuf::from("/test.pdf"));
790 let msg = format!("{}", pdf_err);
791 assert!(msg.contains("test.pdf"));
792
793 let ext_err = ExtractError::PdfNotFound(PathBuf::from("/input.pdf"));
795 let msg = format!("{}", ext_err);
796 assert!(msg.contains("input.pdf"));
797
798 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 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 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 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 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 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 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 #[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 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 #[test]
1078 fn test_all_modules_have_consistent_builder_pattern() {
1079 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 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 assert!(pdf.jpeg_quality >= 1 && pdf.jpeg_quality <= 100);
1103
1104 let dsk = DeskewOptions::default();
1106 assert!(dsk.max_angle >= 1.0 && dsk.max_angle <= 45.0);
1107
1108 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 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 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}