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}