Skip to main content

oxidize_pdf/
lib.rs

1//! # oxidize-pdf
2//!
3//! A comprehensive, pure Rust PDF library for generation, parsing, and manipulation with zero external PDF dependencies.
4
5#![allow(clippy::all)]
6//!
7//! ## Features
8//!
9//! - **PDF Generation**: Create multi-page documents with text, graphics, and images
10//! - **PDF Parsing**: Complete parser supporting rendering and content extraction
11//! - **PDF Operations**: Split, merge, rotate, and extract pages
12//! - **Text Extraction**: Extract text with position and formatting information
13//! - **Image Extraction**: Extract images in JPEG, PNG, and TIFF formats
14//! - **Font Embedding**: TrueType and OpenType font embedding with subsetting support (v1.1.6+)
15//! - **Page Analysis**: Detect scanned vs text content with intelligent classification
16//! - **OCR Integration**: Pluggable OCR support with Tesseract for processing scanned documents (v0.1.3+)
17//! - **Resource Access**: Work with fonts, images, and other PDF resources
18//! - **Pure Rust**: No C dependencies or external libraries
19//! - **100% Native**: Complete PDF implementation from scratch
20//!
21//! ## Quick Start
22//!
23//! ### Creating PDFs
24//!
25//! ```rust
26//! use oxidize_pdf::{Document, Page, Font, Color, Result};
27//!
28//! # fn main() -> Result<()> {
29//! // Create a new document
30//! let mut doc = Document::new();
31//! doc.set_title("My PDF");
32//!
33//! // Create a page
34//! let mut page = Page::a4();
35//!
36//! // Add text
37//! page.text()
38//!     .set_font(Font::Helvetica, 24.0)
39//!     .at(50.0, 700.0)
40//!     .write("Hello, PDF!")?;
41//!
42//! // Add graphics
43//! page.graphics()
44//!     .set_fill_color(Color::rgb(0.0, 0.5, 1.0))
45//!     .circle(300.0, 400.0, 50.0)
46//!     .fill();
47//!
48//! // Save the document
49//! doc.add_page(page);
50//! doc.save("output.pdf")?;
51//! # Ok(())
52//! # }
53//! ```
54//!
55//! ### Parsing PDFs
56//!
57//! ```rust,no_run
58//! use oxidize_pdf::parser::{PdfDocument, PdfReader};
59//!
60//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
61//! // Open and parse a PDF
62//! let reader = PdfReader::open("document.pdf")?;
63//! let document = PdfDocument::new(reader);
64//!
65//! // Get document information
66//! println!("Pages: {}", document.page_count()?);
67//! println!("Version: {}", document.version()?);
68//!
69//! // Process pages
70//! for i in 0..document.page_count()? {
71//!     let page = document.get_page(i)?;
72//!     println!("Page {} size: {}x{} points", i+1, page.width(), page.height());
73//! }
74//!
75//! // Extract text
76//! let text_pages = document.extract_text()?;
77//! for (i, page_text) in text_pages.iter().enumerate() {
78//!     println!("Page {} text: {}", i+1, page_text.text);
79//! }
80//! # Ok(())
81//! # }
82//! ```
83//!
84//! ## Modules
85//!
86//! ### Generation Modules
87//! - [`document`] - PDF document creation and management
88//! - [`page`] - Page creation and layout
89//! - [`graphics`] - Vector graphics and images
90//! - [`text`] - Text rendering and flow
91//! - [`writer`] - Low-level PDF writing
92//!
93//! ### Parsing Modules
94//! - [`parser`] - Complete PDF parsing and reading
95//!   - [`parser::PdfDocument`] - High-level document interface
96//!   - [`parser::ParsedPage`] - Page representation with resources
97//!   - [`parser::ContentParser`] - Content stream parsing
98//!   - [`parser::PdfObject`] - Low-level PDF objects
99//!
100//! ### Manipulation Modules
101//! - [`operations`] - PDF manipulation (split, merge, rotate, extract images)
102//! - [`operations::page_analysis`] - Page content analysis and scanned page detection
103//! - [`text::extraction`] - Text extraction with positioning
104//!
105//! ### OCR Modules (v0.1.3+)
106//! - [`text::ocr`] - OCR trait system and types
107//! - [`text::tesseract_provider`] - Tesseract OCR provider (requires `ocr-tesseract` feature)
108//! - [`text::ocr`] - OCR integration for scanned documents
109//!
110//! ## Examples
111//!
112//! ### Content Stream Processing
113//!
114//! ```rust,no_run
115//! use oxidize_pdf::parser::{PdfDocument, PdfReader};
116//! use oxidize_pdf::parser::content::{ContentParser, ContentOperation};
117//!
118//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
119//! let reader = PdfReader::open("document.pdf")?;
120//! let document = PdfDocument::new(reader);
121//! let page = document.get_page(0)?;
122//!
123//! // Get and parse content streams
124//! let streams = page.content_streams_with_document(&document)?;
125//! for stream in streams {
126//!     let operations = ContentParser::parse(&stream)?;
127//!     
128//!     for op in operations {
129//!         match op {
130//!             ContentOperation::ShowText(text) => {
131//!                 println!("Text: {:?}", String::from_utf8_lossy(&text));
132//!             }
133//!             ContentOperation::SetFont(name, size) => {
134//!                 println!("Font: {} at {} pt", name, size);
135//!             }
136//!             ContentOperation::MoveTo(x, y) => {
137//!                 println!("Move to ({}, {})", x, y);
138//!             }
139//!             _ => {} // Handle other operations
140//!         }
141//!     }
142//! }
143//! # Ok(())
144//! # }
145//! ```
146//!
147//! ### Resource Access
148//!
149//! ```rust,no_run
150//! use oxidize_pdf::parser::{PdfDocument, PdfReader};
151//!
152//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
153//! let reader = PdfReader::open("document.pdf")?;
154//! let document = PdfDocument::new(reader);
155//! let page = document.get_page(0)?;
156//!
157//! // Access page resources
158//! if let Some(resources) = page.get_resources() {
159//!     // Check fonts
160//!     if let Some(fonts) = resources.get("Font").and_then(|f| f.as_dict()) {
161//!         for (name, _) in &fonts.0 {
162//!             println!("Font resource: {}", name.as_str());
163//!         }
164//!     }
165//!     
166//!     // Check images/XObjects
167//!     if let Some(xobjects) = resources.get("XObject").and_then(|x| x.as_dict()) {
168//!         for (name, _) in &xobjects.0 {
169//!             println!("XObject resource: {}", name.as_str());
170//!         }
171//!     }
172//! }
173//! # Ok(())
174//! # }
175//! ```
176
177pub mod actions;
178pub mod advanced_tables;
179pub mod ai;
180pub mod annotations;
181
182pub mod batch;
183pub mod charts;
184pub mod compression;
185pub mod coordinate_system;
186pub mod document;
187pub mod encryption;
188pub mod error;
189pub mod fonts;
190pub mod forms;
191pub mod geometry;
192pub mod graphics;
193pub mod layout;
194pub mod memory;
195pub mod metadata;
196pub mod objects;
197pub mod operations;
198pub mod page;
199pub mod page_forms;
200pub mod page_labels;
201pub mod page_lists;
202pub mod page_tables;
203pub mod page_transitions;
204pub mod page_tree;
205pub mod parser;
206pub mod pdf_objects;
207pub mod pdfa;
208#[cfg(feature = "performance")]
209pub mod performance;
210pub mod pipeline;
211pub mod recovery;
212pub mod streaming;
213pub mod structure;
214pub mod templates;
215pub mod text;
216pub mod verification;
217pub mod viewer_preferences;
218pub mod writer;
219
220pub mod semantic;
221pub mod signatures;
222
223// Dashboard and reporting modules
224pub mod dashboard;
225
226// Re-export generation types
227pub use coordinate_system::{CoordinateSystem, RenderContext, TransformMatrix};
228pub use document::{Document, DocumentMetadata};
229pub use error::{OxidizePdfError, PdfError, Result};
230pub use geometry::{Point, Rectangle};
231pub use graphics::{Color, ColorSpace, GraphicsContext, Image, ImageFormat, MaskType};
232pub use layout::{
233    centered_image_x, fit_image_dimensions, DocumentBuilder, FlowElement, FlowLayout, PageConfig,
234    RichText, TextSpan,
235};
236pub use page::{Margins, Page};
237pub use page_lists::{ListStyle, ListType, PageLists};
238pub use page_tables::{PageTables, TableStyle};
239pub use text::{
240    measure_text,
241    measure_text_block,
242    split_into_words,
243    BulletStyle,
244    Font,
245    FontFamily,
246    FragmentType,
247    HeaderStyle,
248    ImagePreprocessing,
249    ListElement,
250    ListOptions,
251    MockOcrProvider,
252    OcrEngine,
253    OcrError,
254    OcrOptions,
255    OcrProcessingResult,
256    OcrProvider,
257    OcrResult,
258    OcrTextFragment,
259    // List exports
260    OrderedList,
261    OrderedListStyle,
262    // Table exports
263    Table,
264    TableCell,
265    TableOptions,
266    TextAlign,
267    TextBlockMetrics,
268    TextContext,
269    TextFlowContext,
270    UnorderedList,
271};
272
273// Re-export forms types
274pub use forms::{
275    calculations::FieldValue,
276    field_actions::{
277        ActionSettings, FieldAction, FieldActionSystem, FieldActions, FormatActionType,
278        SpecialFormatType, ValidateActionType,
279    },
280    validation::{
281        DateFormat, FieldValidator, FormValidationSystem, FormatMask, PhoneCountry,
282        RequiredFieldInfo, RequirementCondition, TimeFormat, ValidationRule, ValidationSettings,
283    },
284    BorderStyle, FieldType, TextField, Widget,
285};
286
287// Re-export font embedding types
288pub use text::fonts::embedding::{
289    EmbeddedFontData, EmbeddingOptions, EncodingDifference, FontDescriptor, FontEmbedder,
290    FontEncoding, FontFlags, FontMetrics, FontType,
291};
292
293// Re-export font management types
294pub use text::font_manager::{CustomFont, FontManager};
295
296// Re-export parsing types
297pub use parser::{
298    ContentOperation, ContentParser, DocumentMetadata as ParsedDocumentMetadata, ParseOptions,
299    ParsedPage, PdfArray, PdfDictionary, PdfDocument, PdfName, PdfObject, PdfReader, PdfStream,
300    PdfString,
301};
302
303// Re-export operations
304pub use operations::{
305    extract_images_from_pages, extract_images_from_pdf, extract_pdf_pages_lossless, merge_pdfs,
306    merge_pdfs_lossless, move_pdf_page, mutate_pdf_pages_lossless, overlay_pdf,
307    plan_extract_pdf_pages_lossless, plan_merge_pdfs_lossless, plan_pdf_page_mutations,
308    plan_split_pdf_lossless, reorder_pdf_pages, reorder_pdf_pages_lossless, reverse_pdf_pages,
309    rotate_pdf_pages, split_pdf, split_pdf_lossless, swap_pdf_pages, DocumentStructure,
310    ExtractImagesOptions, ExtractedImage, ExtractedImageData, ImageExtractionError,
311    ImageExtractionLimits, ImageExtractionResult, ImageExtractor, InputSemanticReport,
312    InputSemanticRole, LosslessMergeInput, OverlayOptions, OverlayPosition, PageMutation,
313    PageMutationBatch, PageMutationReport, ReorderOptions, SemanticPreservationReport,
314    StructureDisposition, StructureSemanticReport,
315};
316
317// Re-export dashboard types
318pub use dashboard::{
319    Dashboard, DashboardBuilder, DashboardComponent, DashboardConfig, DashboardLayout,
320    DashboardTheme, HeatMap, KpiCard, PivotTable, ScatterPlot, TreeMap, Typography,
321};
322
323// Re-export memory optimization types
324pub use memory::{LazyDocument, MemoryOptions, StreamProcessor, StreamingOptions};
325
326// Re-export streaming types
327pub use streaming::{
328    process_in_chunks, stream_text, ChunkOptions, ChunkProcessor, ChunkType, ContentChunk,
329    IncrementalParser, ParseEvent, StreamingDocument, StreamingOptions as StreamOptions,
330    StreamingPage, TextChunk, TextStreamOptions, TextStreamer,
331};
332
333// Re-export batch processing types
334pub use batch::{
335    batch_merge_pdfs, batch_process_files, batch_split_pdfs, BatchJob, BatchOptions,
336    BatchProcessor, BatchProgress, BatchResult, BatchSummary, JobResult, JobStatus, JobType,
337    ProgressCallback, ProgressInfo,
338};
339
340// Re-export recovery types
341pub use recovery::{
342    analyze_corruption, detect_corruption, quick_recover, repair_document, validate_pdf,
343    CorruptionReport, CorruptionType, ObjectScanner, PartialRecovery, PdfRecovery, RecoveredPage,
344    RecoveryOptions, RepairResult, RepairStrategy, ScanResult, ValidationError, ValidationResult,
345};
346
347// Re-export structure types
348pub use structure::{
349    Destination, DestinationType, NameTree, NameTreeNode, NamedDestinations, OutlineBuilder,
350    OutlineFlags, OutlineItem, OutlineTree, PageDestination, PageTree, PageTreeBuilder,
351    PageTreeNode,
352};
353
354// Re-export action types
355pub use actions::{
356    Action, ActionDictionary, ActionType, GoToAction, LaunchAction, LaunchParameters, NamedAction,
357    RemoteGoToAction, StandardNamedAction, UriAction, UriActionFlags,
358};
359
360// Re-export page label types
361pub use page_labels::{PageLabel, PageLabelBuilder, PageLabelRange, PageLabelStyle, PageLabelTree};
362
363// Re-export template types
364pub use templates::{
365    Template, TemplateContext, TemplateError, TemplateRenderer, TemplateResult, TemplateValue,
366};
367
368// Re-export semantic types for AI-Ready PDFs
369pub use semantic::{
370    BoundingBox, Entity, EntityMap, EntityMetadata, EntityRelation, EntityType, ExportFormat,
371    RelationType, SemanticEntity, SemanticMarking,
372};
373
374// Re-export verification types
375pub use verification::comparators::{
376    compare_pdfs, ComparisonResult, DifferenceSeverity, PdfDifference,
377};
378pub use verification::compliance_report::{
379    format_report_markdown, generate_compliance_report, ComplianceReport,
380};
381pub use verification::iso_matrix::{load_default_matrix, load_matrix, ComplianceStats, IsoMatrix};
382pub use verification::semantic_comparison::{
383    compare_pdfs_semantically, PdfRevisionSummary, RevisionObjectChange, RevisionObjectChangeKind,
384    SemanticComparisonLimits, SemanticComparisonOptions, SemanticComparisonResult,
385    SemanticDifferenceClass, SemanticPdfDifference,
386};
387pub use verification::validators::{
388    check_available_validators, validate_external, validate_with_qpdf,
389};
390pub use verification::{
391    extract_pdf_differences, pdfs_structurally_equivalent, verify_iso_requirement,
392    ExternalValidationResult, IsoRequirement, VerificationLevel, VerificationResult,
393};
394
395// Re-export PDF/A compliance types
396pub use pdfa::{
397    PdfAConformance, PdfAError, PdfALevel, PdfAResult, PdfAValidator,
398    ValidationError as PdfAValidationError, ValidationResult as PdfAValidationResult,
399    ValidationWarning as PdfAValidationWarning, XmpMetadata, XmpPdfAIdentifier,
400};
401
402/// Current version of oxidize-pdf
403pub const VERSION: &str = env!("CARGO_PKG_VERSION");
404
405/// Scanned page analysis and OCR example
406///
407/// ```rust,no_run
408/// use oxidize_pdf::operations::page_analysis::{PageContentAnalyzer, PageType};
409/// use oxidize_pdf::text::{MockOcrProvider, OcrOptions};
410/// use oxidize_pdf::parser::PdfReader;
411///
412/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
413/// let document = PdfReader::open_document("scanned.pdf")?;
414/// let analyzer = PageContentAnalyzer::new(document);
415///
416/// // Analyze pages for scanned content
417/// let analyses = analyzer.analyze_document()?;
418/// for analysis in analyses {
419///     match analysis.page_type {
420///         PageType::Scanned => {
421///             println!("Page {} is scanned - applying OCR", analysis.page_number);
422///             
423///             // Process with OCR
424///             let ocr_provider = MockOcrProvider::new();
425///             let ocr_result = analyzer.extract_text_from_scanned_page(
426///                 analysis.page_number,
427///                 &ocr_provider
428///             )?;
429///             
430///             println!("OCR extracted: {}", ocr_result.text);
431///             println!("Confidence: {:.1}%", ocr_result.confidence * 100.0);
432///         }
433///         PageType::Text => println!("Page {} has vector text", analysis.page_number),
434///         PageType::Mixed => println!("Page {} has mixed content", analysis.page_number),
435///     }
436/// }
437/// # Ok(())
438/// # }
439/// ```
440///
441/// ### Font Embedding
442///
443/// ```rust,no_run
444/// use oxidize_pdf::{FontEmbedder, EmbeddingOptions, Document, Page, Font};
445/// use std::collections::HashSet;
446///
447/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
448/// // Create font embedder
449/// let mut embedder = FontEmbedder::new();
450///
451/// // Define used glyphs (example with basic ASCII)
452/// let mut used_glyphs = HashSet::new();
453/// used_glyphs.insert(65); // 'A'
454/// used_glyphs.insert(66); // 'B'
455/// used_glyphs.insert(67); // 'C'
456///
457/// // Configure embedding options
458/// let options = EmbeddingOptions {
459///     subset: true,                    // Create font subset
460///     compress_font_streams: true,     // Compress font data
461///     ..Default::default()
462/// };
463///
464/// // Load font data (example - you'd load actual TrueType data)
465/// let font_data = std::fs::read("path/to/font.ttf")?;
466///
467/// // Embed the font
468/// let font_name = embedder.embed_truetype_font(&font_data, &used_glyphs, &options)?;
469/// println!("Embedded font as: {}", font_name);
470///
471/// // Generate PDF dictionary for the embedded font
472/// let font_dict = embedder.generate_font_dictionary(&font_name)?;
473/// println!("Font dictionary generated successfully");
474/// # Ok(())
475/// # }
476/// ```
477///
478/// Supported PDF versions
479pub mod pdf_version {
480    /// PDF 1.0 - 1.7 are fully supported
481    pub const SUPPORTED_VERSIONS: &[&str] =
482        &["1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6", "1.7"];
483    /// PDF 2.0 support is planned
484    pub const PLANNED_VERSIONS: &[&str] = &["2.0"];
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490
491    #[test]
492    fn test_create_empty_document() {
493        let doc = Document::new();
494        assert_eq!(doc.pages.len(), 0);
495    }
496
497    #[test]
498    fn test_create_page() {
499        let page = Page::new(595.0, 842.0);
500        assert_eq!(page.width(), 595.0);
501        assert_eq!(page.height(), 842.0);
502    }
503
504    #[test]
505    fn test_version_info() {
506        assert!(!VERSION.is_empty());
507        assert!(pdf_version::SUPPORTED_VERSIONS.contains(&"1.7"));
508    }
509
510    #[test]
511    fn test_pdf_version_constants() {
512        // Test that all expected PDF versions are supported
513        let expected_versions = ["1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6", "1.7"];
514
515        for version in expected_versions {
516            assert!(
517                pdf_version::SUPPORTED_VERSIONS.contains(&version),
518                "Expected PDF version {version} to be supported"
519            );
520        }
521
522        // Test that we have exactly 8 supported versions
523        assert_eq!(pdf_version::SUPPORTED_VERSIONS.len(), 8);
524
525        // Test planned versions
526        assert!(pdf_version::PLANNED_VERSIONS.contains(&"2.0"));
527        assert_eq!(pdf_version::PLANNED_VERSIONS.len(), 1);
528    }
529
530    #[test]
531    fn test_document_with_metadata() {
532        let mut doc = Document::new();
533        doc.set_title("Test Document");
534        doc.set_author("Test Author");
535        doc.set_subject("Test Subject");
536
537        // Verify metadata is set (checking internal state)
538        assert_eq!(doc.pages.len(), 0);
539        // Note: We can't directly test metadata without exposing getters
540        // This test ensures the methods don't panic
541    }
542
543    #[test]
544    fn test_page_creation_variants() {
545        // Test different page creation methods
546        let page_a4 = Page::a4();
547        let page_letter = Page::letter();
548        let page_custom = Page::new(400.0, 600.0);
549
550        // A4 dimensions: 595.276 x 841.89 points (approximation)
551        assert!((page_a4.width() - 595.0).abs() < 10.0);
552        assert!((page_a4.height() - 842.0).abs() < 10.0);
553
554        // Letter dimensions: 612 x 792 points
555        assert_eq!(page_letter.width(), 612.0);
556        assert_eq!(page_letter.height(), 792.0);
557
558        // Custom dimensions
559        assert_eq!(page_custom.width(), 400.0);
560        assert_eq!(page_custom.height(), 600.0);
561    }
562
563    #[test]
564    fn test_color_creation() {
565        let red = Color::rgb(1.0, 0.0, 0.0);
566        let green = Color::rgb(0.0, 1.0, 0.0);
567        let blue = Color::rgb(0.0, 0.0, 1.0);
568        let black = Color::rgb(0.0, 0.0, 0.0);
569        let white = Color::rgb(1.0, 1.0, 1.0);
570
571        // Test color creation doesn't panic
572        let _colors = [red, green, blue, black, white];
573
574        // Test CMYK color (if available)
575        let cyan = Color::cmyk(1.0, 0.0, 0.0, 0.0);
576        let _cmyk_test = cyan;
577    }
578
579    #[test]
580    fn test_font_types() {
581        let helvetica = Font::Helvetica;
582        let times = Font::TimesRoman;
583        let courier = Font::Courier;
584
585        // Test font creation doesn't panic
586        let _fonts = [helvetica, times, courier];
587
588        // Test font family
589        let helvetica_family = FontFamily::Helvetica;
590        let times_family = FontFamily::Times;
591        let courier_family = FontFamily::Courier;
592
593        let _families = [helvetica_family, times_family, courier_family];
594    }
595
596    #[test]
597    fn test_error_types() {
598        // Test that error types can be created
599        let pdf_error = PdfError::InvalidStructure("test error".to_string());
600        let _error_test = pdf_error;
601
602        // Test result type
603        let ok_result: Result<i32> = Ok(42);
604        let err_result: Result<i32> = Err(PdfError::InvalidStructure("test error".to_string()));
605
606        assert!(ok_result.is_ok());
607        assert!(err_result.is_err());
608    }
609
610    #[test]
611    fn test_module_exports() {
612        // Test that all major types are properly exported
613        let _doc = Document::new();
614        let _page = Page::new(100.0, 100.0);
615        let _color = Color::rgb(0.5, 0.5, 0.5);
616        let _font = Font::Helvetica;
617
618        // Test parsing types
619        let _array = PdfArray::new();
620        let _dict = PdfDictionary::new();
621        let _name = PdfName::new("Test".to_string());
622        let _string = PdfString::new(b"Test".to_vec());
623
624        // Test operation types
625        let _margins = Margins {
626            top: 10.0,
627            right: 10.0,
628            bottom: 10.0,
629            left: 10.0,
630        };
631        let _align = TextAlign::Left;
632    }
633
634    #[test]
635    fn test_ocr_types() {
636        // Test OCR-related types
637        let _mock_ocr = MockOcrProvider::new();
638        let _ocr_options = OcrOptions::default();
639        let _ocr_engine = OcrEngine::Tesseract;
640
641        // Test fragment types
642        let _fragment_type = FragmentType::Word;
643        let _image_preprocessing = ImagePreprocessing::default();
644    }
645
646    #[test]
647    fn test_text_utilities() {
648        // Test text utility functions
649        let text = "Hello world test";
650        let words = split_into_words(text);
651        assert!(!words.is_empty());
652        assert!(words.contains(&"Hello"));
653        assert!(words.contains(&"world"));
654
655        // Test text measurement (with mock font)
656        let font = Font::Helvetica;
657        let size = 12.0;
658        let width = measure_text(text, &font, size);
659        assert!(width > 0.0);
660    }
661
662    #[test]
663    fn test_image_types() {
664        // Test image-related types
665        let _format = ImageFormat::Jpeg;
666        let _color_space = ColorSpace::DeviceRGB;
667
668        // Test that image creation doesn't panic
669        let image_data = vec![0u8; 100];
670        let _image = Image::from_jpeg_data(image_data);
671    }
672
673    #[test]
674    fn test_version_string_format() {
675        // Test that version string follows semantic versioning
676        let version_parts: Vec<&str> = VERSION.split('.').collect();
677        assert!(
678            version_parts.len() >= 2,
679            "Version should have at least major.minor format"
680        );
681
682        // Test that major and minor are numeric
683        assert!(
684            version_parts[0].parse::<u32>().is_ok(),
685            "Major version should be numeric"
686        );
687        assert!(
688            version_parts[1].parse::<u32>().is_ok(),
689            "Minor version should be numeric"
690        );
691
692        // Test that version is not empty
693        assert!(!VERSION.is_empty());
694        assert!(!VERSION.is_empty());
695    }
696}