# PDF Oxide — The Fastest PDF Library for Python and Rust — Complete Documentation > The fastest PDF library for Python and Rust. > Text extraction, image extraction, PDF creation, editing, and markdown conversion. > Mean 0.8ms per document. 100% pass rate on 3,830 real-world PDFs. > 5× faster than PyMuPDF, 15× faster than pypdf. > MIT / Apache-2.0 license. v0.3.34. > > Install: pip install pdf-oxide (Python) | cargo add pdf_oxide (Rust) > GitHub: https://github.com/yfedoseev/pdf_oxide > Docs: https://pdf.oxide.fyi --- # Getting Started with PDF Oxide (Python) PDF Oxide is the complete PDF toolkit. One library for extracting, creating, and editing PDFs with a unified API. Built on a Rust core for maximum performance. ## Installation ```bash pip install pdf_oxide ``` **Requirements:** Python 3.8+. Pre-built wheels are available for Linux, macOS, and Windows on both x86_64 and ARM64 architectures. No compiler or system dependencies needed. ## Opening a PDF Use `PdfDocument` to open and inspect any PDF file. ```python from pdf_oxide import PdfDocument doc = PdfDocument("research-paper.pdf") print(f"Pages: {doc.page_count}") print(f"PDF version: {doc.version}") ``` ## Text Extraction ### Single Page Extract plain text from any page by its zero-based index. ```python from pdf_oxide import PdfDocument doc = PdfDocument("report.pdf") text = doc.extract_text(0) print(text) ``` ### All Pages ```python from pdf_oxide import PdfDocument doc = PdfDocument("book.pdf") for i in range(doc.page_count): text = doc.extract_text(i) print(f"--- Page {i + 1} ---") print(text) ``` ## Character-Level Extraction `extract_chars()` returns a list of `TextChar` objects with precise positioning and font metadata for every character on the page. ```python from pdf_oxide import PdfDocument doc = PdfDocument("paper.pdf") chars = doc.extract_chars(0) for ch in chars[:10]: print(f"'{ch.char}' at ({ch.x:.1f}, {ch.y:.1f}) " f"size={ch.font_size:.1f} font={ch.font_name} " f"bbox={ch.bbox}") ``` Each `TextChar` has the following fields: | Field | Type | Description | |---------------|-------------------|--------------------------------------| | `char` | `str` | The Unicode character | | `x` | `float` | Horizontal position in points | | `y` | `float` | Vertical position in points | | `font_size` | `float` | Font size in points | | `font_name` | `str` | PostScript font name | | `bbox` | `tuple[float, 4]` | Bounding box `(x0, y0, x1, y1)` | ## Text Spans `extract_spans()` groups consecutive characters that share the same font and size into spans, giving you structured text with font metadata. ```python from pdf_oxide import PdfDocument doc = PdfDocument("paper.pdf") spans = doc.extract_spans(0) for span in spans: print(f"'{span.text}' font={span.font_name} size={span.font_size}") ``` ## Markdown Conversion Convert a PDF page to Markdown with optional heading detection. ```python from pdf_oxide import PdfDocument doc = PdfDocument("paper.pdf") md = doc.to_markdown(0, detect_headings=True) print(md) ``` ## HTML Conversion Convert a PDF page to HTML. ```python from pdf_oxide import PdfDocument doc = PdfDocument("paper.pdf") html = doc.to_html(0) print(html) ``` ## Image Extraction `extract_images()` returns a list of `ImageInfo` objects for every image embedded on a page, including images referenced in content streams and nested Form XObjects. ```python from pdf_oxide import PdfDocument doc = PdfDocument("brochure.pdf") images = doc.extract_images(0) for i, img in enumerate(images): print(f"Image {i}: {img.width}x{img.height} " f"{img.color_space} {img.bits_per_component}bpc " f"({len(img.data)} bytes)") img.save(f"image_{i}.png") ``` Each `ImageInfo` has the following fields: | Field | Type | Description | |----------------------|---------|------------------------------------| | `width` | `int` | Image width in pixels | | `height` | `int` | Image height in pixels | | `color_space` | `str` | Color space (e.g. `DeviceRGB`) | | `bits_per_component` | `int` | Bits per color channel | | `data` | `bytes` | Raw image data | ## Password-Protected PDFs Pass the user password as a second argument to open encrypted documents. ```python from pdf_oxide import PdfDocument doc = PdfDocument("confidential.pdf", password="secret") text = doc.extract_text(0) print(text) ``` ## PDF Creation The `Pdf` class provides factory methods to create PDFs from various source formats. ### From Markdown ```python from pdf_oxide import Pdf pdf = Pdf.from_markdown("# Hello World\n\nThis is a PDF.") pdf.save("output.pdf") ``` ### From HTML ```python from pdf_oxide import Pdf pdf = Pdf.from_html("

Invoice

Amount due: $42.00

") pdf.save("invoice.pdf") ``` ### From Plain Text ```python from pdf_oxide import Pdf pdf = Pdf.from_text("Plain text document.\n\nSecond paragraph.") pdf.save("notes.pdf") ``` ### From Images ```python from pdf_oxide import Pdf pdf = Pdf.from_image("scan.jpg") pdf.save("scan.pdf") ``` ## Search Search for text across the entire document or within a single page. ```python from pdf_oxide import PdfDocument doc = PdfDocument("manual.pdf") # Search all pages results = doc.search("configuration") for r in results: print(f"Page {r.page}: '{r.text}' at ({r.x:.0f}, {r.y:.0f})") # Search a single page page_results = doc.search_page(0, "configuration") ``` ## Error Handling PDF Oxide raises `PdfError` for PDF-specific failures and standard Python exceptions for I/O problems. ```python from pdf_oxide import PdfDocument, PdfError try: doc = PdfDocument("document.pdf") text = doc.extract_text(0) except PdfError as e: print(f"PDF error: {e}") except FileNotFoundError: print("File not found") ``` ## Next Steps - [Rust Getting Started](/docs/getting-started/rust) -- using PDF Oxide from Rust - [Text Extraction](/docs/extraction/text) -- detailed extraction options and recipes - [PDF Creation](/docs/creation/from-markdown) -- advanced creation with PdfBuilder, encryption, and metadata - [Editing](/docs/editing/overview) -- modifying existing PDFs, annotations, and form fields - [API Reference](https://docs.rs/pdf_oxide) -- full API documentation --- # Getting Started with PDF Oxide (Rust) PDF Oxide is the complete PDF toolkit for Rust. One library for extracting, creating, and editing PDFs with a unified API. ## Installation Add `pdf_oxide` to your `Cargo.toml`: ```toml [dependencies] pdf_oxide = "0.3" ``` ### Feature Flags Enable only the capabilities you need: ```toml # Default -- text extraction, creation, editing pdf_oxide = "0.3" # Page rendering to images pdf_oxide = { version = "0.3", features = ["rendering"] } # Barcode generation pdf_oxide = { version = "0.3", features = ["barcodes"] } # Digital signatures pdf_oxide = { version = "0.3", features = ["signatures"] } # Office document conversion (DOCX, XLSX, PPTX) pdf_oxide = { version = "0.3", features = ["office"] } # Everything pdf_oxide = { version = "0.3", features = ["full"] } ``` ## Opening a PDF Use `PdfDocument::open()` to load a file and inspect its metadata. ```rust use pdf_oxide::PdfDocument; let doc = PdfDocument::open("research-paper.pdf")?; println!("Pages: {}", doc.page_count()); println!("PDF version: {}", doc.version()); ``` ## Text Extraction ### Plain Text ```rust use pdf_oxide::PdfDocument; let doc = PdfDocument::open("report.pdf")?; let text = doc.extract_text(0)?; println!("{text}"); ``` ### Text Spans `extract_spans()` returns a `Vec` with font metadata for each run of identically-styled text. ```rust use pdf_oxide::PdfDocument; let doc = PdfDocument::open("paper.pdf")?; let spans = doc.extract_spans(0)?; for span in &spans { println!("'{}' at ({:.1}, {:.1}) font={} size={:.1}", span.text, span.x, span.y, span.font_name, span.font_size); } ``` `TextSpan` fields: | Field | Type | Description | |-------------|----------|----------------------------------| | `text` | `String` | The text content | | `x` | `f64` | Horizontal position in points | | `y` | `f64` | Vertical position in points | | `font_name` | `String` | PostScript font name | | `font_size` | `f64` | Font size in points | | `bbox` | `Rect` | Bounding rectangle | ## Character-Level Extraction `extract_chars()` returns a `Vec` with precise positioning for every character. ```rust use pdf_oxide::PdfDocument; let doc = PdfDocument::open("paper.pdf")?; let chars = doc.extract_chars(0)?; for ch in chars.iter().take(10) { println!("'{}' at ({:.1}, {:.1}) size={:.1} font={}", ch.char, ch.x, ch.y, ch.font_size, ch.font_name); } ``` `TextChar` fields: | Field | Type | Description | |-------------|----------|----------------------------------| | `char` | `char` | The Unicode character | | `x` | `f64` | Horizontal position in points | | `y` | `f64` | Vertical position in points | | `font_size` | `f64` | Font size in points | | `font_name` | `String` | PostScript font name | | `bbox` | `Rect` | Bounding rectangle | ## Markdown Conversion Convert a page to Markdown with configurable options. ```rust use pdf_oxide::PdfDocument; use pdf_oxide::MarkdownOptions; let doc = PdfDocument::open("paper.pdf")?; let options = MarkdownOptions { detect_headings: true, ..Default::default() }; let md = doc.to_markdown(0, &options)?; println!("{md}"); ``` ## HTML Conversion ```rust use pdf_oxide::PdfDocument; let doc = PdfDocument::open("paper.pdf")?; let html = doc.to_html(0)?; println!("{html}"); ``` ## Image Extraction `extract_images()` returns metadata and raw data for every image on a page, including images in content streams and nested Form XObjects. ```rust use pdf_oxide::PdfDocument; let doc = PdfDocument::open("brochure.pdf")?; let images = doc.extract_images(0)?; for (i, img) in images.iter().enumerate() { println!("Image {i}: {}x{} {} {}bpc ({} bytes)", img.width, img.height, img.color_space, img.bits_per_component, img.data.len()); } ``` Write images directly to disk with `extract_images_to_files()`: ```rust let doc = PdfDocument::open("brochure.pdf")?; let paths = doc.extract_images_to_files(0, "output_dir")?; for path in &paths { println!("Saved: {}", path.display()); } ``` ## PDF Creation ### Factory Methods The `Pdf` type provides high-level factory methods. ```rust use pdf_oxide::api::Pdf; let mut pdf = Pdf::from_markdown("# Hello World\n\nThis is a PDF.")?; pdf.save("output.pdf")?; ``` ```rust let mut pdf = Pdf::from_html("

Invoice

Amount: $42

")?; pdf.save("invoice.pdf")?; ``` ```rust let mut pdf = Pdf::from_text("Plain text content.")?; pdf.save("notes.pdf")?; ``` ```rust let mut pdf = Pdf::from_image("scan.jpg")?; pdf.save("scan.pdf")?; ``` ### PdfBuilder Fluent API For full control over metadata, page size, and margins: ```rust use pdf_oxide::api::PdfBuilder; use pdf_oxide::writer::PageSize; let mut pdf = PdfBuilder::new() .title("Annual Report") .author("Acme Corp") .page_size(PageSize::A4) .margins(72.0, 72.0, 72.0, 72.0) .font_size(11.0) .from_markdown("# Annual Report\n\n...")?; pdf.save("annual-report.pdf")?; ``` ### DocumentBuilder Low-Level API For pixel-level placement of text, shapes, and images: ```rust use pdf_oxide::writer::DocumentBuilder; let mut builder = DocumentBuilder::new(); builder.add_page(612.0, 792.0) .text("Hello, world!", 72.0, 720.0, 12.0) .rect(100.0, 600.0, 200.0, 50.0) .image_at("logo.png", 400.0, 700.0, 100.0, 50.0)?; builder.save("custom.pdf")?; ``` ## Search Search for text across the document or with fine-grained options. ```rust use pdf_oxide::api::Pdf; let pdf = Pdf::open("manual.pdf")?; // Simple search across all pages let results = pdf.search("configuration")?; for r in &results { println!("Page {}: '{}' at ({:.0}, {:.0})", r.page, r.text, r.x, r.y); } ``` ```rust use pdf_oxide::api::{Pdf, SearchOptions}; let pdf = Pdf::open("manual.pdf")?; let opts = SearchOptions { case_sensitive: false, whole_word: true, max_results: Some(50), ..Default::default() }; let results = pdf.search_with_options("configuration", &opts)?; ``` ## Editing ### DocumentEditor Open an existing PDF for structural edits like page rotation and form field manipulation. ```rust use pdf_oxide::api::Pdf; let mut pdf = Pdf::open_editor("form-template.pdf")?; // Rotate a page pdf.rotate_page(0, 90)?; // Add a form field pdf.add_text_field("name", [100.0, 700.0, 300.0, 720.0])?; pdf.add_checkbox("agree", [100.0, 650.0, 120.0, 670.0], false)?; pdf.save("modified.pdf")?; ``` ### DOM-Like Page Editing Navigate page elements and modify text in place. ```rust use pdf_oxide::api::Pdf; let mut pdf = Pdf::open("document.pdf")?; let mut page = pdf.page(0)?; // Find text elements for t in page.find_text_containing("Draft") { println!("Found '{}' at {:?}", t.text(), t.bbox()); } // Replace text let matches = page.find_text_containing("Draft"); for t in &matches { page.set_text(t.id(), "Final")?; } pdf.save_page(page)?; pdf.save("updated.pdf")?; ``` ## Error Handling All fallible operations return `Result`. The `PdfError` enum covers the main failure modes. ```rust use pdf_oxide::PdfDocument; use pdf_oxide::PdfError; fn extract(path: &str) -> Result { let doc = PdfDocument::open(path)?; doc.extract_text(0) } match extract("file.pdf") { Ok(text) => println!("{text}"), Err(PdfError::Io(e)) => eprintln!("I/O error: {e}"), Err(PdfError::Parse(msg)) => eprintln!("Parse error: {msg}"), Err(PdfError::Password) => eprintln!("Password required"), Err(PdfError::PageOutOfRange { index, count }) => { eprintln!("Page {index} does not exist ({count} pages total)"); } Err(e) => eprintln!("Error: {e}"), } ``` `PdfError` variants: | Variant | Description | |------------------|-------------------------------------------------| | `Io` | File system or I/O failure | | `Parse` | Malformed PDF structure | | `Password` | Document is encrypted and no password was given | | `PageOutOfRange` | Requested page index exceeds page count | ## Next Steps - [Python Getting Started](/docs/getting-started/python) -- using PDF Oxide from Python - [Text Extraction](/docs/extraction/text) -- detailed extraction options and recipes - [PDF Creation](/docs/creation/from-markdown) -- advanced creation with PdfBuilder, encryption, and metadata - [Editing](/docs/editing/overview) -- modifying existing PDFs, annotations, and form fields - [API Reference](https://docs.rs/pdf_oxide) -- full API documentation --- # Text Extraction PDF Oxide provides multiple levels of text extraction: full-page text, styled spans with font metadata, and individual characters with precise positioning. Use `extract_text()` for quick content retrieval, `extract_spans()` when you need font and position data, and `extract_chars()` for per-character analysis such as custom layout engines or OCR post-processing. For Tagged PDFs, text extraction automatically follows the document's structure tree for correct reading order. For untagged PDFs, extraction uses page content order with intelligent line-break detection. ## Quick Example **Python** ```python from pdf_oxide import PdfDocument doc = PdfDocument("report.pdf") text = doc.extract_text(0) print(text) ``` **Rust** ```rust use pdf_oxide::PdfDocument; let mut doc = PdfDocument::open("report.pdf")?; let text = doc.extract_text(0)?; println!("{}", text); ``` --- ## API Reference ### `extract_text(page_index) -> str` Extract all text from a page as a single string. Automatically detects Tagged PDFs and uses the structure tree for reading order when available. Inserts line breaks and spaces based on vertical and horizontal gaps between spans. | Parameter | Type | Description | |-----------|------|-------------| | `page_index` | `int` / `usize` | Zero-based page index | **Returns:** The full text content of the page. **Python** ```python doc = PdfDocument("report.pdf") for i in range(doc.page_count()): text = doc.extract_text(i) print(f"--- Page {i + 1} ---") print(text) ``` **Rust** ```rust let mut doc = PdfDocument::open("report.pdf")?; let page_count = doc.page_count()?; for i in 0..page_count { let text = doc.extract_text(i)?; println!("--- Page {} ---", i + 1); println!("{}", text); } ``` --- ### `extract_spans(page_index) -> list[TextSpan]` Extract text as spans -- contiguous runs of text with the same font and style. Each span includes the text content, bounding box, font name, font size, weight, italic flag, and color. This is the recommended approach for most extraction tasks that need layout or font information. | Parameter | Type | Description | |-----------|------|-------------| | `page_index` | `int` / `usize` | Zero-based page index | **Returns:** A list/vector of `TextSpan` objects. #### TextSpan Fields | Field | Type | Description | |-------|------|-------------| | `text` | `str` | The text content of the span | | `bbox` | `Rect` | Bounding box (x, y, width, height) | | `font_name` | `str` | Font name/family (e.g., "Helvetica", "TimesNewRoman") | | `font_size` | `f32` | Font size in points | | `font_weight` | `FontWeight` | Weight: Normal, Bold, Light, SemiBold, etc. | | `is_italic` | `bool` | Whether the span is italic | | `color` | `Color` | RGB color (r, g, b) with values 0.0--1.0 | | `mcid` | `Option` | Marked Content ID for Tagged PDFs | | `sequence` | `usize` | Extraction order (tie-breaker for Y-coordinate sorting) | | `char_spacing` | `f32` | Character spacing (Tc parameter) | | `word_spacing` | `f32` | Word spacing (Tw parameter) | | `horizontal_scaling` | `f32` | Horizontal scaling percentage (Tz, default 100.0) | **Rust** ```rust let mut doc = PdfDocument::open("paper.pdf")?; let spans = doc.extract_spans(0)?; for span in &spans { println!( "'{}' at ({:.1}, {:.1}) font={} size={:.1}pt bold={} italic={}", span.text, span.bbox.x, span.bbox.y, span.font_name, span.font_size, span.font_weight == FontWeight::Bold, span.is_italic, ); } ``` --- ### `extract_spans_with_config(page_index, config) -> Vec` Extract spans with custom span-merging configuration. Use this when the default merging behavior produces incorrect word boundaries for your document. | Parameter | Type | Description | |-----------|------|-------------| | `page_index` | `usize` | Zero-based page index | | `config` | `SpanMergingConfig` | Configuration controlling extraction parameters | **Rust** ```rust use pdf_oxide::extractors::SpanMergingConfig; let mut doc = PdfDocument::open("report.pdf")?; let config = SpanMergingConfig::adaptive(); let spans = doc.extract_spans_with_config(0, config)?; ``` --- ### `extract_chars(page_index) -> list[TextChar]` Extract individual characters with precise bounding boxes, font metadata, and transformation properties. This is a low-level API -- prefer `extract_text()` or `extract_spans()` for most use cases. Character extraction is 30--50% faster than span extraction because it skips text grouping and merging. | Parameter | Type | Description | |-----------|------|-------------| | `page_index` | `int` / `usize` | Zero-based page index | **Returns:** A list/vector of `TextChar` objects. #### TextChar Fields | Field | Type | Description | |-------|------|-------------| | `char` | `char` | The character | | `bbox` | `Rect` | Bounding box (x, y, width, height) | | `font_name` | `str` | Font name/family | | `font_size` | `f32` | Font size in points | | `font_weight` | `FontWeight` | Weight (Normal, Bold, etc.) | | `is_italic` | `bool` | Italic flag | | `color` | `Color` | RGB color (0.0--1.0 per component) | | `mcid` | `Option` | Marked Content ID | | `origin_x` | `f32` | Baseline origin X coordinate | | `origin_y` | `f32` | Baseline origin Y coordinate | | `rotation_degrees` | `f32` | Text rotation angle (0--360, clockwise) | | `advance_width` | `f32` | Horizontal distance to next character position | | `matrix` | `[f32; 6]` | Full transformation matrix [a, b, c, d, e, f] | **Python** ```python doc = PdfDocument("report.pdf") chars = doc.extract_chars(0) for ch in chars: print(f"'{ch.char}' at ({ch.bbox[0]:.1f}, {ch.bbox[1]:.1f}) " f"font={ch.font_name} size={ch.font_size:.1f}") ``` **Rust** ```rust let mut doc = PdfDocument::open("report.pdf")?; let chars = doc.extract_chars(0)?; for ch in &chars { println!( "'{}' origin=({:.1}, {:.1}) rotation={:.0} advance={:.1}", ch.char, ch.origin_x, ch.origin_y, ch.rotation_degrees, ch.advance_width, ); } ``` --- ### `to_plain_text(page_index, options) -> str` Convert a single page to plain text. Accepts conversion options for API consistency, although most options apply primarily to Markdown/HTML output. | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `page_index` | `int` / `usize` | -- | Zero-based page index | | `preserve_layout` | `bool` | `false` | Preserve visual layout | | `detect_headings` | `bool` | `true` | Detect headings | | `include_images` | `bool` | `true` | Include images | | `image_output_dir` | `str` / `None` | `None` | Image output directory | **Python** ```python doc = PdfDocument("paper.pdf") text = doc.to_plain_text(0) ``` **Rust** ```rust use pdf_oxide::converters::ConversionOptions; let mut doc = PdfDocument::open("paper.pdf")?; let options = ConversionOptions::default(); let text = doc.to_plain_text(0, &options)?; ``` --- ### `extract_hierarchical_content(page_index) -> Option` Extract page content as a hierarchical structure tree. Returns `None` for untagged PDFs. For Tagged PDFs, returns a `StructureElement` tree that represents the document's logical structure (headings, paragraphs, tables, figures). | Parameter | Type | Description | |-----------|------|-------------| | `page_index` | `int` / `usize` | Zero-based page index | **Rust** ```rust let mut doc = PdfDocument::open("tagged-report.pdf")?; if let Some(root) = doc.extract_hierarchical_content(0)? { println!("Structure type: {:?}", root.structure_type); for child in &root.children { println!(" Child: {:?}", child.structure_type); } } ``` --- ## Advanced Examples ### Build a word-frequency table from spans ```python from collections import Counter from pdf_oxide import PdfDocument doc = PdfDocument("report.pdf") words = Counter() for page in range(doc.page_count()): text = doc.extract_text(page) for word in text.split(): words[word.lower().strip(".,;:!?\"'()[]")] += 1 for word, count in words.most_common(20): print(f"{word:20s} {count}") ``` ### Detect bold headings using span metadata ```rust use pdf_oxide::PdfDocument; use pdf_oxide::layout::FontWeight; let mut doc = PdfDocument::open("paper.pdf")?; let spans = doc.extract_spans(0)?; let headings: Vec<_> = spans.iter() .filter(|s| s.font_weight == FontWeight::Bold && s.font_size > 14.0) .collect(); for h in headings { println!("Heading: '{}' ({}pt)", h.text, h.font_size); } ``` ### Export per-character data to CSV ```python import csv from pdf_oxide import PdfDocument doc = PdfDocument("report.pdf") chars = doc.extract_chars(0) with open("characters.csv", "w", newline="") as f: writer = csv.writer(f) writer.writerow(["char", "x", "y", "width", "height", "font", "size"]) for ch in chars: writer.writerow([ ch.char, ch.bbox[0], ch.bbox[1], ch.bbox[2], ch.bbox[3], ch.font_name, ch.font_size, ]) ``` --- ## Related Pages - [Markdown Conversion](/docs/extraction/markdown) -- Convert text to structured Markdown - [HTML Conversion](/docs/extraction/html) -- Convert text to HTML with formatting - [Text Search](/docs/extraction/search) -- Search extracted text with regex - [Metadata & XMP](/docs/extraction/metadata) -- Read document-level metadata --- # Image Extraction PDF Oxide extracts images from PDF pages by parsing the content stream, resolving XObject references via `Do` operators, recursing into nested Form XObjects, and decoding inline images. Use `extract_images()` to get image objects in memory, or `extract_images_to_files()` to save them directly to disk as PNG or JPEG files. Since v0.3.5, image extraction processes the full page content stream rather than only scanning the XObject dictionary. This correctly handles images placed via `Do` operators, nested Form XObjects with cycle detection, and inline images embedded with `BI`/`ID`/`EI` sequences. ## Quick Example **Python** ```python from pdf_oxide import PdfDocument doc = PdfDocument("report.pdf") page = doc.page(0) for img in page.images(): print(f"{img.width}x{img.height}") ``` **Rust** ```rust use pdf_oxide::PdfDocument; let mut doc = PdfDocument::open("report.pdf")?; let images = doc.extract_images(0)?; for img in &images { println!("{}x{} {:?}", img.width(), img.height(), img.color_space()); } ``` --- ## API Reference ### `extract_images(page_index) -> Vec` Extract all images from a page. Parses the page content stream to find: 1. **XObject images** referenced via `Do` operators 2. **Form XObjects** containing nested images (recursive, with cycle detection) 3. **Inline images** embedded with `BI`/`ID`/`EI` sequences CTM (Current Transformation Matrix) tracking provides bounding boxes for each image. | Parameter | Type | Description | |-----------|------|-------------| | `page_index` | `int` / `usize` | Zero-based page index | **Returns:** A vector of `PdfImage` objects. #### PdfImage Fields and Methods | Method / Field | Type | Description | |----------------|------|-------------| | `width()` | `u32` | Image width in pixels | | `height()` | `u32` | Image height in pixels | | `color_space()` | `&ColorSpace` | Color space (DeviceRGB, DeviceGray, DeviceCMYK, etc.) | | `bits_per_component()` | `u8` | Bits per color component (typically 8) | | `data()` | `&ImageData` | Raw image data (JPEG bytes or raw pixels) | | `bbox()` | `Option<&Rect>` | Bounding box in PDF user space (if CTM was tracked) | | `save_as_png(path)` | `Result<()>` | Save image as PNG file | | `save_as_jpeg(path)` | `Result<()>` | Save image as JPEG file | | `to_png_bytes()` | `Result>` | Encode as PNG bytes in memory | | `to_jpeg_bytes()` | `Result>` | Encode as JPEG bytes in memory | #### ColorSpace Variants | Variant | Description | |---------|-------------| | `DeviceRGB` | 3-channel RGB | | `DeviceGray` | Single-channel grayscale | | `DeviceCMYK` | 4-channel CMYK | | `Indexed` | Palette-based color | | `ICCBased` | ICC profile-based color | | `CalGray` | Calibrated grayscale | | `CalRGB` | Calibrated RGB | | `Lab` | CIE L*a*b* color | #### ImageData Variants | Variant | Description | |---------|-------------| | `Jpeg(Vec)` | JPEG-compressed data (DCT pass-through) | | `Raw { pixels, format }` | Decoded pixel data with `PixelFormat` (RGB, Gray, CMYK, RGBA) | **Rust** ```rust let mut doc = PdfDocument::open("report.pdf")?; let images = doc.extract_images(0)?; for (i, image) in images.iter().enumerate() { println!( "Image {}: {}x{} {:?} {}bpc", i, image.width(), image.height(), image.color_space(), image.bits_per_component(), ); if let Some(bbox) = image.bbox() { println!(" Position: ({:.1}, {:.1})", bbox.x, bbox.y); } image.save_as_png(&format!("output/image_{}.png", i))?; } ``` --- ### `extract_images_to_files(page_index, output_dir, prefix, start_index) -> Vec` Extract images from a page and save them directly to files. JPEG images are saved in their original format (zero re-encoding loss); other images are saved as PNG. | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `page_index` | `usize` | -- | Zero-based page index | | `output_dir` | `impl AsRef` | -- | Directory to save images (created if absent) | | `prefix` | `Option<&str>` | `"img"` | Filename prefix | | `start_index` | `Option` | `1` | Starting index for filenames | **Returns:** A vector of `ExtractedImageRef` describing saved files. #### ExtractedImageRef Fields | Field | Type | Description | |-------|------|-------------| | `filename` | `String` | Saved filename (e.g., `"img_001.png"`) | | `format` | `ImageFormat` | `Png` or `Jpeg` | | `width` | `u32` | Image width in pixels | | `height` | `u32` | Image height in pixels | **Rust** ```rust let mut doc = PdfDocument::open("report.pdf")?; let refs = doc.extract_images_to_files(0, "output/images", Some("fig"), Some(1))?; for img_ref in &refs { println!("Saved: {} ({}x{}, {:?})", img_ref.filename, img_ref.width, img_ref.height, img_ref.format); } ``` --- ## Advanced Examples ### Extract all images from all pages ```rust use pdf_oxide::PdfDocument; use std::path::Path; let mut doc = PdfDocument::open("book.pdf")?; let page_count = doc.page_count()?; let mut total = 0; for page in 0..page_count { let refs = doc.extract_images_to_files( page, "output/images", Some(&format!("page{}", page + 1)), Some(1), )?; total += refs.len(); println!("Page {}: {} images", page + 1, refs.len()); } println!("Total: {} images extracted", total); ``` ### Get image bytes in memory (no disk I/O) ```rust let mut doc = PdfDocument::open("report.pdf")?; let images = doc.extract_images(0)?; for image in &images { let png_bytes = image.to_png_bytes()?; println!("PNG size: {} bytes", png_bytes.len()); // Use png_bytes with an HTTP response, database, etc. } ``` ### Filter images by size ```rust let mut doc = PdfDocument::open("report.pdf")?; let images = doc.extract_images(0)?; // Only keep images larger than 100x100 pixels let large_images: Vec<_> = images.iter() .filter(|img| img.width() > 100 && img.height() > 100) .collect(); println!("{} large images on page 1", large_images.len()); for img in &large_images { println!(" {}x{} {:?}", img.width(), img.height(), img.color_space()); } ``` ### Distinguish JPEG pass-through from re-encoded images ```rust use pdf_oxide::extractors::ImageData; let mut doc = PdfDocument::open("report.pdf")?; let images = doc.extract_images(0)?; for (i, image) in images.iter().enumerate() { match image.data() { ImageData::Jpeg(bytes) => { // Original JPEG data -- save directly for zero quality loss std::fs::write(format!("image_{}.jpg", i), bytes)?; println!("Image {}: JPEG pass-through ({} bytes)", i, bytes.len()); } ImageData::Raw { pixels, format } => { // Raw pixels -- must encode to a file format image.save_as_png(&format!("image_{}.png", i))?; println!("Image {}: raw {:?} ({}x{})", i, format, image.width(), image.height()); } } } ``` --- ## Related Pages - [Text Extraction](/docs/extraction/text) -- Extract text alongside images - [HTML Conversion](/docs/extraction/html) -- Embed extracted images in HTML output - [Markdown Conversion](/docs/extraction/markdown) -- Include images in Markdown output --- # Markdown Conversion PDF Oxide converts PDF pages to clean, readable Markdown with automatic heading detection based on font size clustering, bold/italic formatting, table preservation, and optional image embedding. Use `to_markdown()` for a single page or `to_markdown_all()` to convert the entire document in one call. The conversion pipeline works by extracting text spans, clustering them into lines, detecting heading levels from font size distribution, grouping paragraphs, and emitting Markdown syntax. For Tagged PDFs, the structure tree is consulted for reading order. ## Quick Example **Python** ```python from pdf_oxide import PdfDocument doc = PdfDocument("paper.pdf") md = doc.to_markdown(0, detect_headings=True) print(md) ``` **Rust** ```rust use pdf_oxide::PdfDocument; use pdf_oxide::converters::ConversionOptions; let mut doc = PdfDocument::open("paper.pdf")?; let options = ConversionOptions { detect_headings: true, ..Default::default() }; let md = doc.to_markdown(0, &options)?; println!("{}", md); ``` --- ## API Reference ### `to_markdown(page_index, ...) -> str` Convert a single page to Markdown. **Python Signature** ```python doc.to_markdown( page: int, preserve_layout: bool = False, detect_headings: bool = True, include_images: bool = True, image_output_dir: str | None = None, embed_images: bool = True, ) -> str ``` **Rust Signature** ```rust pub fn to_markdown( &mut self, page_index: usize, options: &ConversionOptions, ) -> Result ``` | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `page_index` | `int` / `usize` | -- | Zero-based page index | | `preserve_layout` | `bool` | `false` | Preserve visual layout positioning | | `detect_headings` | `bool` | `true` | Detect headings based on font size and weight | | `include_images` | `bool` | `true` | Include images in output | | `image_output_dir` | `str` / `None` | `None` | Directory to save extracted images | | `embed_images` | `bool` | `true` | Embed images as base64 data URIs | **Returns:** Markdown string for the page. --- ### `to_markdown_all(...) -> str` Convert all pages to Markdown, separated by horizontal rules (`---`). **Python Signature** ```python doc.to_markdown_all( preserve_layout: bool = False, detect_headings: bool = True, include_images: bool = True, image_output_dir: str | None = None, embed_images: bool = True, ) -> str ``` **Rust Signature** ```rust pub fn to_markdown_all( &mut self, options: &ConversionOptions, ) -> Result ``` | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `preserve_layout` | `bool` | `false` | Preserve visual layout | | `detect_headings` | `bool` | `true` | Detect headings | | `include_images` | `bool` | `true` | Include images | | `image_output_dir` | `str` / `None` | `None` | Image output directory | | `embed_images` | `bool` | `true` | Embed images as base64 | **Returns:** Markdown string for all pages joined with `---` separators. --- ### `to_markdown_with_ocr(page_index, model_path, options) -> str` Convert a page to Markdown with OCR fallback for scanned pages. When the page has little or no extractable text, OCR is used to recognize text from the rendered page image. Requires the `ocr` feature. | Parameter | Type | Description | |-----------|------|-------------| | `page_index` | `usize` | Zero-based page index | | `model_path` | `&str` | Path to the OCR model files | | `options` | `&ConversionOptions` | Conversion options | **Rust** ```rust let mut doc = PdfDocument::open("scanned.pdf")?; let options = ConversionOptions { detect_headings: true, ..Default::default() }; let md = doc.to_markdown_with_ocr(0, "/path/to/models", &options)?; println!("{}", md); ``` --- ### ConversionOptions The `ConversionOptions` struct controls all conversion behavior. | Field | Type | Default | Description | |-------|------|---------|-------------| | `preserve_layout` | `bool` | `false` | Preserve visual layout with positioning | | `detect_headings` | `bool` | `true` | Auto-detect headings from font size clusters | | `extract_tables` | `bool` | `false` | Extract tables (experimental) | | `include_images` | `bool` | `true` | Include images in output | | `image_output_dir` | `Option` | `None` | Save images to this directory | | `embed_images` | `bool` | `true` | Embed images as base64 data URIs | | `reading_order_mode` | `ReadingOrderMode` | `Auto` | How to determine reading order | | `bold_marker_behavior` | `BoldMarkerBehavior` | `Conservative` | Bold marker application strategy | --- ## How It Works The Markdown conversion pipeline operates in several stages: 1. **Text Extraction** -- Extracts `TextSpan` objects from the page content stream, capturing text, position, font, size, weight, and color. 2. **Character Clustering** -- Groups characters into words based on inter-character gaps, then words into lines based on vertical proximity. 3. **Reading Order** -- Determines reading order using either the Tagged PDF structure tree (preferred) or a graph-based spatial analysis of text block positions. 4. **Heading Detection** -- When `detect_headings` is enabled, clusters font sizes across the page to identify heading levels. Larger and bolder text is mapped to `#`, `##`, `###` headings. 5. **Formatting** -- Applies bold (`**text**`) and italic (`*text*`) markers based on font weight and style metadata. 6. **Table Detection** -- Identifies tabular layouts using spatial analysis of aligned text blocks and emits GFM-style Markdown tables. 7. **Whitespace Cleanup** -- Normalizes spacing, removes redundant blank lines, and ensures consistent paragraph breaks. --- ## Advanced Examples ### Convert entire PDF to a Markdown file ```python from pdf_oxide import PdfDocument doc = PdfDocument("book.pdf") md = doc.to_markdown_all(detect_headings=True) with open("book.md", "w", encoding="utf-8") as f: f.write(md) ``` ### Convert with images saved to a directory ```rust use pdf_oxide::PdfDocument; use pdf_oxide::converters::ConversionOptions; let mut doc = PdfDocument::open("report.pdf")?; let options = ConversionOptions { detect_headings: true, include_images: true, embed_images: false, image_output_dir: Some("output/images".to_string()), ..Default::default() }; let md = doc.to_markdown_all(&options)?; std::fs::write("output/report.md", &md)?; ``` ### Page-by-page conversion with progress ```python from pdf_oxide import PdfDocument doc = PdfDocument("report.pdf") pages = doc.page_count() parts = [] for i in range(pages): md = doc.to_markdown(i, detect_headings=True) parts.append(md) print(f"Converted page {i + 1}/{pages}") full_md = "\n\n---\n\n".join(parts) with open("report.md", "w") as f: f.write(full_md) ``` ### Disable heading detection for flat text ```python doc = PdfDocument("form.pdf") md = doc.to_markdown(0, detect_headings=False) # All text rendered as paragraphs, no # headings ``` --- ## Related Pages - [Text Extraction](/docs/extraction/text) -- Raw text and span extraction - [HTML Conversion](/docs/extraction/html) -- Convert to HTML instead of Markdown - [Image Extraction](/docs/extraction/images) -- Extract images separately --- # HTML Conversion PDF Oxide converts PDF pages to structured HTML with heading detection, font styling, and optional CSS-based layout preservation. Use `to_html()` for a single page or `to_html_all()` to convert the entire document. When `preserve_layout` is enabled, elements are positioned with CSS absolute coordinates matching the original PDF layout. When disabled, the output is semantic HTML with natural flow. ## Quick Example **Python** ```python from pdf_oxide import PdfDocument doc = PdfDocument("report.pdf") html = doc.to_html(0, detect_headings=True) print(html) ``` **Rust** ```rust use pdf_oxide::PdfDocument; use pdf_oxide::converters::ConversionOptions; let mut doc = PdfDocument::open("report.pdf")?; let options = ConversionOptions { detect_headings: true, ..Default::default() }; let html = doc.to_html(0, &options)?; println!("{}", html); ``` --- ## API Reference ### `to_html(page_index, ...) -> str` Convert a single page to HTML. **Python Signature** ```python doc.to_html( page: int, preserve_layout: bool = False, detect_headings: bool = True, include_images: bool = True, image_output_dir: str | None = None, embed_images: bool = True, ) -> str ``` **Rust Signature** ```rust pub fn to_html( &mut self, page_index: usize, options: &ConversionOptions, ) -> Result ``` | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `page_index` | `int` / `usize` | -- | Zero-based page index | | `preserve_layout` | `bool` | `false` | Use CSS absolute positioning to match PDF layout | | `detect_headings` | `bool` | `true` | Auto-detect heading levels from font sizes | | `include_images` | `bool` | `true` | Include images in the HTML output | | `image_output_dir` | `str` / `None` | `None` | Directory to save extracted images | | `embed_images` | `bool` | `true` | Embed images as base64 data URIs | **Returns:** HTML string for the page. When `preserve_layout` is `true`, the output uses `
` elements with absolute CSS positioning: ```html
Introduction
``` When `preserve_layout` is `false`, the output uses semantic elements: ```html

Introduction

This report examines the quarterly results...

``` --- ### `to_html_all(...) -> str` Convert all pages to HTML. Each page is wrapped in a `
` element. **Python Signature** ```python doc.to_html_all( preserve_layout: bool = False, detect_headings: bool = True, include_images: bool = True, image_output_dir: str | None = None, embed_images: bool = True, ) -> str ``` **Rust Signature** ```rust pub fn to_html_all( &mut self, options: &ConversionOptions, ) -> Result ``` | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `preserve_layout` | `bool` | `false` | Use CSS absolute positioning | | `detect_headings` | `bool` | `true` | Detect headings | | `include_images` | `bool` | `true` | Include images | | `image_output_dir` | `str` / `None` | `None` | Image output directory | | `embed_images` | `bool` | `true` | Embed images as base64 | **Returns:** HTML string for all pages. --- ### ConversionOptions See the [Markdown Conversion](/docs/extraction/markdown) page for the full `ConversionOptions` reference. The same options struct is shared between Markdown and HTML conversion. --- ## Advanced Examples ### Create a complete HTML file ```python from pdf_oxide import PdfDocument doc = PdfDocument("report.pdf") body = doc.to_html_all(detect_headings=True) html = f""" Report {body} """ with open("report.html", "w", encoding="utf-8") as f: f.write(html) ``` ### Layout-preserved HTML for visual fidelity ```rust use pdf_oxide::PdfDocument; use pdf_oxide::converters::ConversionOptions; let mut doc = PdfDocument::open("brochure.pdf")?; let options = ConversionOptions { preserve_layout: true, detect_headings: false, // layout mode uses exact positioning include_images: true, embed_images: true, ..Default::default() }; let html = doc.to_html(0, &options)?; std::fs::write("brochure.html", &html)?; ``` ### Convert with external image files ```python from pdf_oxide import PdfDocument doc = PdfDocument("report.pdf") html = doc.to_html_all( detect_headings=True, include_images=True, embed_images=False, image_output_dir="output/images", ) with open("output/report.html", "w") as f: f.write(html) # Images saved as output/images/img_001.png, img_002.jpg, etc. ``` ### Page-by-page conversion with custom wrappers ```rust use pdf_oxide::PdfDocument; use pdf_oxide::converters::ConversionOptions; let mut doc = PdfDocument::open("book.pdf")?; let options = ConversionOptions::default(); let page_count = doc.page_count()?; let mut pages_html = Vec::new(); for i in 0..page_count { let html = doc.to_html(i, &options)?; pages_html.push(format!( "
\n{}\n
", i + 1, html )); } let full = pages_html.join("\n"); std::fs::write("output.html", &full)?; ``` --- ## Related Pages - [Markdown Conversion](/docs/extraction/markdown) -- Convert to Markdown instead of HTML - [Text Extraction](/docs/extraction/text) -- Extract raw text without formatting - [Image Extraction](/docs/extraction/images) -- Extract images separately --- # Form Data Extraction PDF Oxide extracts interactive form fields (AcroForms) from PDF documents, including text fields, checkboxes, radio buttons, choice fields, and signatures. Extracted form data can be exported to FDF or XFDF format for interchange. XFA forms (XML Forms Architecture) can be analyzed and converted as well. Use `FormExtractor::extract_fields()` in Rust or `doc.page().form_fields` via the DOM API to read form field names, types, and current values. ## Quick Example **Python** ```python from pdf_oxide import PdfDocument doc = PdfDocument("form.pdf") page = doc.page(0) for field in page.form_fields(): print(f"{field.name}: {field.value}") ``` **Rust** ```rust use pdf_oxide::extractors::FormExtractor; use pdf_oxide::PdfDocument; let mut doc = PdfDocument::open("form.pdf")?; let fields = FormExtractor::extract_fields(&mut doc)?; for field in &fields { println!("{}: {:?}", field.name, field.value); } ``` --- ## API Reference ### `FormExtractor::extract_fields(doc) -> Vec` Extract all form fields from a PDF document's AcroForm dictionary (ISO 32000-1:2008, Section 12.7). Traverses the field hierarchy, resolving inherited attributes and computing full qualified names. | Parameter | Type | Description | |-----------|------|-------------| | `doc` | `&mut PdfDocument` | The PDF document to extract fields from | **Returns:** A vector of `FormField` objects. #### FormField Fields | Field | Type | Description | |-------|------|-------------| | `name` | `String` | Field name from `/T` key | | `full_name` | `String` | Fully qualified name (dot-separated for hierarchical fields) | | `field_type` | `FieldType` | Field type (Button, Text, Choice, Signature, Unknown) | | `value` | `FieldValue` | Current field value | | `tooltip` | `Option` | Tooltip/description from `/TU` key | | `bounds` | `Option<[f64; 4]>` | Bounding box [x1, y1, x2, y2] from `/Rect` | | `object_ref` | `Option` | Object reference for updates | | `flags` | `Option` | Field flags (ReadOnly, Required, NoExport, etc.) | | `default_value` | `Option` | Default value from `/DV` key | | `max_length` | `Option` | Maximum length for text fields | | `alignment` | `Option` | Text alignment (0=left, 1=center, 2=right) | | `default_appearance` | `Option` | Default appearance string from `/DA` | | `border_style` | `Option` | Border style from `/BS` | | `appearance_chars` | `Option` | Appearance characteristics from `/MK` | #### FieldType Variants | Variant | Description | |---------|-------------| | `Button` | Checkbox, radio button, or push button (`/Btn`) | | `Text` | Single or multi-line text field (`/Tx`) | | `Choice` | List box or combo box (`/Ch`) | | `Signature` | Digital signature field (`/Sig`) | | `Unknown(String)` | Unrecognized field type | #### FieldValue Variants | Variant | Description | |---------|-------------| | `Text(String)` | Text string value | | `Boolean(bool)` | Boolean value (checkboxes) | | `Name(String)` | Name value (radio buttons, choice fields) | | `Array(Vec)` | Multiple values (multi-select list boxes) | | `None` | No value present | --- ### `FormExtractor::export_fdf(doc, fields) -> Result>` Export form field data as FDF (Forms Data Format). FDF is a lightweight format for exchanging form data between applications (ISO 32000-1:2008, Section 12.7.8). | Parameter | Type | Description | |-----------|------|-------------| | `doc` | `&mut PdfDocument` | Source PDF document | | `fields` | `Vec` | Fields to export | **Returns:** FDF file content as bytes. **Rust** ```rust use pdf_oxide::extractors::FormExtractor; let mut doc = PdfDocument::open("form.pdf")?; let fields = FormExtractor::extract_fields(&mut doc)?; let fdf_bytes = FormExtractor::export_fdf(&mut doc, fields)?; std::fs::write("form_data.fdf", &fdf_bytes)?; ``` --- ### `FormExtractor::export_xfdf(doc, fields) -> Result` Export form field data as XFDF (XML Forms Data Format). XFDF is the XML-based equivalent of FDF and is widely supported for form data interchange. | Parameter | Type | Description | |-----------|------|-------------| | `doc` | `&mut PdfDocument` | Source PDF document | | `fields` | `Vec` | Fields to export | **Returns:** XFDF content as an XML string. **Rust** ```rust use pdf_oxide::extractors::FormExtractor; let mut doc = PdfDocument::open("form.pdf")?; let fields = FormExtractor::extract_fields(&mut doc)?; let xfdf = FormExtractor::export_xfdf(&mut doc, fields)?; std::fs::write("form_data.xfdf", &xfdf)?; ``` --- ### `analyze_xfa_document(doc) -> Result` Analyze XFA (XML Forms Architecture) form content. XFA forms use XML-based templates rather than AcroForm fields and are common in government and enterprise forms. | Parameter | Type | Description | |-----------|------|-------------| | `doc` | `&mut PdfDocument` | The PDF document to analyze | **Returns:** An `XfaAnalysis` struct with details about the XFA form structure. **Rust** ```rust use pdf_oxide::xfa::analyze_xfa_document; let mut doc = PdfDocument::open("xfa-form.pdf")?; let analysis = analyze_xfa_document(&mut doc)?; println!("XFA form detected: {} fields", analysis.fields.len()); for field in &analysis.fields { println!(" {} ({:?})", field.name, field.field_type); } ``` --- ### DocumentEditor Form API The `DocumentEditor` provides higher-level form field access through `FormFieldWrapper`, which bridges the read-side `FormField` and write-side `FormFieldWidget` interfaces. #### `editor.get_form_fields() -> Vec` Get all form fields wrapped for reading and modification. #### `editor.get_form_field_value(name) -> Option` Get the current value of a specific field by name. #### `editor.set_form_field_value(name, value)` Set the value of an existing form field. **Rust** ```rust use pdf_oxide::editor::{DocumentEditor, EditableDocument, FormFieldValue}; let mut editor = DocumentEditor::open("form.pdf")?; let fields = editor.get_form_fields()?; for field in &fields { println!("{}: {:?}", field.name(), field.value()); } // Modify a field editor.set_form_field_value("full_name", FormFieldValue::Text("Jane Doe".into()))?; editor.set_form_field_value("agree", FormFieldValue::Boolean(true))?; editor.save("filled_form.pdf")?; ``` --- ## Advanced Examples ### Extract and display all form fields with metadata ```rust use pdf_oxide::extractors::{FormExtractor, FieldType}; let mut doc = PdfDocument::open("application.pdf")?; let fields = FormExtractor::extract_fields(&mut doc)?; for field in &fields { let type_str = match &field.field_type { FieldType::Button => "Button", FieldType::Text => "Text", FieldType::Choice => "Choice", FieldType::Signature => "Signature", FieldType::Unknown(s) => s.as_str(), }; println!("[{}] {} = {:?}", type_str, field.full_name, field.value); if let Some(tooltip) = &field.tooltip { println!(" Tooltip: {}", tooltip); } if let Some(bounds) = &field.bounds { println!(" Bounds: [{:.1}, {:.1}, {:.1}, {:.1}]", bounds[0], bounds[1], bounds[2], bounds[3]); } } ``` ### Check required fields are filled ```rust use pdf_oxide::extractors::{FormExtractor, FieldValue}; let mut doc = PdfDocument::open("form.pdf")?; let fields = FormExtractor::extract_fields(&mut doc)?; let required_empty: Vec<_> = fields.iter() .filter(|f| { // Bit 1 of flags = Required f.flags.map_or(false, |flags| flags & 0x02 != 0) && matches!(f.value, FieldValue::None | FieldValue::Text(ref s) if s.is_empty()) }) .collect(); if !required_empty.is_empty() { println!("Missing required fields:"); for f in &required_empty { println!(" - {}", f.full_name); } } ``` --- ## Related Pages - [Annotation Extraction](/docs/extraction/annotations) -- Access annotations alongside form fields - [Text Extraction](/docs/extraction/text) -- Extract text content from pages - [Metadata & XMP](/docs/extraction/metadata) -- Read document-level properties --- # Annotation Extraction PDF Oxide provides access to all annotation types defined in the PDF specification (ISO 32000-1:2008, Section 12.5), including text notes, hyperlinks, highlights, stamps, ink annotations, and more. The document outline (bookmarks) is also accessible for building navigation structures. Use `get_annotations()` on `PdfDocument` for raw annotation data, or the `PdfPage` DOM API for a unified `AnnotationWrapper` interface that supports both reading and writing. ## Quick Example **Python** ```python from pdf_oxide import PdfDocument doc = PdfDocument("annotated.pdf") page = doc.page(0) for annot in page.annotations(): print(f"{annot.subtype}: {annot.contents}") ``` **Rust** ```rust use pdf_oxide::PdfDocument; let mut doc = PdfDocument::open("annotated.pdf")?; let annotations = doc.get_annotations(0)?; for annot in &annotations { println!("{:?}: {:?}", annot.subtype_enum, annot.contents); } ``` --- ## API Reference ### `get_annotations(page_index) -> Vec` Extract raw annotations from a specific page. Returns all annotation types present on the page. | Parameter | Type | Description | |-----------|------|-------------| | `page_index` | `usize` | Zero-based page index | **Returns:** A vector of `Annotation` objects. #### Annotation Fields | Field | Type | Description | |-------|------|-------------| | `annotation_type` | `String` | Always `"Annot"` | | `subtype` | `Option` | Raw subtype string (e.g., `"Text"`, `"Highlight"`) | | `subtype_enum` | `AnnotationSubtype` | Parsed subtype enum | | `contents` | `Option` | Text contents of the annotation | | `rect` | `Option<[f64; 4]>` | Bounding rectangle [x1, y1, x2, y2] | | `author` | `Option` | Author/creator (`/T` entry) | | `creation_date` | `Option` | Creation date | | `modification_date` | `Option` | Last modification date | | `subject` | `Option` | Subject of the annotation | | `destination` | `Option` | Link destination (for Link annotations) | | `action` | `Option` | Link action (for Link annotations) | | `color` | `Option>` | Annotation color components | | `flags` | `Option` | Annotation flags (invisible, hidden, print, etc.) | #### AnnotationSubtype Variants | Variant | Description | |---------|-------------| | `Text` | Sticky note annotation | | `Link` | Hyperlink annotation | | `FreeText` | Text box annotation | | `Line` | Line shape annotation | | `Square` | Rectangle shape annotation | | `Circle` | Ellipse shape annotation | | `Polygon` | Polygon shape annotation | | `PolyLine` | Polyline shape annotation | | `Highlight` | Text highlight markup | | `Underline` | Text underline markup | | `Squiggly` | Squiggly underline markup | | `StrikeOut` | Strikethrough markup | | `Stamp` | Rubber stamp annotation | | `Ink` | Freehand drawing annotation | | `Popup` | Pop-up note associated with another annotation | | `FileAttachment` | Embedded file annotation | | `Sound` | Sound annotation | | `Movie` | Movie annotation | | `Screen` | Screen annotation | | `Widget` | Form field widget | | `PrinterMark` | Printer's mark annotation | | `TrapNet` | Trap network annotation | | `Watermark` | Watermark annotation | | `ThreeDimensional` | 3D annotation | | `Redact` | Redaction annotation | | `Caret` | Caret annotation (insertion point) | | `RichMedia` | Rich media annotation | | `Unknown` | Unrecognized annotation type | --- ### `get_outline() -> Option>` Get the document outline (bookmarks) if present. Returns a hierarchical tree of outline items that can be used for document navigation. **Returns:** - `Some(Vec)` -- Bookmarks found and parsed - `None` -- No bookmarks in the document #### OutlineItem Fields | Field | Type | Description | |-------|------|-------------| | `title` | `String` | Bookmark title text | | `dest` | `Option` | Navigation destination | | `children` | `Vec` | Nested child bookmarks | #### Destination Variants | Variant | Description | |---------|-------------| | `PageIndex(usize)` | Direct page reference (0-based index) | | `Named(String)` | Named destination identifier | **Rust** ```rust let mut doc = PdfDocument::open("book.pdf")?; if let Some(outline) = doc.get_outline()? { for item in &outline { println!(" {}", item.title); for child in &item.children { println!(" {}", child.title); } } } else { println!("No bookmarks found."); } ``` --- ### PdfPage Annotation API (DOM) The `PdfPage` object from the `DocumentEditor` provides a higher-level `AnnotationWrapper` interface that supports both reading existing annotations and adding new ones. #### `page.annotations() -> &[AnnotationWrapper]` Get all annotations on the page as wrapped objects. #### `page.find_annotations_by_type(subtype) -> Vec<&AnnotationWrapper>` Find annotations of a specific type. #### `page.add_annotation(annotation)` Add a new annotation to the page. #### `page.remove_annotation(index) -> Option` Remove an annotation by index. #### `page.find_annotations_in_region(rect) -> Vec<&AnnotationWrapper>` Find annotations whose bounding boxes intersect a given region. #### AnnotationWrapper Methods | Method | Returns | Description | |--------|---------|-------------| | `id()` | `AnnotationId` | Unique session ID | | `subtype()` | `AnnotationSubtype` | Annotation type | | `rect()` | `Rect` | Bounding rectangle | | `contents()` | `Option<&str>` | Text contents | | `color()` | `Option<(f32, f32, f32)>` | RGB color (0.0--1.0) | | `is_modified()` | `bool` | Whether annotation has been changed | **Python** ```python doc = PdfDocument("annotated.pdf") page = doc.page(0) # List all annotations for annot in page.annotations(): print(f"[{annot.subtype}] {annot.contents} at {annot.rect}") # Find highlights highlights = [a for a in page.annotations() if a.subtype == "Highlight"] print(f"Found {len(highlights)} highlights") ``` **Rust** ```rust use pdf_oxide::editor::{DocumentEditor, EditableDocument}; use pdf_oxide::annotation_types::AnnotationSubtype; let mut editor = DocumentEditor::open("annotated.pdf")?; let page = editor.get_page(0)?; // Find all highlight annotations let highlights = page.find_annotations_by_type(AnnotationSubtype::Highlight); for h in &highlights { println!("Highlight at {:?}: {:?}", h.rect(), h.contents()); } ``` --- ## Advanced Examples ### Build a table of contents from bookmarks ```rust use pdf_oxide::PdfDocument; use pdf_oxide::outline::Destination; let mut doc = PdfDocument::open("book.pdf")?; fn print_toc(items: &[pdf_oxide::outline::OutlineItem], depth: usize) { for item in items { let indent = " ".repeat(depth); let page = match &item.dest { Some(Destination::PageIndex(p)) => format!("page {}", p + 1), Some(Destination::Named(n)) => format!("dest '{}'", n), None => "no dest".to_string(), }; println!("{}{} ({})", indent, item.title, page); print_toc(&item.children, depth + 1); } } if let Some(outline) = doc.get_outline()? { println!("Table of Contents:"); print_toc(&outline, 0); } ``` ### Extract all comments (Text annotations) ```rust use pdf_oxide::PdfDocument; use pdf_oxide::annotation_types::AnnotationSubtype; let mut doc = PdfDocument::open("reviewed.pdf")?; let page_count = doc.page_count()?; for page_idx in 0..page_count { let annotations = doc.get_annotations(page_idx)?; let comments: Vec<_> = annotations.iter() .filter(|a| a.subtype_enum == AnnotationSubtype::Text) .collect(); if !comments.is_empty() { println!("Page {}:", page_idx + 1); for c in &comments { let author = c.author.as_deref().unwrap_or("Unknown"); let text = c.contents.as_deref().unwrap_or(""); println!(" [{}] {}", author, text); } } } ``` ### Extract all hyperlinks ```rust use pdf_oxide::PdfDocument; use pdf_oxide::annotation_types::AnnotationSubtype; let mut doc = PdfDocument::open("report.pdf")?; let annotations = doc.get_annotations(0)?; let links: Vec<_> = annotations.iter() .filter(|a| a.subtype_enum == AnnotationSubtype::Link) .collect(); for link in &links { if let Some(ref action) = link.action { println!("Link: {:?}", action); } if let Some(ref dest) = link.destination { println!("Internal link: {:?}", dest); } } ``` --- ## Related Pages - [Form Data Extraction](/docs/extraction/forms) -- Extract form fields (Widget annotations) - [Text Extraction](/docs/extraction/text) -- Extract text content from pages - [Metadata & XMP](/docs/extraction/metadata) -- Read document properties and bookmarks --- # Metadata & XMP PDF Oxide reads document-level metadata from multiple sources: the PDF header (version), the trailer and catalog dictionaries, XMP metadata streams (ISO 16684), and page label definitions. The `XmpExtractor` parses the Dublin Core, XMP Core, PDF, and XMP Rights namespaces, plus any custom properties. Use `version()` and `catalog()` for basic document properties, `XmpExtractor::extract()` for rich metadata, and `PageLabelExtractor` for page numbering schemes. ## Quick Example **Python** ```python from pdf_oxide import PdfDocument doc = PdfDocument("report.pdf") major, minor = doc.version() print(f"PDF {major}.{minor}, {doc.page_count()} pages") ``` **Rust** ```rust use pdf_oxide::PdfDocument; let mut doc = PdfDocument::open("report.pdf")?; let (major, minor) = doc.version(); println!("PDF {}.{}", major, minor); println!("Pages: {}", doc.page_count()?); ``` --- ## API Reference ### `version() -> (u8, u8)` Get the PDF version from the file header. **Returns:** A tuple of (major, minor), e.g., `(1, 7)` for PDF 1.7 or `(2, 0)` for PDF 2.0. --- ### `catalog() -> Result` Get the document catalog dictionary. The catalog is the root of the PDF object hierarchy and contains references to the page tree, outlines, names, and other document-level structures. **Rust** ```rust let mut doc = PdfDocument::open("report.pdf")?; let catalog = doc.catalog()?; if let Some(dict) = catalog.as_dict() { for (key, _) in dict { println!("Catalog key: {}", key); } } ``` --- ### `trailer() -> &Object` Get the document trailer dictionary. The trailer contains the cross-reference table location, document ID, encryption dictionary reference, and info dictionary reference. **Rust** ```rust let doc = PdfDocument::open("report.pdf")?; let trailer = doc.trailer(); println!("Trailer: {:?}", trailer); ``` --- ### `XmpExtractor::extract(doc) -> Result>` Extract XMP (Extensible Metadata Platform) metadata from the document's metadata stream. XMP provides richer metadata than the traditional Info dictionary, using standard XML namespaces. | Parameter | Type | Description | |-----------|------|-------------| | `doc` | `&mut PdfDocument` | The PDF document | **Returns:** `Some(XmpMetadata)` if XMP data is present, `None` otherwise. #### XmpMetadata Fields **Dublin Core namespace (dc:)** | Field | Type | Description | |-------|------|-------------| | `dc_title` | `Option` | Document title | | `dc_creator` | `Vec` | Authors/creators list | | `dc_description` | `Option` | Document description | | `dc_subject` | `Vec` | Subject keywords | | `dc_language` | `Option` | Document language (e.g., `"en-US"`) | | `dc_rights` | `Option` | Copyright statement | | `dc_format` | `Option` | MIME format (e.g., `"application/pdf"`) | **XMP Core namespace (xmp:)** | Field | Type | Description | |-------|------|-------------| | `xmp_creator_tool` | `Option` | Tool used to create the document | | `xmp_create_date` | `Option` | Creation date (ISO 8601) | | `xmp_modify_date` | `Option` | Last modification date | | `xmp_metadata_date` | `Option` | Metadata modification date | **PDF namespace (pdf:)** | Field | Type | Description | |-------|------|-------------| | `pdf_producer` | `Option` | PDF producer application | | `pdf_keywords` | `Option` | Keywords string | | `pdf_version` | `Option` | PDF version from XMP (may differ from header) | | `pdf_trapped` | `Option` | Trapping status | **XMP Rights namespace (xmpRights:)** | Field | Type | Description | |-------|------|-------------| | `xmp_rights_usage_terms` | `Option` | Usage terms | | `xmp_rights_marked` | `Option` | Whether marked with rights | | `xmp_rights_web_statement` | `Option` | Web statement URL | **Other** | Field | Type | Description | |-------|------|-------------| | `custom` | `HashMap` | Custom properties (namespace:property to value) | | `raw_xml` | `Option` | The original XMP XML packet | **Rust** ```rust use pdf_oxide::extractors::xmp::XmpExtractor; let mut doc = PdfDocument::open("report.pdf")?; if let Some(xmp) = XmpExtractor::extract(&mut doc)? { if let Some(title) = &xmp.dc_title { println!("Title: {}", title); } for creator in &xmp.dc_creator { println!("Author: {}", creator); } if let Some(tool) = &xmp.xmp_creator_tool { println!("Created with: {}", tool); } if let Some(date) = &xmp.xmp_create_date { println!("Created: {}", date); } if let Some(producer) = &xmp.pdf_producer { println!("Producer: {}", producer); } } ``` --- ### Pdf Convenience Methods The high-level `Pdf` API provides shortcut methods for common metadata queries. #### `xmp_metadata() -> Result>` Get the full XMP metadata object. #### `xmp_title() -> Result>` Get just the document title from XMP. #### `xmp_creators() -> Result>` Get the list of creators/authors from XMP. **Rust** ```rust use pdf_oxide::api::Pdf; let mut pdf = Pdf::open("report.pdf")?; if let Some(title) = pdf.xmp_title()? { println!("Title: {}", title); } let creators = pdf.xmp_creators()?; for creator in &creators { println!("Author: {}", creator); } ``` --- ### `PageLabelExtractor::extract(doc) -> Result>` Extract page label definitions from the document. Page labels define how page numbers are displayed (e.g., Roman numerals for front matter, Arabic numerals for body). | Parameter | Type | Description | |-----------|------|-------------| | `doc` | `&mut PdfDocument` | The PDF document | **Returns:** A vector of `PageLabelRange` definitions. #### PageLabelRange Fields | Field | Type | Description | |-------|------|-------------| | `start_page` | `usize` | First page index this range applies to | | `style` | `PageLabelStyle` | Numbering style | | `prefix` | `Option` | Label prefix string | | `start_number` | `u32` | Starting number for this range | #### PageLabelStyle Variants | Variant | Description | Example | |---------|-------------|---------| | `DecimalArabic` | Arabic numerals | 1, 2, 3 | | `UppercaseRoman` | Uppercase Roman | I, II, III | | `LowercaseRoman` | Lowercase Roman | i, ii, iii | | `UppercaseLetters` | Uppercase letters | A, B, C | | `LowercaseLetters` | Lowercase letters | a, b, c | | `None` | No numbering (prefix only) | -- | --- ### Pdf Page Label Convenience Methods #### `page_labels() -> Result>` Get all page label range definitions. #### `page_label(page) -> Result` Get the display label for a specific page index. **Rust** ```rust use pdf_oxide::api::Pdf; let mut pdf = Pdf::open("book.pdf")?; // Get all label ranges let ranges = pdf.page_labels()?; for range in &ranges { println!( "Pages from {}: {:?} style, prefix={:?}, start={}", range.start_page, range.style, range.prefix, range.start_number ); } // Get label for a specific page let label = pdf.page_label(0)?; println!("Page 0 label: {}", label); // e.g., "i" or "Cover" ``` --- ## Advanced Examples ### Display complete document metadata ```rust use pdf_oxide::PdfDocument; use pdf_oxide::extractors::xmp::XmpExtractor; let mut doc = PdfDocument::open("report.pdf")?; // Basic info let (major, minor) = doc.version(); println!("PDF Version: {}.{}", major, minor); println!("Pages: {}", doc.page_count()?); // XMP metadata if let Some(xmp) = XmpExtractor::extract(&mut doc)? { println!("\nXMP Metadata:"); println!(" Title: {:?}", xmp.dc_title); println!(" Authors: {:?}", xmp.dc_creator); println!(" Description: {:?}", xmp.dc_description); println!(" Keywords: {:?}", xmp.pdf_keywords); println!(" Creator: {:?}", xmp.xmp_creator_tool); println!(" Producer: {:?}", xmp.pdf_producer); println!(" Created: {:?}", xmp.xmp_create_date); println!(" Modified: {:?}", xmp.xmp_modify_date); println!(" Language: {:?}", xmp.dc_language); println!(" Rights: {:?}", xmp.dc_rights); if !xmp.custom.is_empty() { println!("\n Custom properties:"); for (key, value) in &xmp.custom { println!(" {}: {}", key, value); } } } ``` ### Access raw XMP XML ```rust use pdf_oxide::extractors::xmp::XmpExtractor; let mut doc = PdfDocument::open("report.pdf")?; if let Some(xmp) = XmpExtractor::extract(&mut doc)? { if let Some(xml) = &xmp.raw_xml { std::fs::write("metadata.xml", xml)?; println!("Raw XMP saved ({} bytes)", xml.len()); } } ``` ### Generate page number display strings ```rust use pdf_oxide::api::Pdf; let mut pdf = Pdf::open("thesis.pdf")?; let page_count = pdf.page_count()?; for i in 0..page_count { let label = pdf.page_label(i)?; println!("Physical page {} -> display label '{}'", i + 1, label); } // Example output: // Physical page 1 -> display label 'i' // Physical page 2 -> display label 'ii' // Physical page 3 -> display label 'iii' // Physical page 4 -> display label '1' // Physical page 5 -> display label '2' ``` --- ## Related Pages - [Text Extraction](/docs/extraction/text) -- Extract text content from pages - [Annotation Extraction](/docs/extraction/annotations) -- Access bookmarks and annotations - [Form Data Extraction](/docs/extraction/forms) -- Extract form field data --- # Text Search PDF Oxide provides full-text search across PDF documents with regex support, case-insensitive matching, whole-word mode, and per-match bounding boxes. Search results include page number, matched text, and precise coordinates for each match, making it straightforward to build search-and-highlight workflows. Use `TextSearcher::search()` for multi-page queries with custom options, or the `Pdf` convenience methods (`search()`, `search_page()`, `highlight_matches()`) for common use cases. ## Quick Example **Python** ```python from pdf_oxide import PdfDocument doc = PdfDocument("report.pdf") results = doc.search("conclusion", case_insensitive=True) for r in results: print(f"Page {r['page']}: '{r['text']}' at ({r['x']:.1f}, {r['y']:.1f})") ``` **Rust** ```rust use pdf_oxide::api::Pdf; let mut pdf = Pdf::open("report.pdf")?; let results = pdf.search("conclusion")?; for r in &results { println!("Page {}: '{}' at ({:.1}, {:.1})", r.page, r.text, r.bbox.x, r.bbox.y); } ``` --- ## API Reference ### `TextSearcher::search(doc, pattern, options) -> Vec` Search for text across multiple pages of a PDF document. The pattern is compiled as a regex unless `literal` mode is enabled. | Parameter | Type | Description | |-----------|------|-------------| | `doc` | `&mut PdfDocument` | The PDF document to search | | `pattern` | `&str` | Regex pattern (or literal text if `literal` is set) | | `options` | `&SearchOptions` | Search configuration | **Returns:** A vector of `SearchResult` objects, ordered by page and position. **Rust** ```rust use pdf_oxide::PdfDocument; use pdf_oxide::search::{TextSearcher, SearchOptions}; let mut doc = PdfDocument::open("report.pdf")?; let options = SearchOptions::new() .with_case_insensitive(true) .with_max_results(50); let results = TextSearcher::search(&mut doc, "error|warning", &options)?; for r in &results { println!("Page {}: '{}'", r.page, r.text); } ``` --- ### `TextSearcher::search_page(doc, page, regex, options) -> Vec` Search for text on a specific page using a pre-compiled regex. | Parameter | Type | Description | |-----------|------|-------------| | `doc` | `&mut PdfDocument` | The PDF document | | `page` | `usize` | Zero-based page index | | `regex` | `&Regex` | Pre-compiled regex pattern | | `options` | `&SearchOptions` | Search configuration | **Returns:** A vector of `SearchResult` objects for the specified page. **Rust** ```rust use pdf_oxide::PdfDocument; use pdf_oxide::search::{TextSearcher, SearchOptions}; use regex::Regex; let mut doc = PdfDocument::open("report.pdf")?; let regex = Regex::new(r"\d{4}-\d{2}-\d{2}")?; // Date pattern let options = SearchOptions::default(); let results = TextSearcher::search_page(&mut doc, 0, ®ex, &options)?; for r in &results { println!("Date found: '{}' at ({:.1}, {:.1})", r.text, r.bbox.x, r.bbox.y); } ``` --- ### SearchOptions Configuration for text search behavior. Uses a builder pattern for ergonomic construction. | Field | Type | Default | Description | |-------|------|---------|-------------| | `case_insensitive` | `bool` | `false` | Ignore case when matching | | `literal` | `bool` | `false` | Treat pattern as literal text (escape regex chars) | | `whole_word` | `bool` | `false` | Match whole words only (wraps pattern in `\b...\b`) | | `max_results` | `usize` | `0` | Maximum results to return (0 = unlimited) | | `page_range` | `Option<(usize, usize)>` | `None` | Page range to search (inclusive start, inclusive end) | #### Builder Methods ```rust let options = SearchOptions::new() .with_case_insensitive(true) .with_literal(true) .with_whole_word(true) .with_max_results(100) .with_page_range(0, 9); ``` #### Convenience Constructor ```rust // Quick case-insensitive search let options = SearchOptions::case_insensitive(); ``` --- ### SearchResult A single search match with position information. | Field | Type | Description | |-------|------|-------------| | `page` | `usize` | Page number (0-indexed) | | `text` | `String` | The matched text | | `bbox` | `Rect` | Combined bounding box of the match | | `start_index` | `usize` | Start index in the page's extracted text | | `end_index` | `usize` | End index in the page's extracted text | | `span_boxes` | `Vec` | Individual bounding boxes for each span in the match (useful for multi-line matches) | **Python:** In the Python API, search results are returned as dictionaries: ```python { "page": 0, "text": "conclusion", "x": 72.0, "y": 650.5, "width": 85.3, "height": 12.0, } ``` --- ### Pdf Convenience Methods The high-level `Pdf` API provides shortcut methods for common search operations. #### `search(pattern) -> Vec` Search the entire document with default options. ```rust let mut pdf = Pdf::open("report.pdf")?; let results = pdf.search("important")?; ``` #### `search_with_options(pattern, options) -> Vec` Search with custom options. ```rust let options = SearchOptions::case_insensitive() .with_whole_word(true) .with_page_range(0, 5); let results = pdf.search_with_options("abstract", options)?; ``` #### `search_page(page, pattern) -> Vec` Search a single page with default options. ```rust let results = pdf.search_page(0, r"\d+\.\d+")?; // Find decimal numbers ``` #### `highlight_matches(results, color) -> Result<()>` Create highlight annotations for search results. Each result gets a yellow (or custom color) highlight annotation on its page. | Parameter | Type | Description | |-----------|------|-------------| | `results` | `&[SearchResult]` | Search results to highlight | | `color` | `[f32; 3]` | RGB color (0.0--1.0 per component) | ```rust let mut pdf = Pdf::open("report.pdf")?; let results = pdf.search("important")?; pdf.highlight_matches(&results, [1.0, 1.0, 0.0])?; // Yellow pdf.save("highlighted.pdf")?; ``` --- ### Python Search API The Python `PdfDocument` class exposes search directly. #### `doc.search(pattern, ...) -> list[dict]` ```python doc.search( pattern: str, case_insensitive: bool = False, literal: bool = False, whole_word: bool = False, max_results: int = 0, ) -> list[dict] ``` #### `doc.search_page(page, pattern, ...) -> list[dict]` ```python doc.search_page( page: int, pattern: str, case_insensitive: bool = False, literal: bool = False, whole_word: bool = False, max_results: int = 0, ) -> list[dict] ``` --- ## Advanced Examples ### Search and highlight with custom color ```rust use pdf_oxide::api::Pdf; use pdf_oxide::search::SearchOptions; let mut pdf = Pdf::open("contract.pdf")?; // Find all dollar amounts let options = SearchOptions::new() .with_literal(false); // regex mode let results = pdf.search_with_options(r"\$[\d,]+\.?\d*", options)?; println!("Found {} dollar amounts", results.len()); for r in &results { println!(" Page {}: {}", r.page + 1, r.text); } // Highlight them in green pdf.highlight_matches(&results, [0.6, 1.0, 0.6])?; pdf.save("highlighted_amounts.pdf")?; ``` ### Search with page range restriction ```python from pdf_oxide import PdfDocument doc = PdfDocument("book.pdf") # Search only the first 10 pages results = doc.search( "introduction", case_insensitive=True, whole_word=True, max_results=5, ) for r in results: print(f"Found on page {r['page'] + 1}") ``` ### Build a search index across multiple PDFs ```rust use pdf_oxide::PdfDocument; use pdf_oxide::search::{TextSearcher, SearchOptions}; use std::collections::HashMap; let files = vec!["paper_a.pdf", "paper_b.pdf", "paper_c.pdf"]; let query = "machine learning"; let options = SearchOptions::case_insensitive(); let mut index: HashMap> = HashMap::new(); for file in &files { let mut doc = PdfDocument::open(file)?; let results = TextSearcher::search(&mut doc, query, &options)?; for r in results { index.entry(file.to_string()) .or_default() .push((r.page, r.text)); } } for (file, matches) in &index { println!("{}: {} matches", file, matches.len()); for (page, text) in matches { println!(" Page {}: '{}'", page + 1, text); } } ``` ### Extract context around matches ```rust use pdf_oxide::PdfDocument; use pdf_oxide::search::{TextSearcher, SearchOptions}; let mut doc = PdfDocument::open("report.pdf")?; let options = SearchOptions::new().with_case_insensitive(true); let results = TextSearcher::search(&mut doc, "error", &options)?; for r in &results { // Extract full page text for context let page_text = doc.extract_text(r.page)?; // Show 50 chars before and after the match let start = r.start_index.saturating_sub(50); let end = (r.end_index + 50).min(page_text.len()); let context = &page_text[start..end]; println!("Page {} match: ...{}...", r.page + 1, context.trim()); } ``` --- ## Related Pages - [Text Extraction](/docs/extraction/text) -- The text extraction that search operates on - [Annotation Extraction](/docs/extraction/annotations) -- Annotations created by highlight_matches - [Markdown Conversion](/docs/extraction/markdown) -- Convert search results context to Markdown --- # Page Rendering Render PDF pages to raster images (PNG or JPEG) using a pure-Rust rendering engine built on tiny-skia. No external dependencies like Poppler or MuPDF required. > **Note:** Page rendering is currently available in the Rust API only. ## Enabling the Feature ```toml [dependencies] pdf_oxide = { version = "0.3", features = ["rendering"] } ``` ## Quick Example ```rust use pdf_oxide::PdfDocument; use pdf_oxide::rendering::{render_page, RenderOptions}; let mut doc = PdfDocument::open("document.pdf")?; // Render first page as PNG at 150 DPI (default) let image = render_page(&mut doc, 0, &RenderOptions::default())?; image.save("page1.png")?; ``` ## Render Options ```rust use pdf_oxide::rendering::{RenderOptions, ImageFormat}; // Default: 150 DPI, PNG, white background let opts = RenderOptions::default(); // High-quality 300 DPI let opts = RenderOptions::with_dpi(300); // JPEG output at 90% quality let opts = RenderOptions::with_dpi(300).as_jpeg(90); // Transparent background (PNG only) let opts = RenderOptions::default().with_transparent_background(); ``` ### RenderOptions Fields | Field | Type | Default | Description | |-------|------|---------|-------------| | `dpi` | `u32` | `150` | Resolution in dots per inch | | `format` | `ImageFormat` | `Png` | Output format (Png or Jpeg) | | `background` | `Option<[f32; 4]>` | White | RGBA background color | | `render_annotations` | `bool` | `true` | Whether to render annotations | | `jpeg_quality` | `u8` | `85` | JPEG quality 1-100 | ### RenderedImage ```rust pub struct RenderedImage { pub data: Vec, pub width: u32, pub height: u32, pub format: ImageFormat, } ``` Methods: `save(path)`, `as_bytes()`. ## Render All Pages ```rust let mut doc = PdfDocument::open("document.pdf")?; let opts = RenderOptions::with_dpi(200); for page in 0..doc.page_count()? { let image = render_page(&mut doc, page, &opts)?; image.save(format!("page_{}.png", page + 1))?; } ``` ## Generate Thumbnails ```rust let opts = RenderOptions::with_dpi(72).as_jpeg(75); let thumb = render_page(&mut doc, 0, &opts)?; thumb.save("thumbnail.jpg")?; ``` --- # Create from Markdown Convert Markdown content into a properly formatted PDF document. Supports headings, paragraphs, bold/italic text, lists, code blocks, blockquotes, and more. ## Quick Example **Python** ```python from pdf_oxide import Pdf pdf = Pdf.from_markdown("# Hello\n\nWorld") pdf.save("out.pdf") ``` **Rust** ```rust use pdf_oxide::api::Pdf; let pdf = Pdf::from_markdown("# Hello\n\nWorld")?; pdf.save("out.pdf")?; ``` ## Supported Markdown Syntax | Syntax | Markdown | Description | |---------------------|------------------------------|-------------------------------| | Heading 1 | `# Title` | Large bold heading | | Heading 2 | `## Section` | Medium bold heading | | Heading 3 | `### Subsection` | Small heading | | Heading 4-6 | `#### ...` | Minor headings | | Paragraph | Plain text with blank line | Body text with word wrapping | | **Bold** | `**bold**` | Bold text | | *Italic* | `*italic*` | Italic text | | Unordered list | `- item` or `* item` | Bulleted list | | Ordered list | `1. item` | Numbered list | | Code block | `` ``` `` fenced blocks | Monospace code | | Inline code | `` `code` `` | Inline monospace | | Blockquote | `> quoted text` | Indented quotation | ## Full API Reference ### `Pdf::from_markdown(content)` (Static Method) Creates a PDF from Markdown content using default settings (Letter page, 72pt margins, 12pt Helvetica). **Rust:** ```rust use pdf_oxide::api::Pdf; let pdf = Pdf::from_markdown("# Report\n\nFindings are summarized below.")?; pdf.save("report.pdf")?; ``` **Python:** ```python from pdf_oxide import Pdf pdf = Pdf.from_markdown("# Report\n\nFindings are summarized below.") pdf.save("report.pdf") ``` **Python Signature:** ```python Pdf.from_markdown( content: str, title: str | None = None, author: str | None = None ) -> Pdf ``` ### `PdfBuilder::new().from_markdown(content)` (Builder Pattern) Use `PdfBuilder` when you need control over page size, margins, font size, or metadata. **Rust:** ```rust use pdf_oxide::api::PdfBuilder; use pdf_oxide::writer::PageSize; let pdf = PdfBuilder::new() .title("Quarterly Report") .author("Finance Team") .page_size(PageSize::A4) .margin(54.0) // 0.75 inch margins .font_size(11.0) .line_height(1.6) .from_markdown("# Q4 Report\n\n## Revenue\n\nRevenue grew **12%** year-over-year.")?; pdf.save("quarterly.pdf")?; ``` ## Advanced Examples ### Multi-Section Document ```rust use pdf_oxide::api::Pdf; let markdown = r#" # Annual Report 2025 ## Executive Summary The company achieved **record growth** in all key metrics. ## Financial Highlights - Revenue: $142M (+18%) - Net Income: $31M (+24%) - Operating Margin: 21.8% ## Strategic Priorities 1. Expand international presence 2. Launch next-generation platform 3. Invest in R&D capabilities ### Timeline > Phase 1 launches in Q2, with full rollout expected by Q4. ## Technical Appendix ```json { "version": "2.1.0", "release_date": "2025-03-15" } ``` "#; let pdf = Pdf::from_markdown(markdown)?; pdf.save("annual_report.pdf")?; ``` ### Python with Metadata ```python from pdf_oxide import Pdf content = """ # Meeting Notes ## Attendees - Alice (Engineering) - Bob (Product) - Carol (Design) ## Action Items 1. **Alice**: Complete API review by Friday 2. **Bob**: Update roadmap with new timeline 3. **Carol**: Share mockups for dashboard redesign """ pdf = Pdf.from_markdown(content, title="Meeting Notes", author="Alice") pdf.save("meeting_notes.pdf") ``` ### Reading Markdown from a File ```python from pdf_oxide import Pdf with open("README.md") as f: content = f.read() pdf = Pdf.from_markdown(content, title="README") pdf.save("readme.pdf") ``` ```rust use pdf_oxide::api::Pdf; let content = std::fs::read_to_string("README.md")?; let pdf = Pdf::from_markdown(&content)?; pdf.save("readme.pdf")?; ``` ## Related Pages - [Create from HTML](/docs/creation/from-html) -- Convert HTML to PDF - [PdfBuilder Fluent API](/docs/creation/builder) -- Full builder configuration options - [DocumentBuilder Low-Level API](/docs/creation/document-builder) -- Programmatic page construction --- # Create from HTML Convert HTML content into a formatted PDF document. Supports standard structural HTML elements for document layout. ## Quick Example **Python** ```python from pdf_oxide import Pdf pdf = Pdf.from_html("

Hello

World

") pdf.save("out.pdf") ``` **Rust** ```rust use pdf_oxide::api::Pdf; let pdf = Pdf::from_html("

Hello

World

")?; pdf.save("out.pdf")?; ``` ## Supported HTML Elements | Element | Description | |---------------------------------|--------------------------------------| | `

` through `

` | Headings (mapped to PDF heading sizes) | | `

` | Paragraphs with automatic spacing | | ``, `` | Bold text | | ``, `` | Italic text | | `

    `, `
      `, `
    1. ` | Unordered and ordered lists | | `
      `, ``              | Preformatted and inline code         |
      | `
      ` | Block quotations | | `
      ` | Line breaks | | `
      ` | Horizontal rules | ## Full API Reference ### `Pdf::from_html(content)` (Static Method) Creates a PDF from HTML content using default settings (Letter page, 72pt margins, 12pt Helvetica). **Rust:** ```rust use pdf_oxide::api::Pdf; let html = r#"

      Product Specification

      This document describes the technical requirements for the new product line.

      Requirements

      • Operating temperature: -20C to 60C
      • Power consumption: <5W
      • Weight: <200g
      "#; let pdf = Pdf::from_html(html)?; pdf.save("spec.pdf")?; ``` **Python:** ```python from pdf_oxide import Pdf html = """

      Product Specification

      This document describes the technical requirements for the new product line.

      """ pdf = Pdf.from_html(html) pdf.save("spec.pdf") ``` **Python Signature:** ```python Pdf.from_html( content: str, title: str | None = None, author: str | None = None ) -> Pdf ``` ### `PdfBuilder::new().from_html(content)` (Builder Pattern) Use `PdfBuilder` for control over page size, margins, font size, and document metadata. **Rust:** ```rust use pdf_oxide::api::PdfBuilder; use pdf_oxide::writer::PageSize; let pdf = PdfBuilder::new() .title("Technical Specification") .author("Engineering") .page_size(PageSize::A4) .margin(54.0) .font_size(11.0) .from_html("

      Spec

      Version 2.0

      ")?; pdf.save("spec_a4.pdf")?; ``` ## Advanced Examples ### Structured Report ```rust use pdf_oxide::api::Pdf; let html = r#"

      Incident Report

      Summary

      On 2025-11-15, a service disruption was detected in the payment processing pipeline.

      Timeline

      1. 14:32 UTC - Alert triggered for elevated error rates
      2. 14:35 UTC - On-call engineer acknowledged
      3. 14:48 UTC - Root cause identified: database connection pool exhaustion
      4. 15:02 UTC - Fix deployed, services recovering
      5. 15:15 UTC - Full recovery confirmed

      Root Cause

      A configuration change deployed at 14:00 UTC reduced the maximum connection pool size from 100 to 10.

      Code Reference

      max_connections: 10  # Should be 100
      timeout_seconds: 30
      

      Action Items

      • Add validation for connection pool configuration
      • Implement canary deployment for config changes
      • Add alerting for connection pool utilization
      "#; let pdf = Pdf::from_html(html)?; pdf.save("incident_report.pdf")?; ``` ### Python with Dynamic HTML ```python from pdf_oxide import Pdf rows = [ ("Widget A", "$12.99", 150), ("Widget B", "$24.50", 89), ("Widget C", "$7.25", 312), ] html = "

      Inventory Report

      " html += "

      Generated on 2025-11-20

      " html += "

      Current Stock

        " for name, price, qty in rows: html += f"
      • {name} - {price} ({qty} units)
      • " html += "
      " pdf = Pdf.from_html(html, title="Inventory Report") pdf.save("inventory.pdf") ``` ### Reading HTML from a File ```python from pdf_oxide import Pdf with open("report.html") as f: html = f.read() pdf = Pdf.from_html(html, title="Report") pdf.save("report.pdf") ``` ```rust use pdf_oxide::api::Pdf; let html = std::fs::read_to_string("report.html")?; let pdf = Pdf::from_html(&html)?; pdf.save("report.pdf")?; ``` ## Related Pages - [Create from Markdown](/docs/creation/from-markdown) -- Convert Markdown to PDF - [PdfBuilder Fluent API](/docs/creation/builder) -- Full builder configuration options - [DocumentBuilder Low-Level API](/docs/creation/document-builder) -- Programmatic page construction --- # Create from Images Convert images into PDF documents. Each image becomes a page, sized to fit the image while maintaining its aspect ratio. Supports JPEG and PNG formats. ## Quick Example **Python** ```python from pdf_oxide import Pdf # Single image pdf = Pdf.from_image("photo.jpg") pdf.save("photo.pdf") # Multiple images pdf = Pdf.from_images(["page1.jpg", "page2.png", "page3.jpg"]) pdf.save("album.pdf") ``` **Rust** ```rust use pdf_oxide::api::Pdf; // Single image let pdf = Pdf::from_image("photo.jpg")?; pdf.save("photo.pdf")?; // Multiple images let pdf = Pdf::from_images(&["page1.jpg", "page2.png", "page3.jpg"])?; pdf.save("album.pdf")?; ``` ## Full API Reference ### `Pdf::from_image(path)` -- Single Image Creates a single-page PDF from an image file. The page is sized to fit the image within the configured page dimensions while preserving aspect ratio. **Rust:** ```rust use pdf_oxide::api::Pdf; let pdf = Pdf::from_image("diagram.png")?; pdf.save("diagram.pdf")?; ``` **Python:** ```python pdf = Pdf.from_image("diagram.png") pdf.save("diagram.pdf") ``` ### `Pdf::from_image_bytes(data)` -- Image from Bytes Creates a PDF from raw image bytes. The image format (JPEG or PNG) is auto-detected from the byte content. **Rust:** ```rust use pdf_oxide::api::Pdf; let image_bytes = std::fs::read("photo.jpg")?; let pdf = Pdf::from_image_bytes(&image_bytes)?; pdf.save("photo.pdf")?; ``` **Python:** ```python from pdf_oxide import Pdf with open("photo.jpg", "rb") as f: image_bytes = f.read() pdf = Pdf.from_image_bytes(image_bytes) pdf.save("photo.pdf") ``` ### `Pdf::from_images(paths)` -- Multiple Images Creates a multi-page PDF where each image becomes a separate page. Pages are individually sized to match each image's dimensions and aspect ratio. **Rust:** ```rust use pdf_oxide::api::Pdf; let pages = vec!["scan_001.jpg", "scan_002.jpg", "scan_003.jpg"]; let pdf = Pdf::from_images(&pages)?; pdf.save("scanned_document.pdf")?; ``` **Python:** ```python from pdf_oxide import Pdf pages = ["scan_001.jpg", "scan_002.jpg", "scan_003.jpg"] pdf = Pdf.from_images(pages) pdf.save("scanned_document.pdf") ``` ### Using PdfBuilder with Images Use `PdfBuilder` to control page size and margins when creating image PDFs. **Rust:** ```rust use pdf_oxide::api::PdfBuilder; use pdf_oxide::writer::PageSize; let pdf = PdfBuilder::new() .title("Photo Album") .author("Photographer") .page_size(PageSize::A4) .margin(36.0) // 0.5 inch margins .from_image("photo.jpg")?; pdf.save("photo_a4.pdf")?; ``` **Multiple images with builder:** ```rust use pdf_oxide::api::PdfBuilder; use pdf_oxide::writer::PageSize; let pdf = PdfBuilder::new() .title("Document Scans") .page_size(PageSize::Letter) .from_images(&["page1.png", "page2.png"])?; pdf.save("scans.pdf")?; ``` ## Advanced Examples ### Batch Convert a Directory of Images ```rust use pdf_oxide::api::Pdf; use std::fs; let mut images: Vec = fs::read_dir("./photos")? .filter_map(|entry| { let path = entry.ok()?.path(); let ext = path.extension()?.to_str()?.to_lowercase(); if ext == "jpg" || ext == "jpeg" || ext == "png" { Some(path.to_string_lossy().to_string()) } else { None } }) .collect(); images.sort(); let pdf = Pdf::from_images(&images)?; pdf.save("all_photos.pdf")?; ``` ### Python: Convert Images from a Directory ```python from pdf_oxide import Pdf from pathlib import Path image_dir = Path("./scans") images = sorted( str(p) for p in image_dir.iterdir() if p.suffix.lower() in (".jpg", ".jpeg", ".png") ) pdf = Pdf.from_images(images) pdf.save("scans.pdf") ``` ### Create PDF from Downloaded Image Bytes ```python from pdf_oxide import Pdf import urllib.request url = "https://example.com/chart.png" image_data = urllib.request.urlopen(url).read() pdf = Pdf.from_image_bytes(image_data) pdf.save("chart.pdf") ``` ## Supported Formats | Format | Extensions | Notes | |--------|---------------|------------------------------------------| | JPEG | `.jpg`, `.jpeg` | Lossy compression, best for photos | | PNG | `.png` | Lossless, supports transparency | ## Related Pages - [PdfBuilder Fluent API](/docs/creation/builder) -- Configure page size, margins, and metadata - [QR Codes and Barcodes](/docs/creation/barcodes) -- Generate barcode images as PDFs - [Create from Markdown](/docs/creation/from-markdown) -- Convert text content to PDF --- # PdfBuilder Fluent API `PdfBuilder` provides a fluent configuration API for PDF creation. Chain methods to set page size, margins, font size, metadata, and more, then call a `from_*` method to generate the PDF. ## Quick Example **Python** ```python from pdf_oxide import Pdf # Python uses optional keyword arguments on Pdf static methods pdf = Pdf.from_markdown( "# Report\n\nContent here.", title="Quarterly Report", author="Finance Team" ) pdf.save("report.pdf") ``` **Rust** ```rust use pdf_oxide::api::PdfBuilder; use pdf_oxide::writer::PageSize; let pdf = PdfBuilder::new() .title("Quarterly Report") .author("Finance Team") .page_size(PageSize::A4) .margin(54.0) .font_size(11.0) .line_height(1.5) .from_markdown("# Report\n\nContent here.")?; pdf.save("report.pdf")?; ``` ## Full API Reference ### Constructor ```rust PdfBuilder::new() -> PdfBuilder ``` Creates a new builder with default configuration: | Setting | Default | |---------------|-------------| | Page size | Letter (8.5" x 11") | | All margins | 72pt (1 inch) | | Font size | 12pt | | Line height | 1.5 | ### Metadata Methods #### `.title(title)` -- Set Document Title ```rust PdfBuilder::new().title("My Document") ``` #### `.author(author)` -- Set Document Author ```rust PdfBuilder::new().author("Jane Smith") ``` #### `.subject(subject)` -- Set Document Subject ```rust PdfBuilder::new().subject("Annual Performance Review") ``` #### `.keywords(keywords)` -- Set Document Keywords ```rust PdfBuilder::new().keywords("report, annual, 2025") ``` ### Layout Methods #### `.page_size(size)` -- Set Page Size ```rust use pdf_oxide::writer::PageSize; PdfBuilder::new().page_size(PageSize::A4) ``` **PageSize enum:** | Variant | Dimensions | |----------------------|--------------------------| | `PageSize::Letter` | 612 x 792 pt (8.5" x 11") | | `PageSize::A4` | 595 x 842 pt (210 x 297 mm) | | `PageSize::Legal` | 612 x 1008 pt (8.5" x 14") | | `PageSize::A3` | 842 x 1190 pt (297 x 420 mm) | | `PageSize::Custom(w, h)` | Custom width x height in points | #### `.margin(margin)` -- Set Uniform Margins Sets all four margins (left, right, top, bottom) to the same value in points. ```rust PdfBuilder::new().margin(54.0) // 0.75 inch margins ``` #### `.margins(left, right, top, bottom)` -- Set Individual Margins ```rust PdfBuilder::new().margins(72.0, 72.0, 54.0, 54.0) ``` ### Typography Methods #### `.font_size(size)` -- Set Default Font Size ```rust PdfBuilder::new().font_size(11.0) ``` #### `.line_height(height)` -- Set Line Height Multiplier Controls spacing between lines. A value of 1.5 means 1.5 times the font size. ```rust PdfBuilder::new().line_height(1.6) ``` ### Generation Methods Each generation method consumes the builder and returns a `Pdf` object. #### `.from_markdown(content)` -- Generate from Markdown ```rust let pdf = PdfBuilder::new() .title("Notes") .from_markdown("# Notes\n\n- Item one\n- Item two")?; ``` #### `.from_html(content)` -- Generate from HTML ```rust let pdf = PdfBuilder::new() .title("Report") .from_html("

      Report

      Summary of findings.

      ")?; ``` #### `.from_text(content)` -- Generate from Plain Text ```rust let pdf = PdfBuilder::new() .font_size(10.0) .from_text("Line 1\nLine 2\nLine 3")?; ``` #### `.from_image(path)` -- Generate from Single Image ```rust let pdf = PdfBuilder::new() .page_size(PageSize::A4) .from_image("photo.jpg")?; ``` #### `.from_image_bytes(data)` -- Generate from Image Bytes ```rust let data = std::fs::read("chart.png")?; let pdf = PdfBuilder::new().from_image_bytes(&data)?; ``` #### `.from_images(paths)` -- Generate from Multiple Images ```rust let pdf = PdfBuilder::new() .title("Photo Album") .from_images(&["img1.jpg", "img2.jpg", "img3.jpg"])?; ``` #### `.from_qrcode(data)` -- Generate QR Code PDF Requires the `barcodes` feature. ```rust let pdf = PdfBuilder::new() .title("QR Code") .from_qrcode("https://example.com")?; ``` #### `.from_barcode(barcode_type, data)` -- Generate Barcode PDF Requires the `barcodes` feature. ```rust use pdf_oxide::writer::barcode::BarcodeType; let pdf = PdfBuilder::new() .from_barcode(BarcodeType::Code128, "ABC-12345")?; ``` ## Advanced Examples ### Full Configuration Chain ```rust use pdf_oxide::api::PdfBuilder; use pdf_oxide::writer::PageSize; let content = r#" # Technical Specification ## Overview This document defines the interface contract for the v2 API. ## Endpoints - `GET /api/v2/users` - List users - `POST /api/v2/users` - Create user - `GET /api/v2/users/:id` - Get user by ID ## Authentication All endpoints require a Bearer token in the Authorization header. "#; let pdf = PdfBuilder::new() .title("API Specification v2") .author("Platform Team") .subject("REST API Technical Specification") .keywords("api, rest, specification, v2") .page_size(PageSize::A4) .margins(72.0, 72.0, 54.0, 72.0) .font_size(11.0) .line_height(1.5) .from_markdown(content)?; pdf.save("api_spec.pdf")?; ``` ### Custom Page Size ```rust use pdf_oxide::api::PdfBuilder; use pdf_oxide::writer::PageSize; // Create a half-letter page (5.5" x 8.5") let pdf = PdfBuilder::new() .page_size(PageSize::Custom(396.0, 612.0)) .margin(36.0) .font_size(10.0) .from_markdown("# Pocket Guide\n\nCompact reference card.")?; pdf.save("pocket_guide.pdf")?; ``` ## `Pdf` Output Methods Once you have a `Pdf` object, use these methods to output it: | Method | Description | |-------------------------|------------------------------------------| | `.save(path)` | Write the PDF to a file | | `.to_bytes()` (Python) | Get the raw PDF bytes | | `.into_bytes()` (Rust) | Consume the Pdf and return `Vec` | ## Related Pages - [Create from Markdown](/docs/creation/from-markdown) -- Markdown to PDF - [Create from HTML](/docs/creation/from-html) -- HTML to PDF - [Create from Images](/docs/creation/from-images) -- Images to PDF - [DocumentBuilder Low-Level API](/docs/creation/document-builder) -- Programmatic page construction - [QR Codes and Barcodes](/docs/creation/barcodes) -- Barcode generation --- # DocumentBuilder Low-Level API `DocumentBuilder` provides a fluent, low-level API for constructing PDF documents page by page with full control over text placement, fonts, annotations, and content elements. ## Quick Example **Rust** ```rust use pdf_oxide::writer::{DocumentBuilder, PageSize, DocumentMetadata}; let mut builder = DocumentBuilder::new() .metadata(DocumentMetadata::new().title("My Document")); builder .page(PageSize::Letter) .at(72.0, 720.0) .heading(1, "Hello, World!") .paragraph("This is a PDF document created with DocumentBuilder.") .done(); let bytes = builder.build()?; std::fs::write("output.pdf", bytes)?; ``` **Python** For programmatic page construction from Python, use `Pdf.from_markdown()` or `Pdf.from_html()` which internally use DocumentBuilder. The `DocumentBuilder` is a Rust-level API. ```python from pdf_oxide import Pdf # Achieve similar results through Markdown pdf = Pdf.from_markdown( "# Hello, World!\n\nThis is a PDF document.", title="My Document" ) pdf.save("output.pdf") ``` ## Full API Reference ### `DocumentBuilder` #### `DocumentBuilder::new()` -- Create Builder ```rust let mut builder = DocumentBuilder::new(); ``` #### `.metadata(metadata)` -- Set Document Metadata ```rust use pdf_oxide::writer::DocumentMetadata; let mut builder = DocumentBuilder::new() .metadata( DocumentMetadata::new() .title("Report") .author("Jane Smith") .subject("Q4 Analysis") .keywords("finance, quarterly") .creator("MyApp") ); ``` #### `.page(size)` -- Add a Page Returns a `FluentPageBuilder` for adding content to the page. Call `.done()` when finished. ```rust use pdf_oxide::writer::PageSize; builder.page(PageSize::A4) .at(72.0, 770.0) .text("Hello") .done(); ``` #### `.letter_page()` / `.a4_page()` -- Convenience Page Methods ```rust builder.letter_page().text("US Letter page").done(); builder.a4_page().text("A4 page").done(); ``` #### `.build()` -- Generate PDF Bytes ```rust let bytes: Vec = builder.build()?; ``` #### `.save(path)` -- Build and Save to File ```rust builder.save("output.pdf")?; ``` ### `DocumentMetadata` Builder for document-level metadata. | Method | Description | |-----------------|----------------------------| | `.title(s)` | Set document title | | `.author(s)` | Set document author | | `.subject(s)` | Set document subject | | `.keywords(s)` | Set document keywords | | `.creator(s)` | Set creator application | ### `FluentPageBuilder` Returned by `builder.page(size)`. All methods return `self` for chaining (except `.done()`). #### Text and Positioning | Method | Description | |----------------------------|--------------------------------------------| | `.at(x, y)` | Set cursor position (points from bottom-left) | | `.text(s)` | Add text at current cursor position | | `.heading(level, s)` | Add heading (1-6, with appropriate font) | | `.paragraph(s)` | Add paragraph with automatic word wrapping | | `.font(name, size)` | Set font for subsequent text | | `.text_config(config)` | Set full text configuration | | `.space(points)` | Add vertical space | | `.horizontal_rule()` | Add a horizontal divider line | | `.element(elem)` | Add a raw `ContentElement` | | `.elements(vec)` | Add multiple raw content elements | #### Annotations | Method | Description | |-------------------------------------|----------------------------------------------| | `.link_url(url)` | Link the last text element to a URL | | `.link_page(page_index)` | Link the last text to an internal page | | `.link_named(destination)` | Link to a named destination | | `.highlight(color)` | Highlight the last text (RGB tuple) | | `.underline(color)` | Underline the last text | | `.strikeout(color)` | Strikethrough the last text | | `.squiggly(color)` | Squiggly underline the last text | | `.sticky_note(text)` | Add sticky note at cursor position | | `.sticky_note_with_icon(text, icon)` | Add sticky note with specific icon | | `.sticky_note_at(x, y, text)` | Add sticky note at specific position | | `.stamp(stamp_type)` | Add stamp at cursor position | | `.stamp_at(rect, stamp_type)` | Add stamp at specific rectangle | | `.freetext(rect, text)` | Add free text annotation | | `.freetext_styled(rect, text, font, size)` | Add styled free text annotation | | `.watermark(text)` | Add diagonal watermark across page | | `.watermark_confidential()` | Add preset "CONFIDENTIAL" watermark | | `.watermark_draft()` | Add preset "DRAFT" watermark | | `.watermark_custom(watermark)` | Add custom watermark annotation | | `.add_annotation(annotation)` | Add any annotation type | #### `.done()` -- Finish Page Returns control to the `DocumentBuilder`. ```rust builder.page(PageSize::Letter) .at(72.0, 720.0) .text("Content") .done(); // Back to builder ``` ### `TextConfig` Configuration for text rendering. ```rust use pdf_oxide::writer::TextConfig; let config = TextConfig { font: "Times-Roman".to_string(), size: 14.0, align: TextAlign::Center, line_height: 1.5, }; ``` | Field | Type | Default | |---------------|------------|----------------| | `font` | `String` | `"Helvetica"` | | `size` | `f32` | `12.0` | | `align` | `TextAlign`| `Left` | | `line_height` | `f32` | `1.2` | ### `PageSize` | Variant | Width x Height (points) | |----------------------|-------------------------| | `Letter` | 612 x 792 | | `A4` | 595 x 842 | | `Legal` | 612 x 1008 | | `A3` | 842 x 1190 | | `Custom(w, h)` | Custom dimensions | ## Advanced Examples ### Multi-Page Document with Annotations ```rust use pdf_oxide::writer::{ DocumentBuilder, DocumentMetadata, PageSize, StampType }; let mut builder = DocumentBuilder::new() .metadata( DocumentMetadata::new() .title("Annotated Report") .author("Review Team") ); // Cover page builder.page(PageSize::Letter) .at(72.0, 600.0) .font("Helvetica-Bold", 28.0) .text("Annual Review 2025") .font("Helvetica", 14.0) .space(20.0) .text("Prepared by the Review Team") .watermark_draft() .done(); // Content page with annotations builder.page(PageSize::Letter) .at(72.0, 720.0) .heading(1, "Executive Summary") .paragraph( "Revenue increased 18% year-over-year, driven by expansion \ into new markets and strong retention rates." ) .text("See full financial details") .link_page(2) // Link to page 3 (0-indexed) .space(12.0) .heading(2, "Key Findings") .text("Customer satisfaction reached an all-time high.") .highlight((1.0, 1.0, 0.0)) // Yellow highlight .sticky_note("Verify this claim with latest survey data") .paragraph( "Operating costs were reduced through automation initiatives, \ achieving a 15% improvement in operational efficiency." ) .stamp(StampType::ForComment) .done(); // Data page builder.page(PageSize::Letter) .at(72.0, 720.0) .heading(1, "Financial Details") .paragraph("Detailed breakdown of revenue by segment...") .horizontal_rule() .paragraph("North America: $82M (+12%)") .paragraph("Europe: $41M (+28%)") .paragraph("Asia-Pacific: $19M (+35%)") .done(); builder.save("annotated_report.pdf")?; ``` ### Precise Text Positioning ```rust use pdf_oxide::writer::{DocumentBuilder, PageSize}; let mut builder = DocumentBuilder::new(); builder.page(PageSize::Letter) // Title at top center area .at(200.0, 740.0) .font("Helvetica-Bold", 20.0) .text("Certificate of Completion") // Recipient name .at(200.0, 620.0) .font("Times-Roman", 16.0) .text("Awarded to: Jane Smith") // Details at specific positions .at(72.0, 500.0) .font("Helvetica", 12.0) .text("For successfully completing the Advanced Rust Programming course.") .at(72.0, 450.0) .text("Date: November 15, 2025") .at(350.0, 450.0) .text("Instructor: Dr. Alan Turing") .horizontal_rule() .at(72.0, 380.0) .font("Helvetica", 9.0) .text("Certificate ID: CERT-2025-00042") .done(); builder.save("certificate.pdf")?; ``` ## Related Pages - [PdfBuilder Fluent API](/docs/creation/builder) -- High-level creation with PdfBuilder - [Annotation Creation](/docs/creation/annotations) -- All annotation types in detail - [Form Field Creation](/docs/creation/forms) -- Adding interactive form fields - [Table Rendering](/docs/creation/tables) -- Creating tables --- # Form Field Creation Add interactive form fields to PDF documents. PDF Oxide supports all standard AcroForm widget types: text fields, checkboxes, radio button groups, combo boxes (dropdowns), list boxes, and push buttons. ## Quick Example **Rust** ```rust use pdf_oxide::writer::{PdfWriter, TextFieldWidget, CheckboxWidget}; use pdf_oxide::geometry::Rect; let mut writer = PdfWriter::new(); { let mut page = writer.add_letter_page(); page.add_text("Name:", 72.0, 720.0, "Helvetica", 12.0); page.text_field("name", Rect::new(130.0, 716.0, 200.0, 20.0)); page.add_text("I agree to terms:", 72.0, 690.0, "Helvetica", 12.0); page.checkbox("agree", Rect::new(200.0, 686.0, 15.0, 15.0)); page.finish(); } writer.save("form.pdf")?; ``` **Python** Form creation uses the low-level `PdfWriter` API, which is available from Rust. From Python, open an existing PDF with `DocumentEditor` to add fields: ```python from pdf_oxide import PdfDocument # Open a PDF and add form fields via the editor API doc = PdfDocument("template.pdf") # Use the editing API to add form fields to existing documents ``` ## Widget Types ### TextFieldWidget -- Text Input Single-line or multi-line text input fields. ```rust use pdf_oxide::writer::TextFieldWidget; use pdf_oxide::geometry::Rect; // Basic text field let field = TextFieldWidget::new("username", Rect::new(72.0, 700.0, 200.0, 20.0)); // Fully configured text field let field = TextFieldWidget::new("email", Rect::new(72.0, 670.0, 250.0, 20.0)) .with_value("user@example.com") .with_default_value("") .with_max_length(100) .required() .with_tooltip("Enter your email address") .with_font("Helv", 12.0) .with_text_color(0.0, 0.0, 0.0) .with_border_color(0.5, 0.5, 0.5) .with_background_color(1.0, 1.0, 0.95); // Password field (characters are masked) let password = TextFieldWidget::new("password", Rect::new(72.0, 640.0, 200.0, 20.0)) .password(); // Multi-line text area let notes = TextFieldWidget::new("notes", Rect::new(72.0, 550.0, 300.0, 80.0)) .multiline(); ``` **Key methods:** | Method | Description | |---------------------------------|------------------------------------------| | `.with_value(s)` | Set current text value | | `.with_default_value(s)` | Set reset value | | `.with_max_length(n)` | Maximum character count | | `.required()` | Mark as required field | | `.read_only()` | Prevent editing | | `.password()` | Mask input characters | | `.multiline()` | Allow multiple lines | | `.with_tooltip(s)` | Hover tooltip text | | `.with_font(name, size)` | Set font and size | | `.with_text_color(r, g, b)` | Set text color (0.0-1.0 RGB) | | `.with_border_color(r, g, b)` | Set border color | | `.with_background_color(r, g, b)` | Set background color | ### CheckboxWidget -- Checkbox Toggle fields with on/off states. ```rust use pdf_oxide::writer::CheckboxWidget; use pdf_oxide::geometry::Rect; // Basic checkbox let cb = CheckboxWidget::new("agree", Rect::new(72.0, 700.0, 15.0, 15.0)); // Pre-checked checkbox with custom export value let cb = CheckboxWidget::new("newsletter", Rect::new(72.0, 680.0, 15.0, 15.0)) .checked() .with_export_value("subscribed") .with_tooltip("Subscribe to newsletter"); ``` **Key methods:** | Method | Description | |---------------------------|-------------------------------------| | `.checked()` | Set initial state to checked | | `.with_export_value(s)` | Value sent on form submission | | `.with_tooltip(s)` | Hover tooltip text | | `.read_only()` | Prevent toggling | ### RadioButtonGroup -- Radio Buttons Mutually exclusive option groups. All buttons in a group share the same field name; selecting one deselects the others. ```rust use pdf_oxide::writer::RadioButtonGroup; use pdf_oxide::geometry::Rect; let group = RadioButtonGroup::new("color") .add_button("Red", Rect::new(72.0, 700.0, 15.0, 15.0)) .add_button("Green", Rect::new(72.0, 680.0, 15.0, 15.0)) .add_button("Blue", Rect::new(72.0, 660.0, 15.0, 15.0)) .with_selected("Green"); // Pre-select "Green" ``` **Key methods:** | Method | Description | |--------------------------------|-------------------------------------| | `.add_button(value, rect)` | Add a radio option | | `.with_selected(value)` | Pre-select an option | | `.no_toggle_off()` | Prevent deselecting all options | ### ComboBoxWidget -- Dropdown Drop-down selection fields with optional editable text. ```rust use pdf_oxide::writer::ComboBoxWidget; use pdf_oxide::writer::form_fields::ChoiceOption; use pdf_oxide::geometry::Rect; let combo = ComboBoxWidget::new("country", Rect::new(72.0, 700.0, 200.0, 20.0)) .add_option("US", "United States") .add_option("GB", "United Kingdom") .add_option("DE", "Germany") .add_option("JP", "Japan") .with_selected("US"); ``` **Key methods:** | Method | Description | |-----------------------------|----------------------------------------| | `.add_option(value, label)` | Add a selectable option | | `.with_selected(value)` | Pre-select an option | | `.editable()` | Allow typing custom values | | `.sorted()` | Sort options alphabetically | ### ListBoxWidget -- Multi-Select List Scrollable list fields with single or multiple selection. ```rust use pdf_oxide::writer::ListBoxWidget; use pdf_oxide::geometry::Rect; let list = ListBoxWidget::new("languages", Rect::new(72.0, 600.0, 200.0, 80.0)) .add_option("rust", "Rust") .add_option("python", "Python") .add_option("go", "Go") .add_option("typescript", "TypeScript") .multi_select() .with_selected("rust"); ``` **Key methods:** | Method | Description | |-----------------------------|----------------------------------------| | `.add_option(value, label)` | Add a selectable option | | `.with_selected(value)` | Pre-select an option | | `.multi_select()` | Allow selecting multiple items | | `.sorted()` | Sort options alphabetically | ### PushButtonWidget -- Button Clickable buttons that trigger actions (submit, reset, or JavaScript). ```rust use pdf_oxide::writer::PushButtonWidget; use pdf_oxide::geometry::Rect; let submit = PushButtonWidget::new("submit", Rect::new(72.0, 500.0, 100.0, 30.0)) .with_label("Submit") .submit_form("https://example.com/submit"); let reset = PushButtonWidget::new("reset", Rect::new(180.0, 500.0, 100.0, 30.0)) .with_label("Reset") .reset_form(); ``` ## Advanced Examples ### Complete Registration Form ```rust use pdf_oxide::writer::{ PdfWriter, PdfWriterConfig, TextFieldWidget, CheckboxWidget, RadioButtonGroup, ComboBoxWidget, PushButtonWidget, }; use pdf_oxide::geometry::Rect; let config = PdfWriterConfig::default() .with_title("Registration Form") .with_author("HR Department"); let mut writer = PdfWriter::with_config(config); { let mut page = writer.add_letter_page(); // Title page.add_text("Employee Registration Form", 72.0, 740.0, "Helvetica-Bold", 18.0); // Personal Information page.add_text("First Name:", 72.0, 700.0, "Helvetica", 12.0); page.add_text_field( TextFieldWidget::new("first_name", Rect::new(170.0, 696.0, 200.0, 20.0)) .required() ); page.add_text("Last Name:", 72.0, 670.0, "Helvetica", 12.0); page.add_text_field( TextFieldWidget::new("last_name", Rect::new(170.0, 666.0, 200.0, 20.0)) .required() ); page.add_text("Email:", 72.0, 640.0, "Helvetica", 12.0); page.add_text_field( TextFieldWidget::new("email", Rect::new(170.0, 636.0, 250.0, 20.0)) .required() .with_tooltip("Work email address") ); // Department dropdown page.add_text("Department:", 72.0, 610.0, "Helvetica", 12.0); page.add_combo_box( ComboBoxWidget::new("department", Rect::new(170.0, 606.0, 200.0, 20.0)) .add_option("eng", "Engineering") .add_option("sales", "Sales") .add_option("hr", "Human Resources") .add_option("ops", "Operations") ); // Employment type radio buttons page.add_text("Employment Type:", 72.0, 570.0, "Helvetica", 12.0); page.add_text("Full-time", 95.0, 550.0, "Helvetica", 10.0); page.add_text("Part-time", 95.0, 530.0, "Helvetica", 10.0); page.add_text("Contract", 95.0, 510.0, "Helvetica", 10.0); page.add_radio_group( RadioButtonGroup::new("employment_type") .add_button("fulltime", Rect::new(72.0, 548.0, 15.0, 15.0)) .add_button("parttime", Rect::new(72.0, 528.0, 15.0, 15.0)) .add_button("contract", Rect::new(72.0, 508.0, 15.0, 15.0)) .with_selected("fulltime") ); // Agreement checkbox page.add_text("I agree to the terms and conditions", 95.0, 470.0, "Helvetica", 10.0); page.add_checkbox( CheckboxWidget::new("agree_terms", Rect::new(72.0, 468.0, 15.0, 15.0)) .with_export_value("agreed") ); // Submit button page.add_push_button( PushButtonWidget::new("submit", Rect::new(72.0, 420.0, 120.0, 30.0)) .with_label("Submit Form") ); page.finish(); } writer.save("registration_form.pdf")?; ``` ### Adding Form Fields to Existing PDFs Use `DocumentEditor` to add fields to a pre-existing PDF document: ```rust use pdf_oxide::editor::DocumentEditor; use pdf_oxide::writer::TextFieldWidget; use pdf_oxide::geometry::Rect; let mut editor = DocumentEditor::open("template.pdf")?; let field = TextFieldWidget::new("signature", Rect::new(72.0, 100.0, 250.0, 25.0)) .with_tooltip("Sign here"); editor.add_form_field(0, field)?; // Add to page 0 editor.save("signed_template.pdf")?; ``` ## Related Pages - [DocumentBuilder Low-Level API](/docs/creation/document-builder) -- Building pages with fluent API - [Annotation Creation](/docs/creation/annotations) -- Non-interactive annotation types - [PdfBuilder Fluent API](/docs/creation/builder) -- High-level PDF creation --- # Annotation Creation PDF Oxide supports creating all standard PDF annotation types. Annotations can be added through `FluentPageBuilder` (DocumentBuilder API) or `PageBuilder` (PdfWriter API). ## Quick Example ### Rust (DocumentBuilder) ```rust use pdf_oxide::writer::{DocumentBuilder, PageSize, StampType}; let mut builder = DocumentBuilder::new(); builder.page(PageSize::Letter) .at(72.0, 720.0) .text("Click here for details") .link_url("https://example.com") .text("Important finding") .highlight((1.0, 1.0, 0.0)) // Yellow highlight .sticky_note("Review this section carefully") .stamp(StampType::Approved) .done(); builder.save("annotated.pdf")?; ``` ### Rust (PdfWriter) ```rust use pdf_oxide::writer::PdfWriter; use pdf_oxide::geometry::Rect; let mut writer = PdfWriter::new(); { let mut page = writer.add_letter_page(); page.add_text("Document text", 72.0, 720.0, "Helvetica", 12.0); page.link(Rect::new(72.0, 720.0, 100.0, 12.0), "https://example.com"); page.highlight_rect(Rect::new(72.0, 700.0, 200.0, 12.0)); page.sticky_note(Rect::new(300.0, 720.0, 24.0, 24.0), "A note"); page.finish(); } writer.save("annotated.pdf")?; ``` ## Annotation Types ### Text Annotations (Sticky Notes) Pop-up notes with various icons. ```rust use pdf_oxide::writer::PdfWriter; use pdf_oxide::annotation_types::TextAnnotationIcon; use pdf_oxide::geometry::Rect; let mut writer = PdfWriter::new(); { let mut page = writer.add_letter_page(); // Default note icon page.sticky_note(Rect::new(72.0, 720.0, 24.0, 24.0), "Review this section"); // Comment icon page.comment(Rect::new(72.0, 690.0, 24.0, 24.0), "Needs clarification"); // Custom icon page.text_note_with_icon( Rect::new(72.0, 660.0, 24.0, 24.0), "Important", TextAnnotationIcon::Key, ); page.finish(); } ``` **Available icons:** `Note`, `Comment`, `Key`, `Help`, `NewParagraph`, `Paragraph`, `Insert` **FluentPageBuilder equivalent:** ```rust builder.page(PageSize::Letter) .at(72.0, 720.0) .sticky_note("Review this") .sticky_note_with_icon("Important", TextAnnotationIcon::Key) .sticky_note_at(300.0, 720.0, "Positioned note") .done(); ``` ### Link Annotations URL links and internal page navigation. ```rust // URL link page.link(Rect::new(72.0, 720.0, 150.0, 12.0), "https://example.com"); // Internal page link (0-indexed page number) page.internal_link(Rect::new(72.0, 700.0, 100.0, 12.0), 2); ``` **FluentPageBuilder:** ```rust builder.page(PageSize::Letter) .at(72.0, 720.0) .text("Visit website") .link_url("https://example.com") .text("Go to appendix") .link_page(5) .text("Jump to glossary") .link_named("glossary") .done(); ``` ### FreeText Annotations Text displayed directly on the page surface. ```rust use pdf_oxide::geometry::Rect; // Basic text box page.textbox(Rect::new(72.0, 650.0, 200.0, 50.0), "Annotation text"); // Styled text box page.textbox_styled( Rect::new(72.0, 580.0, 200.0, 50.0), "Styled text", "Courier", 14.0, ); // Centered text page.textbox_centered(Rect::new(72.0, 520.0, 200.0, 30.0), "Centered"); // Callout with leader line page.callout( Rect::new(200.0, 450.0, 150.0, 50.0), "Callout text", vec![150.0, 430.0, 200.0, 475.0], ); // Typewriter (borderless text) page.typewriter(Rect::new(72.0, 400.0, 300.0, 20.0), "Typewriter text"); ``` **FluentPageBuilder:** ```rust builder.page(PageSize::Letter) .freetext(Rect::new(100.0, 600.0, 200.0, 50.0), "Comment text") .freetext_styled(Rect::new(100.0, 530.0, 200.0, 50.0), "Styled", "Courier", 14.0) .done(); ``` ### Highlight, Underline, Strikeout, Squiggly Text markup annotations for reviewing. ```rust use pdf_oxide::geometry::Rect; page.highlight_rect(Rect::new(72.0, 720.0, 200.0, 12.0)); page.underline_rect(Rect::new(72.0, 700.0, 200.0, 12.0)); page.strikeout_rect(Rect::new(72.0, 680.0, 200.0, 12.0)); page.squiggly_rect(Rect::new(72.0, 660.0, 200.0, 12.0)); ``` With explicit QuadPoints for precise positioning: ```rust page.highlight( Rect::new(72.0, 720.0, 200.0, 12.0), vec![[72.0, 732.0, 272.0, 732.0, 72.0, 720.0, 272.0, 720.0]], ); ``` **FluentPageBuilder** (color is RGB 0.0-1.0): ```rust builder.page(PageSize::Letter) .at(72.0, 720.0) .text("Highlighted text") .highlight((1.0, 1.0, 0.0)) // Yellow .text("Underlined text") .underline((0.0, 0.0, 1.0)) // Blue .text("Deleted text") .strikeout((1.0, 0.0, 0.0)) // Red .text("Questionable text") .squiggly((1.0, 0.5, 0.0)) // Orange .done(); ``` ### Line Annotations Lines and arrows between two points. ```rust // Simple line page.line((100.0, 500.0), (300.0, 500.0)); // Arrow page.arrow((100.0, 470.0), (300.0, 470.0)); // Double-headed arrow page.double_arrow((100.0, 440.0), (300.0, 440.0)); ``` ### Shape Annotations (Square, Circle) Rectangles and ellipses. ```rust use pdf_oxide::geometry::Rect; // Rectangle outline page.rectangle(Rect::new(72.0, 400.0, 150.0, 80.0)); // Filled rectangle page.rectangle_filled( Rect::new(250.0, 400.0, 150.0, 80.0), (0.0, 0.0, 1.0), // Blue stroke (0.8, 0.8, 1.0), // Light blue fill ); // Circle outline page.circle(Rect::new(72.0, 300.0, 80.0, 80.0)); // Filled circle page.circle_filled( Rect::new(180.0, 300.0, 80.0, 80.0), (1.0, 0.0, 0.0), // Red stroke (1.0, 0.9, 0.9), // Light red fill ); ``` ### Polygon and Polyline Annotations Closed polygons and open polylines. ```rust // Closed triangle page.polygon(vec![(200.0, 250.0), (250.0, 300.0), (150.0, 300.0)]); // Filled polygon page.polygon_filled( vec![(300.0, 250.0), (350.0, 300.0), (250.0, 300.0)], (0.0, 0.5, 0.0), // Green stroke (0.8, 1.0, 0.8), // Light green fill ); // Open polyline (zigzag) page.polyline(vec![ (72.0, 200.0), (150.0, 230.0), (220.0, 200.0), (300.0, 230.0), ]); ``` ### Ink Annotations (Freehand Drawing) Freehand strokes and drawings. ```rust // Single stroke page.ink(vec![(100.0, 150.0), (120.0, 170.0), (140.0, 150.0), (160.0, 170.0)]); // Multiple strokes page.freehand(vec![ vec![(100.0, 100.0), (200.0, 100.0)], // Horizontal line vec![(150.0, 50.0), (150.0, 150.0)], // Vertical line ]); // Styled ink page.ink_styled( vec![(200.0, 150.0), (250.0, 180.0), (300.0, 150.0)], (1.0, 0.0, 0.0), // Red 3.0, // 3pt line width ); ``` ### Stamp Annotations Standard rubber-stamp annotations. ```rust use pdf_oxide::writer::StampType; use pdf_oxide::geometry::Rect; page.stamp(Rect::new(400.0, 700.0, 150.0, 50.0), StampType::Approved); page.stamp_approved(Rect::new(400.0, 640.0, 150.0, 50.0)); page.stamp_draft(Rect::new(400.0, 580.0, 120.0, 40.0)); page.stamp_confidential(Rect::new(400.0, 520.0, 150.0, 50.0)); page.stamp_final(Rect::new(400.0, 460.0, 100.0, 40.0)); page.stamp_not_approved(Rect::new(400.0, 400.0, 150.0, 50.0)); page.stamp_for_comment(Rect::new(400.0, 340.0, 150.0, 50.0)); page.stamp_custom(Rect::new(400.0, 280.0, 150.0, 50.0), "ReviewPending"); ``` **StampType variants:** `Approved`, `Experimental`, `NotApproved`, `AsIs`, `Expired`, `NotForPublicRelease`, `Confidential`, `Final`, `Sold`, `Departmental`, `ForComment`, `TopSecret`, `Draft`, `ForPublicRelease`, `Custom(String)` ### Watermark Annotations Page-level watermarks that appear behind content. ```rust // FluentPageBuilder API builder.page(PageSize::Letter) .watermark("DRAFT") .done(); builder.page(PageSize::Letter) .watermark_confidential() .done(); builder.page(PageSize::Letter) .watermark_draft() .done(); ``` ### Redact Annotations Mark regions for redaction. ```rust use pdf_oxide::geometry::Rect; page.redact(Rect::new(72.0, 600.0, 200.0, 20.0)); page.redact_with_text(Rect::new(72.0, 570.0, 200.0, 20.0), "REDACTED"); ``` ### Additional Annotation Types ```rust use pdf_oxide::geometry::Rect; // Popup window page.popup(Rect::new(200.0, 500.0, 200.0, 100.0), true); // Caret (text insertion marker) page.caret(Rect::new(72.0, 450.0, 20.0, 20.0)); page.caret_paragraph(Rect::new(72.0, 420.0, 20.0, 20.0)); page.caret_with_comment(Rect::new(72.0, 390.0, 20.0, 20.0), "Insert paragraph here"); // File attachment page.file_attachment(Rect::new(72.0, 350.0, 24.0, 24.0), "report.xlsx"); page.file_attachment_paperclip(Rect::new(72.0, 320.0, 24.0, 24.0), "notes.txt"); ``` ### Generic Annotation Method Add any annotation type using the generic `add_annotation()` method: ```rust use pdf_oxide::writer::{LinkAnnotation, Annotation}; use pdf_oxide::geometry::Rect; let link = LinkAnnotation::uri( Rect::new(72.0, 720.0, 100.0, 12.0), "https://example.com", ); page.add_annotation(link); ``` ## Advanced Example ### Full Annotation Showcase ```rust use pdf_oxide::writer::{PdfWriter, StampType}; use pdf_oxide::geometry::Rect; let mut writer = PdfWriter::new(); { let mut page = writer.add_letter_page(); // Links page.add_text("Visit Rust", 72.0, 750.0, "Helvetica", 12.0); page.link(Rect::new(72.0, 750.0, 70.0, 12.0), "https://rust-lang.org"); // Text markup page.highlight_rect(Rect::new(72.0, 720.0, 150.0, 12.0)); page.underline_rect(Rect::new(72.0, 700.0, 150.0, 12.0)); page.strikeout_rect(Rect::new(72.0, 680.0, 150.0, 12.0)); page.squiggly_rect(Rect::new(72.0, 660.0, 150.0, 12.0)); // Notes page.sticky_note(Rect::new(300.0, 720.0, 24.0, 24.0), "Important"); page.comment(Rect::new(340.0, 720.0, 24.0, 24.0), "Review needed"); // Shapes page.line((72.0, 620.0), (250.0, 620.0)); page.arrow((72.0, 600.0), (250.0, 600.0)); page.rectangle(Rect::new(72.0, 540.0, 100.0, 50.0)); page.circle(Rect::new(200.0, 540.0, 50.0, 50.0)); // Ink page.ink(vec![(72.0, 500.0), (120.0, 520.0), (170.0, 500.0)]); // Stamp page.stamp_approved(Rect::new(400.0, 720.0, 150.0, 50.0)); // Redact page.redact(Rect::new(72.0, 450.0, 200.0, 15.0)); page.finish(); } writer.save("annotation_showcase.pdf")?; ``` ## Related Pages - [DocumentBuilder Low-Level API](/docs/creation/document-builder) -- Fluent page building with annotations - [Form Field Creation](/docs/creation/forms) -- Interactive form fields - [Graphics, Patterns and Shadings](/docs/creation/graphics) -- Low-level drawing primitives --- # Table Rendering The `Table` API provides a comprehensive table creation system with support for headers, cell styling, borders, backgrounds, column alignment, padding, and row striping. ## Quick Example **Rust** ```rust use pdf_oxide::writer::{Table, TableCell, TableStyle}; let table = Table::new(vec![ vec![TableCell::header("Name"), TableCell::header("Age"), TableCell::header("City")], vec![TableCell::text("Alice"), TableCell::number("30"), TableCell::text("New York")], vec![TableCell::text("Bob"), TableCell::number("25"), TableCell::text("London")], vec![TableCell::text("Carol"), TableCell::number("35"), TableCell::text("Tokyo")], ]) .with_header_row() .with_width(468.0); // Page width minus margins ``` **Python** Table rendering is available through the Rust API. From Python, use Markdown tables with `Pdf.from_markdown()`: ```python from pdf_oxide import Pdf markdown = """ # Employee Directory | Name | Age | City | |-------|-----|----------| | Alice | 30 | New York | | Bob | 25 | London | | Carol | 35 | Tokyo | """ pdf = Pdf.from_markdown(markdown) pdf.save("directory.pdf") ``` ## Full API Reference ### `Table` -- Table Container #### Construction ```rust use pdf_oxide::writer::{Table, TableCell, TableRow}; // From Vec> let table = Table::new(vec![ vec![TableCell::text("A"), TableCell::text("B")], vec![TableCell::text("1"), TableCell::text("2")], ]); // From TableRow objects let table = Table::from_rows(vec![ TableRow::header(vec![TableCell::header("A"), TableCell::header("B")]), TableRow::new(vec![TableCell::text("1"), TableCell::text("2")]), ]); // Empty table with rows added later let mut table = Table::empty(); table.add_row(TableRow::new(vec![TableCell::text("1"), TableCell::text("2")])); ``` #### Configuration Methods | Method | Description | |---------------------------------|------------------------------------------| | `.with_header_row()` | Mark first row as header | | `.with_style(style)` | Set table style | | `.with_width(width)` | Set total table width in points | | `.with_column_widths(widths)` | Set column width specifications | | `.with_column_aligns(aligns)` | Set default column alignments | #### Properties | Method | Returns | Description | |-----------------|----------|---------------------------| | `.num_columns()`| `usize` | Number of columns | | `.num_rows()` | `usize` | Number of rows | | `.is_empty()` | `bool` | Whether table has no rows | ### `TableCell` -- Cell Content ```rust use pdf_oxide::writer::{TableCell, CellAlign, CellVAlign, CellPadding, Borders}; // Basic cells let cell = TableCell::text("Hello"); let empty = TableCell::empty(); // Semantic cell types let header = TableCell::header("Column Name"); // Centered + bold let number = TableCell::number("$1,234.56"); // Right-aligned // Full configuration let cell = TableCell::text("Important") .colspan(2) // Span 2 columns .rowspan(3) // Span 3 rows .align(CellAlign::Center) // Horizontal alignment .valign(CellVAlign::Middle) // Vertical alignment .padding(CellPadding::uniform(8.0)) // Cell padding .borders(Borders::all(TableBorderStyle::thick())) .background(0.95, 0.95, 1.0) // Light blue background .font("Helvetica-Bold", 14.0) // Custom font .bold() // Bold text .italic(); // Italic text ``` ### `TableRow` -- Row Container ```rust use pdf_oxide::writer::{TableRow, TableCell}; // Data row let row = TableRow::new(vec![ TableCell::text("Alice"), TableCell::number("30"), ]); // Header row let header = TableRow::header(vec![ TableCell::header("Name"), TableCell::header("Age"), ]); // Configured row let row = TableRow::new(vec![TableCell::text("Total"), TableCell::number("$500")]) .min_height(30.0) .background(0.9, 0.9, 0.9) // Gray background .as_header(); ``` ### `TableStyle` -- Style Configuration ```rust use pdf_oxide::writer::{ TableStyle, TableBorderStyle, Borders, CellPadding, }; // Default style (thin borders, gray header, Helvetica 10pt) let style = TableStyle::new(); // Custom style let style = TableStyle::new() .cell_padding(CellPadding::symmetric(8.0, 6.0)) .cell_borders(Borders::all(TableBorderStyle::medium())) .outer_border(TableBorderStyle::thick()) .font("Times-Roman", 11.0) .header_background(0.2, 0.4, 0.8) // Blue header .striped(0.95, 0.95, 0.95); // Light gray stripes // Preset styles let minimal = TableStyle::minimal(); // No borders let bordered = TableStyle::bordered(); // Thick outer, medium inner ``` ### `TableBorderStyle` -- Border Appearance ```rust use pdf_oxide::writer::TableBorderStyle; let border = TableBorderStyle::new(0.5); // Custom width let border = TableBorderStyle::thin(); // 0.25pt let border = TableBorderStyle::medium(); // 0.5pt let border = TableBorderStyle::thick(); // 1.0pt let border = TableBorderStyle::none(); // No border let border = TableBorderStyle::medium() .with_color(0.0, 0.0, 0.8); // Blue border ``` ### `Borders` -- Four-Side Border Configuration ```rust use pdf_oxide::writer::{Borders, TableBorderStyle}; let borders = Borders::none(); let borders = Borders::all(TableBorderStyle::thin()); let borders = Borders::horizontal(TableBorderStyle::medium()); // Top + bottom let borders = Borders::vertical(TableBorderStyle::medium()); // Left + right // Individual sides let borders = Borders::none() .with_top(TableBorderStyle::thick()) .with_bottom(TableBorderStyle::thick()) .with_left(TableBorderStyle::thin()) .with_right(TableBorderStyle::thin()); ``` ### `CellPadding` -- Padding Configuration ```rust use pdf_oxide::writer::CellPadding; let padding = CellPadding::uniform(8.0); // All sides 8pt let padding = CellPadding::symmetric(10.0, 6.0); // H=10, V=6 let padding = CellPadding::none(); // No padding let padding = CellPadding { top: 4.0, right: 8.0, bottom: 4.0, left: 8.0, }; ``` ### `ColumnWidth` -- Column Width Specification ```rust use pdf_oxide::writer::ColumnWidth; let widths = vec![ ColumnWidth::Auto, // Automatic sizing ColumnWidth::Fixed(150.0), // Fixed 150pt ColumnWidth::Percent(30.0), // 30% of table width ColumnWidth::Weight(2.0), // Proportional flex weight ]; ``` ### `CellAlign` / `CellVAlign` -- Alignment ```rust use pdf_oxide::writer::{CellAlign, CellVAlign}; // Horizontal let align = CellAlign::Left; let align = CellAlign::Center; let align = CellAlign::Right; // Vertical let valign = CellVAlign::Top; let valign = CellVAlign::Middle; let valign = CellVAlign::Bottom; ``` ## Advanced Examples ### Financial Report Table ```rust use pdf_oxide::writer::{ Table, TableCell, TableRow, TableStyle, TableBorderStyle, Borders, CellPadding, CellAlign, ColumnWidth, }; let style = TableStyle::new() .cell_padding(CellPadding::symmetric(10.0, 6.0)) .cell_borders(Borders::horizontal(TableBorderStyle::thin())) .outer_border(TableBorderStyle::medium()) .font("Helvetica", 10.0) .header_background(0.15, 0.3, 0.55) .striped(0.96, 0.96, 0.96); let table = Table::from_rows(vec![ TableRow::header(vec![ TableCell::header("Category"), TableCell::header("Q1"), TableCell::header("Q2"), TableCell::header("Q3"), TableCell::header("Q4"), TableCell::header("Total"), ]), TableRow::new(vec![ TableCell::text("Revenue"), TableCell::number("$32M"), TableCell::number("$35M"), TableCell::number("$38M"), TableCell::number("$42M"), TableCell::number("$147M").bold(), ]), TableRow::new(vec![ TableCell::text("Expenses"), TableCell::number("$24M"), TableCell::number("$25M"), TableCell::number("$27M"), TableCell::number("$29M"), TableCell::number("$105M").bold(), ]), TableRow::new(vec![ TableCell::text("Net Income").bold(), TableCell::number("$8M").bold(), TableCell::number("$10M").bold(), TableCell::number("$11M").bold(), TableCell::number("$13M").bold(), TableCell::number("$42M").bold(), ]).background(0.9, 0.95, 0.9), ]) .with_style(style) .with_width(468.0) .with_column_widths(vec![ ColumnWidth::Fixed(100.0), ColumnWidth::Weight(1.0), ColumnWidth::Weight(1.0), ColumnWidth::Weight(1.0), ColumnWidth::Weight(1.0), ColumnWidth::Fixed(80.0), ]) .with_column_aligns(vec![ CellAlign::Left, CellAlign::Right, CellAlign::Right, CellAlign::Right, CellAlign::Right, CellAlign::Right, ]); ``` ### Table with Spanning Cells ```rust use pdf_oxide::writer::{Table, TableCell, TableRow}; let table = Table::from_rows(vec![ TableRow::header(vec![ TableCell::header("Product").colspan(2), TableCell::header("Sales"), ]), TableRow::new(vec![ TableCell::text("Category A").rowspan(2), TableCell::text("Widget X"), TableCell::number("1,234"), ]), TableRow::new(vec![ // First cell omitted due to rowspan above TableCell::text("Widget Y"), TableCell::number("567"), ]), ]) .with_header_row() .with_width(400.0); ``` ## Related Pages - [DocumentBuilder Low-Level API](/docs/creation/document-builder) -- Page-level content construction - [Graphics, Patterns and Shadings](/docs/creation/graphics) -- Drawing primitives - [Create from Markdown](/docs/creation/from-markdown) -- Markdown tables auto-convert to PDF --- # Graphics, Patterns & Shadings PDF Oxide provides low-level graphics primitives through `ContentStreamBuilder` for drawing paths and shapes, `TilingPatternBuilder` for repeating patterns, gradient builders for linear and radial gradients, and `ExtGStateBuilder` for transparency and blend modes. ## Quick Example **Rust** ```rust use pdf_oxide::writer::{ContentStreamBuilder, LineCap, LineJoin}; let mut builder = ContentStreamBuilder::new(); builder .save_state() .set_stroke_color(0.0, 0.0, 1.0) // Blue stroke .set_fill_color(0.8, 0.8, 1.0) // Light blue fill .set_line_width(2.0) .rect(72.0, 600.0, 200.0, 100.0) .fill_and_stroke() .restore_state(); ``` ## ContentStreamBuilder -- Drawing Primitives `ContentStreamBuilder` generates PDF content stream operators for rendering graphics on a page. ### Path Operations ```rust use pdf_oxide::writer::ContentStreamBuilder; let mut cs = ContentStreamBuilder::new(); // Move/Line/Curve cs.move_to(72.0, 700.0) .line_to(200.0, 700.0) .line_to(200.0, 600.0) .close_path() .stroke(); // Rectangle cs.rect(72.0, 500.0, 150.0, 80.0) .fill(); // Bezier curve cs.move_to(72.0, 400.0) .curve_to(100.0, 450.0, 200.0, 350.0, 250.0, 400.0) .stroke(); ``` ### Color Operations ```rust // RGB colors (0.0 to 1.0) cs.set_fill_color(1.0, 0.0, 0.0); // Red fill cs.set_stroke_color(0.0, 0.5, 0.0); // Green stroke // Grayscale cs.set_fill_color_gray(0.5); // 50% gray fill cs.set_stroke_color_gray(0.0); // Black stroke // CMYK cs.set_fill_color_cmyk(0.0, 1.0, 1.0, 0.0); // Red in CMYK cs.set_stroke_color_cmyk(1.0, 0.0, 0.0, 0.0); // Cyan stroke ``` ### Line Style ```rust use pdf_oxide::writer::{ContentStreamBuilder, LineCap, LineJoin}; let mut cs = ContentStreamBuilder::new(); cs.set_line_width(2.0) .set_line_cap(LineCap::Round) .set_line_join(LineJoin::Round) .set_miter_limit(10.0) .set_dash_pattern(vec![5.0, 3.0], 0.0); // 5pt dash, 3pt gap ``` **LineCap variants:** `Butt` (default), `Round`, `Square` **LineJoin variants:** `Miter` (default), `Round`, `Bevel` ### Graphics State ```rust // Save/restore state for isolated operations cs.save_state() .set_fill_color(1.0, 0.0, 0.0) .rect(100.0, 100.0, 50.0, 50.0) .fill() .restore_state(); // State is restored to what it was before save_state() ``` ### Path Filling and Stroking | Method | Description | |--------------------------------|------------------------------------------| | `.stroke()` | Stroke the path outline | | `.fill()` | Fill the path interior (non-zero winding)| | `.fill_even_odd()` | Fill using even-odd rule | | `.fill_and_stroke()` | Fill and stroke | | `.fill_and_stroke_even_odd()` | Fill (even-odd) and stroke | | `.close_and_stroke()` | Close path then stroke | | `.close_fill_and_stroke()` | Close, fill, and stroke | | `.end_path()` | End path without painting | ### Clipping ```rust // Clip to rectangle, then draw inside cs.save_state() .rect(100.0, 100.0, 200.0, 200.0) .clip() .end_path() // Everything drawn here is clipped to the rectangle .set_fill_color(1.0, 0.0, 0.0) .rect(50.0, 50.0, 300.0, 300.0) // Only visible within clip .fill() .restore_state(); ``` ### Transformations ```rust // Apply transformation matrix [a b c d e f] cs.save_state() .transform(1.0, 0.0, 0.0, 1.0, 100.0, 200.0) // Translate .rect(0.0, 0.0, 50.0, 50.0) .fill() .restore_state(); ``` ### ContentStreamOp Enum For maximum control, build operations directly: ```rust use pdf_oxide::writer::ContentStreamOp; let ops = vec![ ContentStreamOp::SaveState, ContentStreamOp::SetLineWidth(2.0), ContentStreamOp::SetStrokeColorRGB(0.0, 0.0, 1.0), ContentStreamOp::MoveTo(72.0, 500.0), ContentStreamOp::LineTo(300.0, 500.0), ContentStreamOp::Stroke, ContentStreamOp::RestoreState, ]; ``` ## TilingPatternBuilder -- Repeating Patterns Tiling patterns repeat a small cell across an area. ```rust use pdf_oxide::writer::{TilingPatternBuilder, PatternPaintType, PatternTilingType}; // Striped pattern let (pattern_dict, content_bytes) = TilingPatternBuilder::new() .bbox(0.0, 0.0, 10.0, 10.0) .x_step(10.0) .y_step(10.0) .colored() .tiling_type(PatternTilingType::ConstantSpacing) .content_bytes(b"0.8 0 0 rg 0 0 5 10 re f".to_vec()) .build(); ``` ### Configuration Methods | Method | Description | |-------------------------------|------------------------------------------| | `.bbox(x, y, w, h)` | Set bounding box of pattern cell | | `.x_step(step)` | Horizontal spacing between cells | | `.y_step(step)` | Vertical spacing between cells | | `.step(x, y)` | Set both steps at once | | `.colored()` | Colors defined in pattern content | | `.uncolored()` | Color specified when pattern is used | | `.tiling_type(type)` | Set tiling algorithm | | `.matrix(a, b, c, d, e, f)` | Apply transformation to pattern | | `.content_bytes(bytes)` | Set raw content stream bytes | | `.build()` | Returns `(Object, Vec)` | ### PatternPresets PDF Oxide includes preset patterns for common use cases: ```rust use pdf_oxide::writer::PatternPresets; // Access presets for common patterns like hatching, dots, etc. ``` ## LinearGradientBuilder -- Linear Gradients Create axial (linear) gradient shadings. ```rust use pdf_oxide::writer::{LinearGradientBuilder, GradientStop}; use pdf_oxide::layout::Color; let (shading_dict, function_dict) = LinearGradientBuilder::new() .from(0.0, 0.0) .to(468.0, 0.0) .add_stop(0.0, Color { r: 1.0, g: 0.0, b: 0.0 }) // Red .add_stop(0.5, Color { r: 1.0, g: 1.0, b: 0.0 }) // Yellow .add_stop(1.0, Color { r: 0.0, g: 0.0, b: 1.0 }) // Blue .extend(true) .build(); ``` ### Two-Color Shortcut ```rust use pdf_oxide::writer::LinearGradientBuilder; use pdf_oxide::layout::Color; let gradient = LinearGradientBuilder::two_color( Color { r: 0.0, g: 0.0, b: 0.5 }, // Dark blue Color { r: 0.5, g: 0.8, b: 1.0 }, // Light blue ); ``` ### Configuration Methods | Method | Description | |-------------------------|------------------------------------------| | `.from(x, y)` | Start point of gradient | | `.to(x, y)` | End point of gradient | | `.add_stop(pos, color)` | Add color stop (position 0.0-1.0) | | `.extend_start(bool)` | Extend gradient before start point | | `.extend_end(bool)` | Extend gradient after end point | | `.extend(bool)` | Set both extend flags | ## RadialGradientBuilder -- Radial Gradients Create circular gradient shadings. ```rust use pdf_oxide::writer::RadialGradientBuilder; use pdf_oxide::layout::Color; let (shading_dict, function_dict) = RadialGradientBuilder::new() .center(200.0, 400.0) .radius(0.0, 150.0) // Inner radius 0, outer radius 150 .add_stop(0.0, Color { r: 1.0, g: 1.0, b: 1.0 }) // White center .add_stop(1.0, Color { r: 0.0, g: 0.0, b: 0.5 }) // Dark blue edge .build(); ``` ## ExtGStateBuilder -- Transparency & Blend Modes Control transparency, blend modes, and other graphics state parameters. ```rust use pdf_oxide::writer::{ExtGStateBuilder, BlendMode}; let gs_dict = ExtGStateBuilder::new() .fill_alpha(0.5) // 50% transparent fill .stroke_alpha(0.8) // 80% opaque stroke .blend_mode(BlendMode::Multiply) .build(); ``` ### Configuration Methods | Method | Description | |---------------------------|------------------------------------------| | `.fill_alpha(a)` | Fill opacity (0.0 transparent, 1.0 opaque) | | `.stroke_alpha(a)` | Stroke opacity | | `.blend_mode(mode)` | Set blend mode | | `.line_width(w)` | Override line width | | `.line_cap(cap)` | Override line cap style | | `.line_join(join)` | Override line join style | | `.miter_limit(limit)` | Override miter limit | | `.flatness(f)` | Flatness tolerance | | `.overprint_stroke(b)` | Overprint mode for stroke | | `.overprint_fill(b)` | Overprint mode for fill | ### BlendMode Variants `Normal`, `Multiply`, `Screen`, `Overlay`, `Darken`, `Lighten`, `ColorDodge`, `ColorBurn`, `HardLight`, `SoftLight` ## GradientPresets Convenience methods for common gradient patterns: ```rust use pdf_oxide::writer::GradientPresets; // Access preset gradient configurations ``` ## Advanced Example ### Drawing a Chart Background with Gradient ```rust use pdf_oxide::writer::{ ContentStreamBuilder, LinearGradientBuilder, ExtGStateBuilder, BlendMode, }; use pdf_oxide::layout::Color; let mut cs = ContentStreamBuilder::new(); // Draw chart area with rounded-corner appearance cs.save_state() .set_fill_color(0.95, 0.95, 0.97) .rect(72.0, 200.0, 468.0, 400.0) .fill() .restore_state(); // Draw grid lines cs.save_state() .set_stroke_color(0.85, 0.85, 0.85) .set_line_width(0.5); for i in 0..5 { let y = 200.0 + (i as f32 * 100.0); cs.move_to(72.0, y).line_to(540.0, y).stroke(); } cs.restore_state(); // Draw data bars let values = [280.0, 350.0, 180.0, 420.0, 310.0]; let colors = [ (0.2, 0.5, 0.8), (0.3, 0.7, 0.4), (0.8, 0.3, 0.3), (0.6, 0.4, 0.8), (0.9, 0.6, 0.2), ]; for (i, (&val, &(r, g, b))) in values.iter().zip(colors.iter()).enumerate() { let x = 100.0 + (i as f32 * 85.0); cs.save_state() .set_fill_color(r, g, b) .rect(x, 200.0, 50.0, val) .fill() .restore_state(); } ``` ## Related Pages - [DocumentBuilder Low-Level API](/docs/creation/document-builder) -- High-level page construction - [Table Rendering](/docs/creation/tables) -- Table creation API - [Annotation Creation](/docs/creation/annotations) -- Adding annotations to pages --- # QR Codes & Barcodes Generate QR codes and 1D barcodes directly as PDF documents. Barcode generation is behind a feature flag to keep the core library lightweight. **Requires feature flag:** `barcodes` ```toml # Cargo.toml [dependencies] pdf_oxide = { version = "0.3", features = ["barcodes"] } ``` ## Quick Example **Python** ```python from pdf_oxide import Pdf # QR code pdf = Pdf.from_qrcode("https://example.com") pdf.save("qrcode.pdf") # Barcode pdf = Pdf.from_barcode("Code128", "ABC-12345") pdf.save("barcode.pdf") ``` **Rust** ```rust use pdf_oxide::api::Pdf; // QR code let pdf = Pdf::from_qrcode("https://example.com")?; pdf.save("qrcode.pdf")?; // Barcode use pdf_oxide::writer::barcode::BarcodeType; let pdf = Pdf::from_barcode(BarcodeType::Code128, "ABC-12345")?; pdf.save("barcode.pdf")?; ``` ## Full API Reference ### QR Code Generation #### `Pdf::from_qrcode(data)` -- Default QR Code Creates a single-page PDF with a 300px QR code. **Rust:** ```rust use pdf_oxide::api::Pdf; let pdf = Pdf::from_qrcode("https://example.com")?; pdf.save("qr.pdf")?; ``` **Python:** ```python from pdf_oxide import Pdf pdf = Pdf.from_qrcode("https://example.com") pdf.save("qr.pdf") ``` **Python Signature:** ```python Pdf.from_qrcode(data: str) -> Pdf ``` #### `Pdf::from_qrcode_with_options(data, options)` -- Custom QR Code Full control over size, error correction, colors, and quiet zone. **Rust:** ```rust use pdf_oxide::api::Pdf; use pdf_oxide::writer::barcode::{QrCodeOptions, QrErrorCorrection}; let options = QrCodeOptions::new() .size(400) .error_correction(QrErrorCorrection::High) .quiet_zone(6) .foreground(0, 0, 128, 255) // Navy blue .background(255, 255, 255, 255); // White let pdf = Pdf::from_qrcode_with_options("https://example.com", &options)?; pdf.save("custom_qr.pdf")?; ``` #### `QrCodeOptions` -- QR Configuration | Method | Default | Description | |-------------------------------|--------------------|-------------------------------------| | `.size(px)` | 200 | QR code size in pixels | | `.error_correction(level)` | `Medium` | Error correction level | | `.quiet_zone(modules)` | 4 | Border width in modules | | `.foreground(r, g, b, a)` | Black (0,0,0,255) | Module color (RGBA) | | `.background(r, g, b, a)` | White (255,255,255,255) | Background color (RGBA) | #### `QrErrorCorrection` Enum | Variant | Recovery Capacity | Use Case | |------------|-------------------|-----------------------------------| | `Low` | ~7% | Maximum data density | | `Medium` | ~15% | General purpose (default) | | `Quartile` | ~25% | Labels that may get damaged | | `High` | ~30% | Industrial / harsh environments | ### 1D Barcode Generation #### `Pdf::from_barcode(barcode_type, data)` -- Default Barcode Creates a single-page PDF with a barcode using default size (200x80). **Rust:** ```rust use pdf_oxide::api::Pdf; use pdf_oxide::writer::barcode::BarcodeType; let pdf = Pdf::from_barcode(BarcodeType::Ean13, "5901234123457")?; pdf.save("product.pdf")?; ``` **Python:** ```python from pdf_oxide import Pdf # Barcode type as string: "code128", "ean13", "upca", "code39", "ean8", "itf" pdf = Pdf.from_barcode("ean13", "5901234123457") pdf.save("product.pdf") ``` **Python Signature:** ```python Pdf.from_barcode( barcode_type: str, # "code128" | "ean13" | "upca" | "code39" | "ean8" | "itf" data: str ) -> Pdf ``` #### `Pdf::from_barcode_with_options(barcode_type, data, options)` -- Custom Barcode Full control over barcode dimensions and colors. **Rust:** ```rust use pdf_oxide::api::Pdf; use pdf_oxide::writer::barcode::{BarcodeType, BarcodeOptions}; let options = BarcodeOptions::new() .width(400) .height(120) .foreground(0, 0, 0, 255) .background(255, 255, 255, 255); let pdf = Pdf::from_barcode_with_options( BarcodeType::Code128, "SHIP-2025-00042", &options, )?; pdf.save("shipping_label.pdf")?; ``` #### `BarcodeOptions` -- 1D Configuration | Method | Default | Description | |-------------------------------|---------------------|-----------------------------------| | `.width(px)` | 200 | Barcode width in pixels | | `.height(px)` | 80 | Barcode height in pixels | | `.foreground(r, g, b, a)` | Black (0,0,0,255) | Bar color (RGBA) | | `.background(r, g, b, a)` | White (255,255,255,255) | Background color (RGBA) | | `.show_text(bool)` | false | Show human-readable text | ### `BarcodeType` Enum | Variant | Name | Data Format | |------------|----------------------------|------------------------------------| | `Code128` | Code 128 | Alphanumeric (auto-selects A/B/C) | | `Code39` | Code 39 | Uppercase alphanumeric + symbols | | `Ean13` | EAN-13 | 13 digits (European Article Number)| | `Ean8` | EAN-8 | 8 digits (compact EAN) | | `UpcA` | UPC-A | 11-12 digits (Universal Product Code)| | `Itf` | Interleaved 2 of 5 | Numeric pairs (even digit count) | | `Code93` | Code 93 | Alphanumeric (compact) | | `Codabar` | Codabar | Digits + special chars (A-D start/stop)| ### Low-Level BarcodeGenerator For generating barcode images without creating a full PDF (e.g., to embed in an existing document): ```rust use pdf_oxide::writer::barcode::{BarcodeGenerator, BarcodeType, BarcodeOptions, QrCodeOptions}; // Generate QR code as PNG bytes let qr_png = BarcodeGenerator::generate_qr( "https://example.com", &QrCodeOptions::default().size(256), )?; // Generate QR code with simple API let qr_png = BarcodeGenerator::generate_qr_simple("https://example.com", 200)?; // Generate 1D barcode as PNG bytes let barcode_png = BarcodeGenerator::generate_1d( BarcodeType::Code128, "ABC123", &BarcodeOptions::default().width(300).height(100), )?; // Convenience methods let code128_png = BarcodeGenerator::generate_code128("DATA", 200, 80)?; let ean13_png = BarcodeGenerator::generate_ean13("5901234123457", 200, 80)?; ``` ### Using PdfBuilder with Barcodes `PdfBuilder` provides a fluent interface for setting page size and document metadata before generating barcode PDFs. ```rust use pdf_oxide::api::PdfBuilder; use pdf_oxide::writer::PageSize; use pdf_oxide::writer::barcode::{BarcodeType, QrCodeOptions, QrErrorCorrection}; // QR code with custom page size let pdf = PdfBuilder::new() .title("WiFi Access") .page_size(PageSize::Custom(300.0, 300.0)) .from_qrcode("WIFI:T:WPA;S:MyNetwork;P:secret123;;")?; pdf.save("wifi_qr.pdf")?; // Barcode with metadata let pdf = PdfBuilder::new() .title("Product Label") .author("Warehouse System") .from_barcode(BarcodeType::Ean13, "5901234123457")?; pdf.save("label.pdf")?; ``` ## Advanced Examples ### Shipping Label with Multiple Barcodes ```rust use pdf_oxide::writer::barcode::{BarcodeGenerator, BarcodeType, BarcodeOptions, QrCodeOptions}; use pdf_oxide::writer::{PdfWriter, PdfWriterConfig}; use pdf_oxide::geometry::Rect; // Generate barcode images let tracking_barcode = BarcodeGenerator::generate_code128( "1Z999AA10123456784", 300, 80, )?; let qr_bytes = BarcodeGenerator::generate_qr( "https://track.example.com/1Z999AA10123456784", &QrCodeOptions::new().size(200).error_correction(QrErrorCorrection::Medium), )?; // These PNG bytes can then be embedded in a PDF using the image APIs ``` ### Batch Generate Product Labels ```rust use pdf_oxide::api::Pdf; use pdf_oxide::writer::barcode::BarcodeType; let products = vec![ ("5901234123457", "Widget A"), ("4006381333931", "Widget B"), ("0012345678905", "Widget C"), ]; for (ean, name) in products { let pdf = Pdf::from_barcode(BarcodeType::Ean13, ean)?; pdf.save(format!("label_{}.pdf", name.to_lowercase().replace(' ', "_")))?; } ``` ### Python: Generate and Save QR Codes ```python from pdf_oxide import Pdf urls = [ "https://example.com/product/1", "https://example.com/product/2", "https://example.com/product/3", ] for i, url in enumerate(urls): pdf = Pdf.from_qrcode(url) pdf.save(f"qr_product_{i + 1}.pdf") ``` ### Python: Batch Barcode Generation ```python from pdf_oxide import Pdf items = [ ("code128", "SHIP-00001"), ("code128", "SHIP-00002"), ("ean13", "5901234123457"), ("upca", "012345678905"), ] for barcode_type, data in items: pdf = Pdf.from_barcode(barcode_type, data) pdf.save(f"barcode_{data}.pdf") ``` ## Feature Flag The `barcodes` feature pulls in two additional dependencies: - `barcoders` -- 1D barcode encoding - `qrcode` -- QR code encoding Without the feature flag, all barcode methods return an error message indicating the feature is required. ```rust // Without the barcodes feature enabled: let result = Pdf::from_qrcode("test"); // Returns Err("QR code generation requires the 'barcodes' feature") ``` ## Related Pages - [PdfBuilder Fluent API](/docs/creation/builder) -- Configure page size and metadata for barcode PDFs - [Create from Images](/docs/creation/from-images) -- Embed barcode PNGs in multi-content PDFs - [Graphics, Patterns and Shadings](/docs/creation/graphics) -- Low-level drawing for custom label layouts - [Types & Enums](/docs/reference/types) -- BarcodeType enum details --- # Convert Office Documents to PDF Convert Microsoft Office documents (Word, Excel, PowerPoint) to PDF without Microsoft Office or LibreOffice installed. ## Quick Example **Python** ```python from pdf_oxide import OfficeConverter pdf = OfficeConverter.convert("report.docx") pdf.save("report.pdf") ``` **Rust** ```rust use pdf_oxide::converters::office::OfficeConverter; let converter = OfficeConverter::new(); let pdf_bytes = converter.convert("report.docx")?; std::fs::write("report.pdf", pdf_bytes)?; ``` ## Supported Formats | Format | Extension | Description | |--------|-----------|-------------| | DOCX | `.docx` | Word documents | | XLSX | `.xlsx` | Excel spreadsheets | | PPTX | `.pptx` | PowerPoint presentations | ## Format-Specific Methods **Python** ```python pdf = OfficeConverter.from_docx("document.docx") pdf = OfficeConverter.from_xlsx("data.xlsx") pdf = OfficeConverter.from_pptx("slides.pptx") # From bytes with open("document.docx", "rb") as f: pdf = OfficeConverter.from_docx_bytes(f.read()) pdf.save("output.pdf") ``` **Rust** ```rust let converter = OfficeConverter::new(); let pdf_bytes = converter.convert_docx("document.docx")?; let pdf_bytes = converter.convert_xlsx("data.xlsx")?; let pdf_bytes = converter.convert_pptx("slides.pptx")?; // From bytes let docx_bytes = std::fs::read("document.docx")?; let pdf_bytes = converter.convert_docx_bytes(&docx_bytes)?; ``` ## Configuration (Rust) ```rust use pdf_oxide::converters::office::{OfficeConverter, OfficeConfig}; let config = OfficeConfig::a4(); let converter = OfficeConverter::with_config(config); let pdf_bytes = converter.convert_docx("document.docx")?; ``` ### OfficeConfig Fields | Field | Type | Default | Description | |-------|------|---------|-------------| | `page_size` | `PageSize` | Letter | Page dimensions | | `margins` | `Margins` | 1 inch | Page margins in points | | `embed_fonts` | `bool` | `false` | Embed fonts | | `default_font` | `String` | `"Helvetica"` | Fallback font | | `default_font_size` | `f32` | `11.0` | Default text size | | `line_height` | `f32` | `1.2` | Line height multiplier | --- # Editing Overview PDF Oxide provides two levels of API for editing existing PDFs: the high-level `Pdf` class (recommended) and the lower-level `DocumentEditor`. Both allow you to open a PDF, modify its content and metadata, track changes, and save the result. ## Opening a PDF for Editing **Python** ```python from pdf_oxide import PdfDocument doc = PdfDocument("input.pdf") ``` The editor is initialized lazily on the first modification. You can start reading immediately and the editor activates when you call any mutating method such as `set_title()` or `page()`. **Rust** Use the unified `Pdf` API: ```rust use pdf_oxide::api::Pdf; let mut doc = Pdf::open("input.pdf")?; ``` Or use `DocumentEditor` directly for lower-level control: ```rust use pdf_oxide::editor::DocumentEditor; let mut editor = DocumentEditor::open("input.pdf")?; ``` ## Checking for Modifications Before saving, you can check whether any changes have been made: **Python** ```python doc = PdfDocument("input.pdf") print(doc.is_modified) # False -- no changes yet doc.set_title("Updated Title") print(doc.is_modified) # True ``` **Rust** ```rust let mut doc = Pdf::open("input.pdf")?; assert!(!doc.is_modified()); doc.editor().unwrap().set_title("Updated Title"); assert!(doc.is_modified()); ``` ## Saving **Python** ```python doc = PdfDocument("input.pdf") doc.set_title("New Title") doc.save("output.pdf") ``` **Rust** ```rust let mut doc = Pdf::open("input.pdf")?; doc.editor().unwrap().set_title("New Title"); doc.save("output.pdf")?; // Or save to a new path doc.save_as("copy.pdf")?; ``` The `save()` method performs a full rewrite of the PDF by default. For advanced save options (incremental updates, encryption), see [Encryption & Security](/docs/editing/encryption). ## Document Metadata Read and write the standard PDF metadata fields: title, author, subject, and keywords. **Python** ```python from pdf_oxide import PdfDocument doc = PdfDocument("input.pdf") # Set metadata doc.set_title("Quarterly Report") doc.set_author("Jane Smith") doc.set_subject("Q4 2025 Financial Results") doc.set_keywords("finance, quarterly, 2025") doc.save("output.pdf") ``` **Rust** ```rust use pdf_oxide::editor::DocumentEditor; let mut editor = DocumentEditor::open("input.pdf")?; // Read metadata if let Some(title) = editor.title()? { println!("Current title: {}", title); } if let Some(author) = editor.author()? { println!("Current author: {}", author); } if let Some(subject) = editor.subject()? { println!("Current subject: {}", subject); } if let Some(keywords) = editor.keywords()? { println!("Current keywords: {}", keywords); } // Set metadata editor.set_title("Quarterly Report"); editor.set_author("Jane Smith"); editor.set_subject("Q4 2025 Financial Results"); editor.set_keywords("finance, quarterly, 2025"); editor.save("output.pdf")?; ``` ## Document Information ### Source Path and Version ```rust use pdf_oxide::editor::DocumentEditor; let editor = DocumentEditor::open("input.pdf")?; // Path to the original file println!("Source: {}", editor.source_path()); // PDF version as (major, minor) let (major, minor) = editor.version(); println!("PDF version: {}.{}", major, minor); // Number of pages println!("Pages: {}", editor.current_page_count()); ``` ## Full API Reference ### DocumentEditor | Method | Returns | Description | |--------|---------|-------------| | `open(path)` | `Result` | Open a PDF for editing | | `is_modified()` | `bool` | Check if any changes have been made | | `source_path()` | `&str` | Path to the source PDF | | `source()` | `&PdfDocument` | Read-only access to the source document | | `version()` | `(u8, u8)` | PDF version (major, minor) | | `current_page_count()` | `usize` | Number of pages in the document | | `title()` | `Result>` | Get document title | | `set_title(title)` | `()` | Set document title | | `author()` | `Result>` | Get document author | | `set_author(author)` | `()` | Set document author | | `subject()` | `Result>` | Get document subject | | `set_subject(subject)` | `()` | Set document subject | | `keywords()` | `Result>` | Get document keywords | | `set_keywords(keywords)` | `()` | Set document keywords | | `save(path)` | `Result<()>` | Save with full rewrite | | `save_with_options(path, options)` | `Result<()>` | Save with custom options | ### Pdf (Unified API) | Method | Returns | Description | |--------|---------|-------------| | `Pdf::open(path)` | `Result` | Open a PDF for editing | | `Pdf::open_editor(path)` | `Result` | Open directly as DocumentEditor | | `is_modified()` | `bool` | Check if changes exist | | `save(path)` | `Result<()>` | Save the document | | `save_as(path)` | `Result<()>` | Save to a new path | | `page(index)` | `Result` | Get a page for DOM editing | | `save_page(page)` | `Result<()>` | Save a modified page back | | `editor()` | `Option<&mut DocumentEditor>` | Access the underlying editor | ### EditableDocument Trait The `EditableDocument` trait defines the core editing contract: ```rust pub trait EditableDocument { fn get_info(&mut self) -> Result; fn set_info(&mut self, info: DocumentInfo) -> Result<()>; fn page_count(&mut self) -> Result; fn get_page_info(&mut self, index: usize) -> Result; fn remove_page(&mut self, index: usize) -> Result<()>; fn move_page(&mut self, from: usize, to: usize) -> Result<()>; fn duplicate_page(&mut self, index: usize) -> Result; fn save(&mut self, path: impl AsRef) -> Result<()>; fn save_with_options(&mut self, path: impl AsRef, options: SaveOptions) -> Result<()>; } ``` ## Complete Edit Workflow This example demonstrates a complete editing session: open, inspect, modify metadata, edit content, and save. **Python** ```python from pdf_oxide import PdfDocument # Open the document doc = PdfDocument("report.pdf") print(f"Pages: {doc.page_count}") # Update metadata doc.set_title("Annual Report 2025") doc.set_author("Finance Team") # Edit text on page 0 page = doc.page(0) for text in page.find_text_containing("DRAFT"): page.set_text(text.id, "FINAL") doc.save_page(page) # Save doc.save("report-final.pdf") ``` **Rust** ```rust use pdf_oxide::api::Pdf; let mut doc = Pdf::open("report.pdf")?; println!("Pages: {}", doc.page_count()?); // Update metadata { let editor = doc.editor().unwrap(); editor.set_title("Annual Report 2025"); editor.set_author("Finance Team"); } // Edit text on page 0 let mut page = doc.page(0)?; let drafts = page.find_text_containing("DRAFT"); for t in &drafts { page.set_text(t.id(), "FINAL")?; } doc.save_page(page)?; // Save doc.save("report-final.pdf")?; ``` ## Related Pages - [Text Editing](/docs/editing/text) -- find and replace text, modify fonts and positioning - [Page Operations](/docs/editing/pages) -- rotation, cropping, merging, and extracting pages - [Form Field Editing](/docs/editing/forms) -- fill, add, and flatten form fields - [Encryption & Security](/docs/editing/encryption) -- password-protect and set permissions --- # Text Editing PDF Oxide provides DOM-style text editing through the `PdfPage` object. You can query text elements, modify their content, and save changes back to the PDF. Every text element is represented as a `PdfText` node with access to content, font metadata, bounding box, and positioning. ## Getting a Page **Python** ```python from pdf_oxide import PdfDocument doc = PdfDocument("input.pdf") page = doc.page(0) # Get page at index 0 ``` **Rust** Use the `Pdf` API: ```rust use pdf_oxide::api::Pdf; let mut doc = Pdf::open("input.pdf")?; let page = doc.page(0)?; ``` Or use `DocumentEditor` directly: ```rust use pdf_oxide::editor::DocumentEditor; let mut editor = DocumentEditor::open("input.pdf")?; let page = editor.get_page(0)?; ``` ## Finding Text ### find_text_containing Search for text elements that contain a specific string. ```python page = doc.page(0) texts = page.find_text_containing("Hello") for t in texts: print(f"Found: '{t.value}' at {t.bbox}") ``` ```rust let page = doc.page(0)?; let texts = page.find_text_containing("Hello"); for t in &texts { println!("Found: '{}' at {:?}", t.text(), t.bbox()); } ``` ### find_text (Rust only) Search with a custom predicate for more flexible matching. ```rust // Find all bold text larger than 14pt let headings = page.find_text(|t| t.is_bold() && t.font_size() > 14.0); for h in &headings { println!("Heading: {}", h.text()); } // Find text by font name let courier_text = page.find_text(|t| t.font_name().contains("Courier")); ``` ## Modifying Text ### set_text Replace the content of a text element by its ID. ```python page = doc.page(0) for t in page.find_text_containing("DRAFT"): page.set_text(t.id, "FINAL") doc.save_page(page) doc.save("output.pdf") ``` ```rust let mut page = doc.page(0)?; let drafts = page.find_text_containing("DRAFT"); for t in &drafts { page.set_text(t.id(), "FINAL")?; } doc.save_page(page)?; doc.save("output.pdf")?; ``` ### modify_text (Rust only) Apply a closure to modify a text element in place, with access to the full `PdfText` API. ```rust let mut page = doc.page(0)?; let texts = page.find_text_containing("price"); for t in &texts { page.modify_text(t.id(), |text| { let old = text.text().to_string(); let new = old.replace("$9.99", "$12.99"); text.set_text(new); })?; } doc.save_page(page)?; ``` ## PdfText Properties Every `PdfText` element provides read access to its properties. **Python** ```python page = doc.page(0) for t in page.find_text_containing(""): print(f"Text: {t.value}") print(f"Font: {t.font_name} {t.font_size}pt") print(f"Bold: {t.is_bold}, Italic: {t.is_italic}") print(f"BBox: {t.bbox}") print(f"Contains 'hello': {t.contains('hello')}") print(f"Starts with 'A': {t.starts_with('A')}") print(f"Ends with '.': {t.ends_with('.')}") ``` **Rust** ```rust let page = doc.page(0)?; for t in &page.find_text_containing("") { println!("Text: {}", t.text()); println!("Font: {} {:.1}pt", t.font_name(), t.font_size()); println!("Bold: {}, Italic: {}", t.is_bold(), t.is_italic()); println!("BBox: {:?}", t.bbox()); println!("Color: {:?}", t.color()); // String operations println!("Contains 'hello': {}", t.contains("hello")); println!("Starts with 'A': {}", t.starts_with("A")); println!("Ends with '.': {}", t.ends_with(".")); println!("Empty: {}", t.is_empty()); println!("Length: {}", t.len()); } ``` ## Full PdfText API (Rust) ### Content Methods | Method | Returns | Description | |--------|---------|-------------| | `text()` | `&str` | Get the text content | | `value()` | `&str` | Alias for `text()` | | `set_text(new_text)` | `()` | Replace the text content | | `set_value(new_text)` | `()` | Alias for `set_text()` | | `append(text)` | `()` | Append text to the end | | `replace(old, new)` | `usize` | Replace occurrences, returns count | | `clear()` | `()` | Remove all text content | | `is_empty()` | `bool` | Check if text is empty | | `len()` | `usize` | Get the text length | | `contains(needle)` | `bool` | Check if text contains a substring | | `starts_with(prefix)` | `bool` | Check for prefix | | `ends_with(suffix)` | `bool` | Check for suffix | ### Font and Style Methods | Method | Returns | Description | |--------|---------|-------------| | `font_name()` | `&str` | PostScript font name | | `font_size()` | `f32` | Font size in points | | `is_bold()` | `bool` | Check if bold | | `is_italic()` | `bool` | Check if italic | | `color()` | `Color` | Text color | | `set_style(style)` | `()` | Set text style | ### Position and Transform Methods | Method | Returns | Description | |--------|---------|-------------| | `id()` | `ElementId` | Unique element identifier | | `bbox()` | `Rect` | Bounding box | | `origin()` | `Option` | Text origin point | | `set_origin(point)` | `()` | Set text origin | | `rotation_degrees()` | `Option` | Rotation in degrees | | `rotation_radians()` | `Option` | Rotation in radians | | `set_rotation(degrees)` | `()` | Set rotation angle | | `is_rotated()` | `bool` | Check if text is rotated | | `matrix()` | `Option<[f32; 6]>` | Transformation matrix | | `set_matrix(matrix)` | `()` | Set transformation matrix | ## Advanced Examples ### Find and Replace Across All Pages ```python from pdf_oxide import PdfDocument doc = PdfDocument("contract.pdf") for i in range(doc.page_count): page = doc.page(i) texts = page.find_text_containing("Acme Corp") for t in texts: page.set_text(t.id, "NewCo Inc.") doc.save_page(page) doc.save("contract-updated.pdf") ``` ```rust use pdf_oxide::api::Pdf; let mut doc = Pdf::open("contract.pdf")?; let count = doc.page_count()?; for i in 0..count { let mut page = doc.page(i)?; let texts = page.find_text_containing("Acme Corp"); for t in &texts { page.set_text(t.id(), "NewCo Inc.")?; } doc.save_page(page)?; } doc.save("contract-updated.pdf")?; ``` ### Using the PageEditor (Rust) The `PageEditor` provides a fluent API for batch editing operations. ```rust use pdf_oxide::editor::DocumentEditor; let mut editor = DocumentEditor::open("input.pdf")?; editor.edit_page(0, |page| { let texts = page.find_text_containing("old"); for t in &texts { page.set_text(t.id(), "new")?; } Ok(()) })?; editor.save("output.pdf")?; ``` Or using the `PageEditor` directly for collection-based operations: ```rust let mut editor = DocumentEditor::open("input.pdf")?; let page = editor.page_editor(0)? .find_text_containing("price")? .for_each(|text| { let old = text.text().to_string(); text.set_text(old.replace("$9.99", "$12.99")); })? .done()?; editor.save_page_from_editor(page)?; editor.save("output.pdf")?; ``` ### Adding New Text to a Page ```python page = doc.page(0) text_id = page.add_text("CONFIDENTIAL", 100, 750, 24.0) doc.save_page(page) doc.save("stamped.pdf") ``` ```rust use pdf_oxide::elements::{FontSpec, TextContent, TextStyle}; let mut page = doc.page(0)?; let content = TextContent { text: "CONFIDENTIAL".to_string(), bbox: pdf_oxide::geometry::Rect::new(100.0, 750.0, 200.0, 24.0), font: FontSpec { name: "Helvetica-Bold".to_string(), size: 24.0, }, style: TextStyle::default(), reading_order: None, origin: None, rotation_degrees: None, matrix: None, }; let id = page.add_text(content); doc.save_page(page)?; ``` ### Removing Text Elements ```python page = doc.page(0) for t in page.find_text_containing("DELETE ME"): page.remove_element(t.id) doc.save_page(page) doc.save("cleaned.pdf") ``` ```rust let mut page = doc.page(0)?; let to_remove = page.find_text_containing("DELETE ME"); for t in &to_remove { page.remove_element(t.id()); } doc.save_page(page)?; doc.save("cleaned.pdf")?; ``` ## Saving Modified Pages After modifying text on a page, you must call `save_page()` to persist the changes. Without this call, modifications are discarded. ```python page = doc.page(0) page.set_text(text_id, "updated") doc.save_page(page) # Persist page changes doc.save("output.pdf") # Write to disk ``` ```rust let mut page = doc.page(0)?; page.set_text(text_id, "updated")?; doc.save_page(page)?; // Persist page changes doc.save("output.pdf")?; // Write to disk ``` ## Related Pages - [Editing Overview](/docs/editing/overview) -- opening, metadata, and save workflow - [Page Operations](/docs/editing/pages) -- rotation, cropping, merging pages - [Annotation Editing](/docs/editing/annotations) -- add highlights, links, and notes - [Image Manipulation](/docs/editing/images) -- reposition and resize images --- # Page Operations PDF Oxide provides a complete set of page-level operations: rotation, media and crop box control, margin cropping, page merging, extraction, content erasure, and page reordering. ## Page Rotation ### Get Rotation Retrieve the current rotation of a page in degrees (0, 90, 180, or 270). ```python from pdf_oxide import PdfDocument doc = PdfDocument("input.pdf") rotation = doc.page_rotation(0) print(f"Page 0 is rotated {rotation} degrees") ``` ```rust use pdf_oxide::editor::DocumentEditor; let mut editor = DocumentEditor::open("input.pdf")?; let rotation = editor.get_page_rotation(0)?; println!("Page 0 is rotated {} degrees", rotation); ``` ### Set Rotation Set the absolute rotation for a specific page. ```python doc = PdfDocument("input.pdf") doc.set_page_rotation(0, 90) # Rotate page 0 to 90 degrees doc.save("rotated.pdf") ``` ```rust let mut editor = DocumentEditor::open("input.pdf")?; editor.set_page_rotation(0, 90)?; editor.save("rotated.pdf")?; ``` ### Rotate By (Incremental) Add a rotation increment to the existing rotation. ```rust let mut editor = DocumentEditor::open("input.pdf")?; // Rotate page 0 by an additional 90 degrees editor.rotate_page_by(0, 90)?; editor.save("rotated.pdf")?; ``` ### Rotate All Pages Apply the same rotation to every page in the document. ```python doc = PdfDocument("input.pdf") doc.rotate_all_pages(180) doc.save("flipped.pdf") ``` ```rust let mut editor = DocumentEditor::open("input.pdf")?; editor.rotate_all_pages(180)?; editor.save("flipped.pdf")?; ``` ## Media Box and Crop Box The **MediaBox** defines the full physical page size. The **CropBox** defines the visible region (the area displayed and printed). ### Get and Set MediaBox ```python doc = PdfDocument("input.pdf") # Get MediaBox: (llx, lly, urx, ury) box = doc.page_media_box(0) print(f"MediaBox: {box}") # Set MediaBox to US Letter (612 x 792 points) doc.set_page_media_box(0, 0, 0, 612, 792) doc.save("output.pdf") ``` ```rust let mut editor = DocumentEditor::open("input.pdf")?; // Get MediaBox as [llx, lly, urx, ury] let media_box = editor.get_page_media_box(0)?; println!("MediaBox: {:?}", media_box); // Set MediaBox editor.set_page_media_box(0, [0.0, 0.0, 612.0, 792.0])?; editor.save("output.pdf")?; ``` ### Get and Set CropBox ```python doc = PdfDocument("input.pdf") # Get CropBox (returns None if not set) crop = doc.page_crop_box(0) print(f"CropBox: {crop}") # Set CropBox to crop a 1-inch border (72 points = 1 inch) doc.set_page_crop_box(0, 72, 72, 540, 720) doc.save("cropped.pdf") ``` ```rust let mut editor = DocumentEditor::open("input.pdf")?; // Get CropBox (None if not explicitly set) let crop_box = editor.get_page_crop_box(0)?; println!("CropBox: {:?}", crop_box); // Set CropBox editor.set_page_crop_box(0, [72.0, 72.0, 540.0, 720.0])?; editor.save("cropped.pdf")?; ``` ### Crop Margins A convenience method that sets the CropBox to be inset from the MediaBox by the specified margins on all pages. ```python doc = PdfDocument("input.pdf") # Crop 0.5 inch (36pt) from all sides on every page doc.crop_margins(36, 36, 36, 36) doc.save("cropped.pdf") ``` ```rust let mut editor = DocumentEditor::open("input.pdf")?; // Arguments: left, right, top, bottom (in points) editor.crop_margins(36.0, 36.0, 36.0, 36.0)?; editor.save("cropped.pdf")?; ``` ## Merging PDFs ### Merge All Pages from Another PDF Append all pages from another PDF to the end of the current document. ```rust use pdf_oxide::editor::DocumentEditor; let mut editor = DocumentEditor::open("main.pdf")?; let pages_added = editor.merge_from("appendix.pdf")?; println!("Added {} pages", pages_added); editor.save("combined.pdf")?; ``` ### Merge Specific Pages Select specific pages from the source PDF to append. ```rust let mut editor = DocumentEditor::open("main.pdf")?; // Merge only pages 0, 2, and 4 from the source editor.merge_pages_from("source.pdf", &[0, 2, 4])?; editor.save("selected.pdf")?; ``` ## Extracting Pages Extract a subset of pages into a new PDF file. ```rust use pdf_oxide::editor::DocumentEditor; let mut editor = DocumentEditor::open("book.pdf")?; // Extract pages 0-4 (first 5 pages) to a new file editor.extract_pages(&[0, 1, 2, 3, 4], "chapter1.pdf")?; ``` ## Content Erasure Erase (whiteout) rectangular regions on a page. This draws a white rectangle over the specified area, visually hiding the content beneath. ### Erase a Single Region ```python doc = PdfDocument("input.pdf") # Erase a region: (page, llx, lly, urx, ury) doc.erase_region(0, 72, 700, 300, 792) doc.save("erased.pdf") ``` ```rust let mut editor = DocumentEditor::open("input.pdf")?; editor.erase_region(0, [72.0, 700.0, 300.0, 792.0])?; editor.save("erased.pdf")?; ``` ### Erase Multiple Regions ```python doc = PdfDocument("input.pdf") doc.erase_regions(0, [ (72, 700, 300, 792), (72, 600, 300, 650), ]) doc.save("erased.pdf") ``` ```rust let mut editor = DocumentEditor::open("input.pdf")?; editor.erase_regions(0, &[ [72.0, 700.0, 300.0, 792.0], [72.0, 600.0, 300.0, 650.0], ])?; editor.save("erased.pdf")?; ``` ### Clear Pending Erasures Remove all pending erase operations for a page before saving. ```python doc.clear_erase_regions(0) ``` ```rust editor.clear_erase_regions(0); ``` ## Page Reordering and Manipulation The `EditableDocument` trait provides page manipulation methods. ### Remove a Page ```rust use pdf_oxide::editor::{DocumentEditor, EditableDocument}; let mut editor = DocumentEditor::open("input.pdf")?; editor.remove_page(2)?; // Remove page at index 2 editor.save("output.pdf")?; ``` ### Move a Page ```rust let mut editor = DocumentEditor::open("input.pdf")?; editor.move_page(0, 3)?; // Move page 0 to position 3 editor.save("reordered.pdf")?; ``` ### Duplicate a Page ```rust let mut editor = DocumentEditor::open("input.pdf")?; let new_index = editor.duplicate_page(0)?; // Duplicate page 0 println!("Duplicate is at index {}", new_index); editor.save("output.pdf")?; ``` ## Full API Reference ### Rotation | Method | Returns | Description | |--------|---------|-------------| | `get_page_rotation(index)` | `Result` | Get rotation in degrees | | `set_page_rotation(index, degrees)` | `Result<()>` | Set absolute rotation | | `rotate_page_by(index, degrees)` | `Result<()>` | Add incremental rotation | | `rotate_all_pages(degrees)` | `Result<()>` | Rotate every page | ### Page Boxes | Method | Returns | Description | |--------|---------|-------------| | `get_page_media_box(index)` | `Result<[f32; 4]>` | Get MediaBox | | `set_page_media_box(index, box)` | `Result<()>` | Set MediaBox | | `get_page_crop_box(index)` | `Result>` | Get CropBox | | `set_page_crop_box(index, box)` | `Result<()>` | Set CropBox | | `crop_margins(left, right, top, bottom)` | `Result<()>` | Crop all pages by margins | ### Merging and Extracting | Method | Returns | Description | |--------|---------|-------------| | `merge_from(path)` | `Result` | Merge all pages from another PDF | | `merge_pages_from(path, pages)` | `Result` | Merge specific pages | | `extract_pages(pages, output)` | `Result<()>` | Extract pages to new file | ### Content Erasure | Method | Returns | Description | |--------|---------|-------------| | `erase_region(page, rect)` | `Result<()>` | Erase one region | | `erase_regions(page, rects)` | `Result<()>` | Erase multiple regions | | `clear_erase_regions(page)` | `()` | Clear pending erasures | ### Page Management (EditableDocument) | Method | Returns | Description | |--------|---------|-------------| | `page_count()` | `Result` | Number of pages | | `get_page_info(index)` | `Result` | Page dimensions and rotation | | `remove_page(index)` | `Result<()>` | Remove a page | | `move_page(from, to)` | `Result<()>` | Reorder a page | | `duplicate_page(index)` | `Result` | Duplicate a page | ## Advanced Example: Normalize Scanned Pages ```rust use pdf_oxide::editor::{DocumentEditor, EditableDocument}; let mut editor = DocumentEditor::open("scanned.pdf")?; let count = editor.current_page_count(); for i in 0..count { // Set all pages to portrait Letter size editor.set_page_media_box(i, [0.0, 0.0, 612.0, 792.0])?; // Reset any rotation editor.set_page_rotation(i, 0)?; // Apply uniform margins editor.set_page_crop_box(i, [36.0, 36.0, 576.0, 756.0])?; } editor.save("normalized.pdf")?; ``` ## Related Pages - [Editing Overview](/docs/editing/overview) -- opening, metadata, and save workflow - [Text Editing](/docs/editing/text) -- find and replace text on pages - [Redaction](/docs/editing/redaction) -- permanently redact content - [Annotation Editing](/docs/editing/annotations) -- add and flatten annotations --- # Form Field Editing PDF Oxide provides comprehensive form field support: read existing values, fill fields programmatically, add new fields, configure properties, flatten forms to static content, and export form data in FDF/XFDF formats. XFA forms can be analyzed and converted to AcroForm. ## Reading Form Fields ### Get All Form Fields ```rust use pdf_oxide::editor::DocumentEditor; let mut editor = DocumentEditor::open("form.pdf")?; let fields = editor.get_form_fields()?; for field in &fields { println!("Field: {} = {:?}", field.name(), field.value()); if let Some(ft) = field.field_type() { println!(" Type: {:?}", ft); } if let Some(tooltip) = field.tooltip() { println!(" Tooltip: {}", tooltip); } } ``` ### Get a Specific Field Value ```rust let mut editor = DocumentEditor::open("form.pdf")?; let value = editor.get_form_field_value("first_name")?; println!("First name: {:?}", value); ``` ### Check if a Field Exists ```rust let mut editor = DocumentEditor::open("form.pdf")?; if editor.has_form_field("email")? { println!("Email field exists"); } ``` ## Setting Form Field Values ### Set a Field Value ```rust use pdf_oxide::editor::DocumentEditor; use pdf_oxide::editor::form_fields::FormFieldValue; let mut editor = DocumentEditor::open("form.pdf")?; // Set a text field editor.set_form_field_value("first_name", FormFieldValue::Text("Jane".to_string()))?; // Set a checkbox editor.set_form_field_value("agree_terms", FormFieldValue::Boolean(true))?; // Set a choice field editor.set_form_field_value("country", FormFieldValue::Choice("United States".to_string()))?; editor.save("filled.pdf")?; ``` ### FormFieldValue Variants | Variant | Description | Example | |---------|-------------|---------| | `Text(String)` | Text field value | `FormFieldValue::Text("Hello".into())` | | `Boolean(bool)` | Checkbox/radio state | `FormFieldValue::Boolean(true)` | | `Choice(String)` | Single choice selection | `FormFieldValue::Choice("Option A".into())` | | `MultiChoice(Vec)` | Multiple selections | `FormFieldValue::MultiChoice(vec!["A".into(), "B".into()])` | | `None` | No value / clear field | `FormFieldValue::None` | ## Adding Form Fields ### Add a New Form Field ```rust use pdf_oxide::editor::DocumentEditor; use pdf_oxide::writer::form_fields::TextFieldWidget; let mut editor = DocumentEditor::open("document.pdf")?; // Create a text input field on page 0 let widget = TextFieldWidget::new("user_name") .with_rect(100.0, 700.0, 200.0, 20.0) .with_default_value("Enter name"); editor.add_form_field(widget, 0)?; editor.save("with-form.pdf")?; ``` ### Add Hierarchical Fields Create parent-child field relationships for structured forms. ```rust use pdf_oxide::editor::form_fields::ParentFieldConfig; let mut editor = DocumentEditor::open("document.pdf")?; // Create a parent field let parent = ParentFieldConfig::new("address"); editor.add_parent_field(parent)?; // Add child fields under the parent let street = TextFieldWidget::new("street") .with_rect(100.0, 600.0, 300.0, 20.0); editor.add_child_field(street, 0, "address")?; let city = TextFieldWidget::new("city") .with_rect(100.0, 570.0, 150.0, 20.0); editor.add_child_field(city, 0, "address")?; editor.save("hierarchical-form.pdf")?; ``` ### Remove a Form Field ```rust let mut editor = DocumentEditor::open("form.pdf")?; editor.remove_form_field("obsolete_field")?; editor.save("cleaned.pdf")?; ``` ## Form Field Properties ### Setting Properties Configure individual field properties by name. ```rust let mut editor = DocumentEditor::open("form.pdf")?; // Access control editor.set_form_field_readonly("signature_date", true)?; editor.set_form_field_required("email", true)?; // Tooltip editor.set_form_field_tooltip("phone", "Enter phone number with area code")?; // Position and size editor.set_form_field_rect("name", pdf_oxide::geometry::Rect::new(100.0, 700.0, 200.0, 20.0))?; // Text constraints editor.set_form_field_max_length("zip_code", 10)?; editor.set_form_field_alignment("amount", 2)?; // 0=left, 1=center, 2=right // Appearance editor.set_form_field_background_color("highlight_field", [1.0, 1.0, 0.8])?; editor.set_form_field_border_color("name", [0.0, 0.0, 0.0])?; editor.set_form_field_border_width("name", 1.0)?; editor.set_form_field_default_appearance("name", "/Helv 12 Tf 0 g")?; // Raw flags editor.set_form_field_flags("options", 0x100000)?; editor.save("styled-form.pdf")?; ``` ### FormFieldWrapper Properties When working with `FormFieldWrapper` objects returned by `get_form_fields()`: | Method | Returns | Description | |--------|---------|-------------| | `name()` | `&str` | Full field name | | `partial_name()` | `&str` | Partial name (without parent prefix) | | `value()` | `FormFieldValue` | Current field value | | `set_value(value)` | `()` | Set the field value | | `field_type()` | `Option<&FieldType>` | Field type | | `page_index()` | `usize` | Page containing the field | | `bounds()` | `Option` | Field position and size | | `tooltip()` | `Option<&str>` | Tooltip text | | `is_modified()` | `bool` | Whether value has been changed | | `is_new()` | `bool` | Whether field was added (not from source) | | `is_readonly()` | `bool` | Read-only flag | | `set_readonly(bool)` | `()` | Set read-only flag | | `is_required()` | `bool` | Required flag | | `set_required(bool)` | `()` | Set required flag | | `is_no_export()` | `bool` | No-export flag | | `set_no_export(bool)` | `()` | Set no-export flag | | `set_tooltip(text)` | `()` | Set tooltip text | | `set_rect(rect)` | `()` | Set position and size | | `set_max_length(len)` | `()` | Set maximum text length | | `get_max_length()` | `Option` | Get maximum text length | | `set_alignment(align)` | `()` | Set text alignment | | `get_alignment()` | `Option` | Get text alignment | | `set_background_color(rgb)` | `()` | Set background color | | `get_background_color()` | `Option<[f32; 3]>` | Get background color | | `set_border_color(rgb)` | `()` | Set border color | | `get_border_color()` | `Option<[f32; 3]>` | Get border color | | `set_border_width(width)` | `()` | Set border width | | `get_border_width()` | `Option` | Get border width | | `set_default_appearance(da)` | `()` | Set default appearance string | | `get_default_appearance()` | `Option<&str>` | Get default appearance string | | `set_default_value(value)` | `()` | Set default value | | `get_default_value()` | `Option<&FormFieldValue>` | Get default value | | `has_parent()` | `bool` | Check for parent field | | `parent_name()` | `Option<&str>` | Parent field name | ## Flattening Forms Flattening converts interactive form fields into static page content. The field values become part of the page drawing and can no longer be edited. ### Flatten a Single Page ```rust let mut editor = DocumentEditor::open("form.pdf")?; editor.flatten_forms_on_page(0)?; editor.save("flat-page0.pdf")?; ``` ### Flatten All Forms ```rust let mut editor = DocumentEditor::open("form.pdf")?; editor.flatten_forms()?; editor.save("flat.pdf")?; ``` ### Check Flatten Status ```rust editor.flatten_forms_on_page(0)?; assert!(editor.is_page_marked_for_form_flatten(0)); assert!(editor.will_remove_acroform()); ``` ## Exporting Form Data Export form field values to FDF or XFDF format for external processing. ### Export to FDF ```rust let mut editor = DocumentEditor::open("filled-form.pdf")?; editor.export_form_data_fdf("form-data.fdf")?; ``` ### Export to XFDF ```rust let mut editor = DocumentEditor::open("filled-form.pdf")?; editor.export_form_data_xfdf("form-data.xfdf")?; ``` ## XFA Form Support PDF Oxide can detect, analyze, and convert XFA forms to standard AcroForm. ### Check for XFA ```rust let mut editor = DocumentEditor::open("xfa-form.pdf")?; if editor.has_xfa()? { println!("Document contains XFA form data"); } ``` ### Analyze XFA Structure ```rust let mut editor = DocumentEditor::open("xfa-form.pdf")?; let analysis = editor.analyze_xfa()?; println!("XFA analysis: {:?}", analysis); ``` ### Convert XFA to AcroForm Convert XFA forms to standard AcroForm fields for broader compatibility. ```rust let mut editor = DocumentEditor::open("xfa-form.pdf")?; editor.convert_xfa_to_acroform(&Default::default())?; editor.save("acroform.pdf")?; ``` ## Full API Reference ### Field Operations | Method | Returns | Description | |--------|---------|-------------| | `get_form_fields()` | `Result>` | List all form fields | | `get_form_field_value(name)` | `Result` | Get a field's value | | `has_form_field(name)` | `Result` | Check if field exists | | `set_form_field_value(name, value)` | `Result<()>` | Set a field's value | | `add_form_field(widget, page)` | `Result<()>` | Add a new field | | `add_parent_field(config)` | `Result<()>` | Add a parent field | | `add_child_field(widget, page, parent)` | `Result<()>` | Add a child field | | `remove_form_field(name)` | `Result<()>` | Remove a field | ### Field Properties (by name) | Method | Returns | Description | |--------|---------|-------------| | `set_form_field_readonly(name, bool)` | `Result<()>` | Set read-only | | `set_form_field_required(name, bool)` | `Result<()>` | Set required | | `set_form_field_tooltip(name, text)` | `Result<()>` | Set tooltip | | `set_form_field_rect(name, rect)` | `Result<()>` | Set position/size | | `set_form_field_max_length(name, len)` | `Result<()>` | Set max text length | | `set_form_field_alignment(name, align)` | `Result<()>` | Set text alignment | | `set_form_field_background_color(name, rgb)` | `Result<()>` | Set background color | | `set_form_field_border_color(name, rgb)` | `Result<()>` | Set border color | | `set_form_field_border_width(name, width)` | `Result<()>` | Set border width | | `set_form_field_default_appearance(name, da)` | `Result<()>` | Set default appearance | | `set_form_field_flags(name, flags)` | `Result<()>` | Set raw field flags | ### Flattening | Method | Returns | Description | |--------|---------|-------------| | `flatten_forms_on_page(page)` | `Result<()>` | Flatten forms on one page | | `flatten_forms()` | `Result<()>` | Flatten all forms | | `is_page_marked_for_form_flatten(page)` | `bool` | Check flatten status | | `will_remove_acroform()` | `bool` | Check if AcroForm will be removed | ### Export | Method | Returns | Description | |--------|---------|-------------| | `export_form_data_fdf(path)` | `Result<()>` | Export to FDF file | | `export_form_data_xfdf(path)` | `Result<()>` | Export to XFDF file | ### XFA | Method | Returns | Description | |--------|---------|-------------| | `has_xfa()` | `Result` | Check for XFA form data | | `analyze_xfa()` | `Result` | Analyze XFA structure | | `convert_xfa_to_acroform(options)` | `Result<()>` | Convert XFA to AcroForm | ## Advanced Example: Batch Fill and Flatten ```rust use pdf_oxide::editor::DocumentEditor; use pdf_oxide::editor::form_fields::FormFieldValue; let mut editor = DocumentEditor::open("template.pdf")?; // Fill form fields editor.set_form_field_value("name", FormFieldValue::Text("John Doe".to_string()))?; editor.set_form_field_value("date", FormFieldValue::Text("2025-12-01".to_string()))?; editor.set_form_field_value("approved", FormFieldValue::Boolean(true))?; // Make the completed form non-editable editor.flatten_forms()?; // Export data before saving editor.export_form_data_xfdf("submission.xfdf")?; editor.save("completed.pdf")?; ``` ## Related Pages - [Editing Overview](/docs/editing/overview) -- opening, metadata, and save workflow - [Text Editing](/docs/editing/text) -- modify text content directly - [Annotation Editing](/docs/editing/annotations) -- add and manage annotations - [Encryption & Security](/docs/editing/encryption) -- restrict form editing with permissions --- # Annotation Editing PDF Oxide provides DOM-level access to annotations through the `PdfPage` object. You can read existing annotations, add new ones (links, highlights, sticky notes), modify annotation properties, remove annotations, and flatten them into page content. ## Getting Annotations ### List All Annotations ```python from pdf_oxide import PdfDocument doc = PdfDocument("annotated.pdf") page = doc.page(0) for ann in page.annotations(): print(f"Type: {ann.subtype}") print(f"Rect: {ann.rect}") if ann.contents: print(f"Contents: {ann.contents}") ``` ```rust use pdf_oxide::api::Pdf; let mut doc = Pdf::open("annotated.pdf")?; let page = doc.page(0)?; for ann in page.annotations() { println!("Type: {:?}", ann.subtype()); println!("Rect: {:?}", ann.rect()); if let Some(contents) = ann.contents() { println!("Contents: {}", contents); } if let Some(color) = ann.color() { println!("Color: {:?}", color); } } ``` ### Access by Index ```rust let page = doc.page(0)?; if let Some(ann) = page.annotation(0) { println!("First annotation: {:?}", ann.subtype()); } println!("Total annotations: {}", page.annotation_count()); ``` ### Find Annotations #### By ID ```rust let page = doc.page(0)?; let id = page.annotations()[0].id(); if let Some(ann) = page.find_annotation(id) { println!("Found: {:?}", ann.subtype()); } ``` #### By Region Find annotations within a rectangular area. ```rust use pdf_oxide::geometry::Rect; let page = doc.page(0)?; let region = Rect::new(0.0, 700.0, 612.0, 92.0); let top_annotations = page.find_annotations_in_region(region); for ann in top_annotations { println!("Annotation in header area: {:?}", ann.subtype()); } ``` #### By Type ```rust use pdf_oxide::AnnotationSubtype; let page = doc.page(0)?; let highlights = page.find_annotations_by_type(AnnotationSubtype::Highlight); println!("Found {} highlights", highlights.len()); ``` ## Adding Annotations ### Add a Link ```python doc = PdfDocument("input.pdf") page = doc.page(0) # Add a clickable URL link page.add_link(100, 700, 150, 12, "https://example.com") doc.save_page(page) doc.save("with-link.pdf") ``` ```rust use pdf_oxide::api::Pdf; use pdf_oxide::writer::LinkAnnotation; use pdf_oxide::geometry::Rect; let mut doc = Pdf::open("input.pdf")?; let mut page = doc.page(0)?; let link = LinkAnnotation::uri( Rect::new(100.0, 700.0, 150.0, 12.0), "https://example.com" ); page.add_annotation(link); doc.save_page(page)?; doc.save("with-link.pdf")?; ``` ### Add a Text Highlight ```python doc = PdfDocument("input.pdf") page = doc.page(0) # Yellow highlight page.add_highlight(100, 700, 200, 12, (1.0, 1.0, 0.0)) doc.save_page(page) doc.save("highlighted.pdf") ``` ```rust use pdf_oxide::writer::TextMarkupAnnotation; use pdf_oxide::TextMarkupType; use pdf_oxide::geometry::Rect; let mut doc = Pdf::open("input.pdf")?; let mut page = doc.page(0)?; let highlight = TextMarkupAnnotation::from_rect( TextMarkupType::Highlight, Rect::new(100.0, 700.0, 200.0, 12.0), ).with_color(1.0, 1.0, 0.0); // Yellow page.add_annotation(highlight); doc.save_page(page)?; doc.save("highlighted.pdf")?; ``` ### Add a Sticky Note ```python doc = PdfDocument("input.pdf") page = doc.page(0) page.add_note(50, 750, "Review this section before publishing.") doc.save_page(page) doc.save("with-notes.pdf") ``` ```rust use pdf_oxide::writer::TextAnnotation; use pdf_oxide::geometry::Rect; let mut doc = Pdf::open("input.pdf")?; let mut page = doc.page(0)?; let note = TextAnnotation::new( Rect::new(50.0, 750.0, 24.0, 24.0), "Review this section before publishing." ); page.add_annotation(note); doc.save_page(page)?; doc.save("with-notes.pdf")?; ``` ## Removing Annotations ### Remove by Index ```python doc = PdfDocument("input.pdf") page = doc.page(0) # Remove the first annotation page.remove_annotation(0) doc.save_page(page) doc.save("cleaned.pdf") ``` ```rust let mut page = doc.page(0)?; page.remove_annotation(0); // Returns Option doc.save_page(page)?; ``` ### Remove by ID ```rust let mut page = doc.page(0)?; // Get the ID of an annotation to remove let ann_id = page.annotations()[0].id(); page.remove_annotation_by_id(ann_id); doc.save_page(page)?; doc.save("cleaned.pdf")?; ``` ## Modifying Annotations Access mutable annotations to change their properties. ```rust let mut page = doc.page(0)?; // Modify annotations through mutable access for ann in page.annotations_mut() { // Change contents text ann.set_contents("Updated comment"); // Change position ann.set_rect(pdf_oxide::geometry::Rect::new(100.0, 700.0, 200.0, 20.0)); // Change color ann.set_color(1.0, 0.0, 0.0); // Red } doc.save_page(page)?; doc.save("modified.pdf")?; ``` ### Modify a Specific Annotation ```rust let mut page = doc.page(0)?; if let Some(ann) = page.annotation_mut(0) { ann.set_contents("First annotation - updated"); } doc.save_page(page)?; ``` ### Mutable Find ```rust let mut page = doc.page(0)?; let target_id = page.annotations()[0].id(); if let Some(ann) = page.find_annotation_mut(target_id) { ann.set_contents("Found and updated"); ann.set_color(0.0, 1.0, 0.0); // Green } doc.save_page(page)?; ``` ## Flattening Annotations Flattening renders annotation appearance streams into the page content and removes the annotation objects. This makes annotations permanent and non-editable. ### Flatten a Single Page ```python doc = PdfDocument("annotated.pdf") doc.flatten_page_annotations(0) doc.save("flat.pdf") ``` ```rust let mut editor = DocumentEditor::open("annotated.pdf")?; editor.flatten_page_annotations(0)?; editor.save("flat.pdf")?; ``` ### Flatten All Annotations ```python doc = PdfDocument("annotated.pdf") doc.flatten_all_annotations() doc.save("flat.pdf") ``` ```rust let mut editor = DocumentEditor::open("annotated.pdf")?; editor.flatten_all_annotations()?; editor.save("flat.pdf")?; ``` ### Check and Undo Flatten Marking ```python doc.flatten_page_annotations(0) print(doc.is_page_marked_for_flatten(0)) # True doc.unmark_page_for_flatten(0) print(doc.is_page_marked_for_flatten(0)) # False ``` ```rust editor.flatten_page_annotations(0)?; assert!(editor.is_page_marked_for_flatten(0)); editor.unmark_page_for_flatten(0); assert!(!editor.is_page_marked_for_flatten(0)); ``` ## Full API Reference ### PdfPage Annotation Methods | Method | Returns | Description | |--------|---------|-------------| | `annotations()` | `&[AnnotationWrapper]` | Get all annotations | | `annotation(index)` | `Option<&AnnotationWrapper>` | Get annotation by index | | `annotations_mut()` | `&mut [AnnotationWrapper]` | Get mutable annotations | | `annotation_mut(index)` | `Option<&mut AnnotationWrapper>` | Get mutable annotation by index | | `annotation_count()` | `usize` | Number of annotations | | `has_annotations_modified()` | `bool` | Check if annotations were changed | | `add_annotation(ann)` | `AnnotationId` | Add a new annotation | | `remove_annotation(index)` | `Option` | Remove by index | | `remove_annotation_by_id(id)` | `Option` | Remove by ID | | `find_annotation(id)` | `Option<&AnnotationWrapper>` | Find by ID | | `find_annotation_mut(id)` | `Option<&mut AnnotationWrapper>` | Find mutable by ID | | `find_annotations_in_region(rect)` | `Vec<&AnnotationWrapper>` | Find in area | | `find_annotations_by_type(subtype)` | `Vec<&AnnotationWrapper>` | Find by type | ### AnnotationWrapper Properties | Method | Returns | Description | |--------|---------|-------------| | `id()` | `AnnotationId` | Unique identifier | | `subtype()` | `AnnotationSubtype` | Annotation type | | `rect()` | `Rect` | Position and size | | `contents()` | `Option<&str>` | Text contents | | `color()` | `Option<(f32, f32, f32)>` | RGB color | | `is_modified()` | `bool` | Has been changed | | `is_new()` | `bool` | Was added (not from source) | | `set_contents(text)` | `()` | Set text contents | | `set_rect(rect)` | `()` | Set position/size | | `set_color(r, g, b)` | `()` | Set RGB color | ### DocumentEditor Annotation Methods | Method | Returns | Description | |--------|---------|-------------| | `flatten_page_annotations(page)` | `Result<()>` | Flatten one page | | `flatten_all_annotations()` | `Result<()>` | Flatten all pages | | `is_page_marked_for_flatten(page)` | `bool` | Check flatten status | | `unmark_page_for_flatten(page)` | `()` | Cancel pending flatten | ## Advanced Example: Annotation Review Workflow ```rust use pdf_oxide::api::Pdf; use pdf_oxide::writer::{TextAnnotation, TextMarkupAnnotation}; use pdf_oxide::{AnnotationSubtype, TextMarkupType}; use pdf_oxide::geometry::Rect; let mut doc = Pdf::open("draft.pdf")?; let count = doc.page_count()?; for i in 0..count { let mut page = doc.page(i)?; // Find all text containing "TODO" let todos = page.find_text_containing("TODO"); for t in &todos { // Add a highlight over the TODO text let highlight = TextMarkupAnnotation::from_rect( TextMarkupType::Highlight, t.bbox(), ).with_color(1.0, 0.5, 0.0); // Orange page.add_annotation(highlight); // Add a note next to it let note = TextAnnotation::new( Rect::new(t.bbox().x - 30.0, t.bbox().y, 24.0, 24.0), &format!("TODO found: {}", t.text()), ); page.add_annotation(note); } doc.save_page(page)?; } doc.save("reviewed.pdf")?; ``` ## Related Pages - [Text Editing](/docs/editing/text) -- modify text content directly - [Redaction](/docs/editing/redaction) -- redact content with redaction annotations - [Page Operations](/docs/editing/pages) -- page-level operations - [Form Field Editing](/docs/editing/forms) -- interactive form fields --- # Image Manipulation PDF Oxide provides two levels of image manipulation: low-level operations via `DocumentEditor` for repositioning and resizing images by their XObject name, and DOM-level access via `PdfPage` for querying image metadata. Both approaches work with images already embedded in the PDF. ## Getting Page Images ### DocumentEditor: Low-Level Image Info Retrieve detailed placement information for all images on a page, including XObject names and transformation matrices. ```python from pdf_oxide import PdfDocument doc = PdfDocument("brochure.pdf") images = doc.page_images(0) for img in images: print(f"Name: {img['name']}") print(f"Position: ({img['x']:.1f}, {img['y']:.1f})") print(f"Size: {img['width']:.1f} x {img['height']:.1f}") print(f"Matrix: {img['matrix']}") ``` ```rust use pdf_oxide::editor::DocumentEditor; let mut editor = DocumentEditor::open("brochure.pdf")?; let images = editor.get_page_images(0)?; for img in &images { println!("Name: {}", img.name); println!("Bounds: {:?}", img.bounds); // [x, y, width, height] println!("Matrix: {:?}", img.matrix); // [a, b, c, d, e, f] } ``` The returned `ImageInfo` struct contains: | Field | Type | Description | |-------|------|-------------| | `name` | `String` | XObject name (e.g., `"Im0"`, `"Image1"`) | | `bounds` | `[f32; 4]` | Position and size `[x, y, width, height]` | | `matrix` | `[f32; 6]` | Full transformation matrix `[a, b, c, d, e, f]` | ### PdfPage: DOM-Level Image Info The DOM API provides richer metadata about each image. ```python doc = PdfDocument("brochure.pdf") page = doc.page(0) for img in page.find_images(): print(f"BBox: {img.bbox}") print(f"Format: {img.format}") print(f"Dimensions: {img.dimensions}") ``` ```rust let mut doc = Pdf::open("brochure.pdf")?; let page = doc.page(0)?; for img in page.find_images() { println!("BBox: {:?}", img.bbox()); println!("Format: {:?}", img.format()); println!("Dimensions: {:?}", img.dimensions()); println!("Aspect ratio: {:.2}", img.aspect_ratio()); println!("Grayscale: {}", img.is_grayscale()); if let Some(alt) = img.alt_text() { println!("Alt text: {}", alt); } if let Some((h_dpi, v_dpi)) = img.resolution() { println!("Resolution: {:.0} x {:.0} DPI", h_dpi, v_dpi); } } ``` ## Repositioning Images Move an image to a new position on the page without changing its size. ```python doc = PdfDocument("input.pdf") images = doc.page_images(0) # Move the first image to position (100, 500) doc.reposition_image(0, images[0]["name"], 100, 500) doc.save("moved.pdf") ``` ```rust let mut editor = DocumentEditor::open("input.pdf")?; let images = editor.get_page_images(0)?; // Move the first image editor.reposition_image(0, &images[0].name, 100.0, 500.0)?; editor.save("moved.pdf")?; ``` ## Resizing Images Change the dimensions of an image without moving its position. ```python doc = PdfDocument("input.pdf") images = doc.page_images(0) # Resize the first image to 300x200 points doc.resize_image(0, images[0]["name"], 300, 200) doc.save("resized.pdf") ``` ```rust let mut editor = DocumentEditor::open("input.pdf")?; let images = editor.get_page_images(0)?; editor.resize_image(0, &images[0].name, 300.0, 200.0)?; editor.save("resized.pdf")?; ``` ## Setting Full Image Bounds Set both position and size in a single operation. ```python doc = PdfDocument("input.pdf") images = doc.page_images(0) # Set position (72, 600) and size (468, 200) doc.set_image_bounds(0, images[0]["name"], 72, 600, 468, 200) doc.save("adjusted.pdf") ``` ```rust let mut editor = DocumentEditor::open("input.pdf")?; let images = editor.get_page_images(0)?; // x, y, width, height editor.set_image_bounds(0, &images[0].name, 72.0, 600.0, 468.0, 200.0)?; editor.save("adjusted.pdf")?; ``` ## Managing Image Modifications ### Clear Modifications Discard all pending image modifications for a page before saving. ```python doc.clear_image_modifications(0) ``` ```rust editor.clear_image_modifications(0); ``` ### Check for Pending Modifications ```python if doc.has_image_modifications(0): print("Page 0 has pending image changes") ``` ```rust if editor.has_image_modifications(0) { println!("Page 0 has pending image changes"); } ``` ## PdfImage DOM API (Rust) The DOM-level `PdfImage` provides rich metadata for each image found on a page. | Method | Returns | Description | |--------|---------|-------------| | `id()` | `ElementId` | Unique element identifier | | `bbox()` | `Rect` | Position and size on the page | | `format()` | `ImageFormat` | Image format (JPEG, PNG, etc.) | | `dimensions()` | `(u32, u32)` | Width and height in pixels | | `aspect_ratio()` | `f32` | Width / height ratio | | `is_grayscale()` | `bool` | True if grayscale image | | `alt_text()` | `Option<&str>` | Accessibility alt text | | `set_alt_text(text)` | `()` | Set accessibility alt text | | `resolution()` | `Option<(f32, f32)>` | DPI as (horizontal, vertical) | | `horizontal_dpi()` | `Option` | Horizontal DPI | | `vertical_dpi()` | `Option` | Vertical DPI | | `is_high_resolution()` | `bool` | Resolution >= 300 DPI | | `is_medium_resolution()` | `bool` | Resolution between 150-300 DPI | | `is_low_resolution()` | `bool` | Resolution < 150 DPI | ### Find Images in a Region ```rust use pdf_oxide::geometry::Rect; let page = doc.page(0)?; // Find images in the top half of the page let region = Rect::new(0.0, 396.0, 612.0, 396.0); let top_images = page.find_images_in_region(region); println!("Found {} images in top half", top_images.len()); ``` ### Set Accessibility Alt Text ```rust let mut page = doc.page(0)?; let images = page.find_images(); for img in &images { if img.alt_text().is_none() { page.set_image_alt_text(img.id(), "Descriptive alt text")?; } } doc.save_page(page)?; ``` ## Full API Reference ### DocumentEditor Image Methods | Method | Returns | Description | |--------|---------|-------------| | `get_page_images(page)` | `Result>` | List all images on a page | | `reposition_image(page, name, x, y)` | `Result<()>` | Move image to new position | | `resize_image(page, name, w, h)` | `Result<()>` | Change image dimensions | | `set_image_bounds(page, name, x, y, w, h)` | `Result<()>` | Set position and size | | `clear_image_modifications(page)` | `()` | Discard pending changes | | `has_image_modifications(page)` | `bool` | Check for pending changes | ## Advanced Example: Center All Images ```rust use pdf_oxide::editor::DocumentEditor; let mut editor = DocumentEditor::open("input.pdf")?; let count = editor.current_page_count(); for page_idx in 0..count { let media_box = editor.get_page_media_box(page_idx)?; let page_width = media_box[2] - media_box[0]; let images = editor.get_page_images(page_idx)?; for img in &images { let img_width = img.bounds[2]; let centered_x = (page_width - img_width) / 2.0; editor.reposition_image(page_idx, &img.name, centered_x, img.bounds[1])?; } } editor.save("centered.pdf")?; ``` ## Advanced Example: Scale to Fit Width ```python from pdf_oxide import PdfDocument doc = PdfDocument("photos.pdf") for page_idx in range(doc.page_count): media_box = doc.page_media_box(page_idx) page_width = media_box[2] - media_box[0] margin = 72 # 1 inch images = doc.page_images(page_idx) for img in images: target_width = page_width - 2 * margin scale = target_width / img["width"] new_height = img["height"] * scale doc.set_image_bounds( page_idx, img["name"], margin, img["y"], target_width, new_height ) doc.save("fitted.pdf") ``` ## Related Pages - [Editing Overview](/docs/editing/overview) -- opening, metadata, and save workflow - [Text Editing](/docs/editing/text) -- modify text around images - [Page Operations](/docs/editing/pages) -- crop and resize pages - [Annotation Editing](/docs/editing/annotations) -- add captions or notes near images --- # Redaction PDF Oxide supports a two-phase redaction workflow following the PDF specification: first, mark regions for redaction using redaction annotations, then apply the redactions to draw colored overlays that hide the content. This approach gives you a review step before permanently obscuring sensitive information. ## Redaction Workflow The standard redaction process has three steps: 1. **Mark** -- Add redaction annotations to identify content to hide 2. **Review** -- Inspect marked regions before applying (optional) 3. **Apply** -- Draw overlays and remove redaction annotations 4. **Save** -- Write the redacted PDF to disk ## Step 1: Add Redaction Annotations Use the PdfPage annotation API to add redaction annotations that mark regions for removal. **Python** ```python from pdf_oxide import PdfDocument doc = PdfDocument("confidential.pdf") page = doc.page(0) # Find sensitive text and mark it for redaction for t in page.find_text_containing("SSN"): bbox = t.bbox # (x, y, width, height) page.add_highlight(bbox[0], bbox[1], bbox[2], bbox[3], (0.0, 0.0, 0.0)) doc.save_page(page) ``` **Rust** ```rust use pdf_oxide::api::Pdf; use pdf_oxide::writer::RedactAnnotation; use pdf_oxide::geometry::Rect; let mut doc = Pdf::open("confidential.pdf")?; let mut page = doc.page(0)?; // Mark a specific region for redaction let redact = RedactAnnotation::new( Rect::new(100.0, 700.0, 200.0, 14.0) ); page.add_annotation(redact); // Mark all text containing "SSN" for redaction let sensitive = page.find_text_containing("SSN"); for t in &sensitive { let redact = RedactAnnotation::new(t.bbox()); page.add_annotation(redact); } doc.save_page(page)?; ``` ## Step 2: Apply Redactions Once redaction annotations are in place, apply them to draw colored overlays over the marked regions. This step finds all redaction annotations, renders overlays, and removes the redaction annotations. ### Apply on a Single Page ```python doc = PdfDocument("marked.pdf") doc.apply_page_redactions(0) doc.save("redacted.pdf") ``` ```rust use pdf_oxide::editor::DocumentEditor; let mut editor = DocumentEditor::open("marked.pdf")?; editor.apply_page_redactions(0)?; editor.save("redacted.pdf")?; ``` ### Apply on All Pages ```python doc = PdfDocument("marked.pdf") doc.apply_all_redactions() doc.save("redacted.pdf") ``` ```rust let mut editor = DocumentEditor::open("marked.pdf")?; editor.apply_all_redactions()?; editor.save("redacted.pdf")?; ``` ## Checking Redaction Status ### Check if a Page is Marked ```python doc = PdfDocument("input.pdf") doc.apply_page_redactions(0) print(doc.is_page_marked_for_redaction(0)) # True (before save) ``` ```rust let mut editor = DocumentEditor::open("input.pdf")?; editor.apply_page_redactions(0)?; assert!(editor.is_page_marked_for_redaction(0)); ``` ### Cancel Pending Redactions If you change your mind before saving, unmark a page to cancel the pending redaction. ```python doc.unmark_page_for_redaction(0) print(doc.is_page_marked_for_redaction(0)) # False ``` ```rust editor.unmark_page_for_redaction(0); assert!(!editor.is_page_marked_for_redaction(0)); ``` ## Complete Redaction Workflow **Python** ```python from pdf_oxide import PdfDocument doc = PdfDocument("sensitive-report.pdf") # Step 1: Add redaction annotations via the DOM for i in range(doc.page_count): page = doc.page(i) # Mark SSN patterns for t in page.find_text_containing("SSN"): bbox = t.bbox page.add_highlight(bbox[0], bbox[1], bbox[2], bbox[3], (0.0, 0.0, 0.0)) # Mark email addresses for t in page.find_text_containing("@"): bbox = t.bbox page.add_highlight(bbox[0], bbox[1], bbox[2], bbox[3], (0.0, 0.0, 0.0)) doc.save_page(page) # Step 2: Apply all redactions doc.apply_all_redactions() # Step 3: Save the redacted document doc.save("report-redacted.pdf") ``` **Rust** ```rust use pdf_oxide::api::Pdf; use pdf_oxide::writer::RedactAnnotation; let mut doc = Pdf::open("sensitive-report.pdf")?; let count = doc.page_count()?; // Step 1: Mark regions for redaction for i in 0..count { let mut page = doc.page(i)?; // Find and mark sensitive text let ssn_matches = page.find_text_containing("SSN"); for t in &ssn_matches { let redact = RedactAnnotation::new(t.bbox()); page.add_annotation(redact); } let email_matches = page.find_text_containing("@"); for t in &email_matches { let redact = RedactAnnotation::new(t.bbox()); page.add_annotation(redact); } doc.save_page(page)?; } // Step 2: Apply redactions let editor = doc.editor().unwrap(); editor.apply_all_redactions()?; // Step 3: Save doc.save("report-redacted.pdf")?; ``` ## Full API Reference ### DocumentEditor Redaction Methods | Method | Returns | Description | |--------|---------|-------------| | `apply_page_redactions(page)` | `Result<()>` | Apply redactions on a single page | | `apply_all_redactions()` | `Result<()>` | Apply redactions on all pages | | `is_page_marked_for_redaction(page)` | `bool` | Check if page has pending redactions | | `unmark_page_for_redaction(page)` | `()` | Cancel pending redactions for a page | ### Python (PdfDocument) Methods | Method | Parameters | Description | |--------|------------|-------------| | `apply_page_redactions(page)` | `page: int` | Apply redactions on a single page | | `apply_all_redactions()` | -- | Apply redactions on all pages | | `is_page_marked_for_redaction(page)` | `page: int` | Check redaction status | | `unmark_page_for_redaction(page)` | `page: int` | Cancel pending redactions | ## Important Notes - **Visual overlay**: Redaction draws a colored rectangle over the marked area. The underlying content stream data is visually hidden but may still be present in the file. For complete removal, consider combining redaction with a full rewrite save. - **Two-phase process**: Always add redaction annotations first, then call `apply_page_redactions()` or `apply_all_redactions()`. Calling apply without redaction annotations has no effect. - **Irreversible**: Once saved, the visual overlay is permanent. Always work on a copy of the original document. - **Color**: By default, redactions use a black overlay. Use `RedactAnnotation` with color options for custom overlay colors. ## Related Pages - [Annotation Editing](/docs/editing/annotations) -- working with annotations - [Page Operations](/docs/editing/pages) -- content erasure (whiteout) as an alternative - [Text Editing](/docs/editing/text) -- finding text to redact - [Encryption & Security](/docs/editing/encryption) -- restrict access after redaction --- # Encryption & Security PDF Oxide supports encrypting PDFs with passwords and permissions using industry-standard algorithms. You can set a user password (required to open the document), an owner password (required for full access), and fine-grained permissions controlling printing, copying, and modification. ## Quick Start: Save with Encryption **Python** ```python from pdf_oxide import PdfDocument doc = PdfDocument("input.pdf") doc.set_title("Confidential Report") # Encrypt with user and owner passwords doc.save_encrypted("protected.pdf", "user123", "owner456") ``` **Rust** ```rust use pdf_oxide::api::Pdf; let mut doc = Pdf::open("input.pdf")?; // Simple encryption with user and owner passwords doc.save_encrypted("protected.pdf", "user123", "owner456")?; ``` ## Encryption with Custom Permissions **Python** The `save_encrypted` method accepts permission flags as keyword arguments. ```python from pdf_oxide import PdfDocument doc = PdfDocument("input.pdf") # View-only: no printing, copying, or modifying doc.save_encrypted( "readonly.pdf", "viewpass", "adminpass", allow_print=False, allow_copy=False, allow_modify=False, allow_annotate=False, ) # Allow only printing doc.save_encrypted( "print-only.pdf", "", # No open password required "adminpass", allow_print=True, allow_copy=False, allow_modify=False, allow_annotate=False, ) ``` ### Python save_encrypted Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `path` | `str` | required | Output file path | | `user_password` | `str` | required | Password to open (empty = no open password) | | `owner_password` | `str` | `None` | Full-access password (defaults to user password) | | `allow_print` | `bool` | `True` | Allow printing | | `allow_copy` | `bool` | `True` | Allow copying text/graphics | | `allow_modify` | `bool` | `True` | Allow modifying the document | | `allow_annotate` | `bool` | `True` | Allow adding annotations | **Rust** For complete control over encryption settings, use `EncryptionConfig` and `SaveOptions`. ```rust use pdf_oxide::api::Pdf; use pdf_oxide::editor::{ EncryptionConfig, EncryptionAlgorithm, Permissions, SaveOptions, }; let mut doc = Pdf::open("input.pdf")?; // Build permissions let mut perms = Permissions::read_only(); perms.print = true; // Allow printing only // Build encryption config let config = EncryptionConfig::new("user123", "owner456") .with_algorithm(EncryptionAlgorithm::Aes256) .with_permissions(perms); // Save with encryption doc.save_with_encryption("protected.pdf", config)?; ``` ## EncryptionConfig The `EncryptionConfig` struct controls all encryption parameters. ```rust use pdf_oxide::editor::{EncryptionConfig, EncryptionAlgorithm, Permissions}; let config = EncryptionConfig { user_password: "user123".to_string(), owner_password: "owner456".to_string(), algorithm: EncryptionAlgorithm::Aes256, permissions: Permissions::all(), }; ``` Or use the builder pattern: ```rust let config = EncryptionConfig::new("user123", "owner456") .with_algorithm(EncryptionAlgorithm::Aes128) .with_permissions(Permissions::read_only()); ``` ### EncryptionConfig Fields | Field | Type | Description | |-------|------|-------------| | `user_password` | `String` | Password required to open the document | | `owner_password` | `String` | Password for full access and changing security | | `algorithm` | `EncryptionAlgorithm` | Encryption algorithm to use | | `permissions` | `Permissions` | Access control flags | ## Encryption Algorithms | Algorithm | Description | |-----------|-------------| | `EncryptionAlgorithm::Aes256` | AES-256 (strongest, recommended) | | `EncryptionAlgorithm::Aes128` | AES-128 | | `EncryptionAlgorithm::Rc4_128` | RC4 128-bit (legacy compatibility) | | `EncryptionAlgorithm::Rc4_40` | RC4 40-bit (legacy, weak) | AES-256 is the default when using `save_encrypted()` in Python or the `Pdf` API. ## Permissions The `Permissions` struct controls what operations are allowed when a document is opened with the user password. ```rust use pdf_oxide::editor::Permissions; // Allow everything let all = Permissions::all(); // Restrict everything let readonly = Permissions::read_only(); ``` ### Permissions Fields | Field | Type | Default (all) | Default (read_only) | Description | |-------|------|---------------|---------------------|-------------| | `print` | `bool` | `true` | `false` | Allow printing | | `print_high_quality` | `bool` | `true` | `false` | Allow high-quality printing | | `modify` | `bool` | `true` | `false` | Allow modifying content | | `copy` | `bool` | `true` | `false` | Allow copying text/graphics | | `annotate` | `bool` | `true` | `false` | Allow adding annotations | | `fill_forms` | `bool` | `true` | `false` | Allow filling form fields | | `accessibility` | `bool` | `true` | `true` | Allow accessibility extraction | | `assemble` | `bool` | `true` | `false` | Allow page assembly operations | ### Custom Permissions ```rust let mut perms = Permissions::read_only(); perms.print = true; // Allow printing perms.fill_forms = true; // Allow filling forms perms.accessibility = true; // Always allow for compliance ``` ## SaveOptions Use `SaveOptions` for full control over how the document is written. ```rust use pdf_oxide::editor::{SaveOptions, EncryptionConfig}; // Full rewrite (default) let opts = SaveOptions::full_rewrite(); // Incremental update (faster, preserves structure) let opts = SaveOptions::incremental(); // With encryption let config = EncryptionConfig::new("user", "owner"); let opts = SaveOptions::with_encryption(config); ``` ## Opening Encrypted PDFs **Python** Pass the password when opening the document. ```python from pdf_oxide import PdfDocument doc = PdfDocument("protected.pdf", password="user123") text = doc.extract_text(0) print(text) ``` **Rust** ```rust use pdf_oxide::PdfDocument; let doc = PdfDocument::open_with_password("protected.pdf", "user123")?; let text = doc.extract_text(0)?; println!("{}", text); ``` ## Complete Encryption Workflow **Python** ```python from pdf_oxide import PdfDocument # Open and modify doc = PdfDocument("report.pdf") doc.set_title("Confidential Report") doc.set_author("Finance Team") # Save with view-only restrictions doc.save_encrypted( "report-protected.pdf", "", # No password to open "admin2025", # Owner password for full access allow_print=True, allow_copy=False, allow_modify=False, ) ``` **Rust** ```rust use pdf_oxide::api::Pdf; use pdf_oxide::editor::{ DocumentEditor, EditableDocument, EncryptionConfig, EncryptionAlgorithm, Permissions, SaveOptions, }; // Open and modify let mut doc = Pdf::open("report.pdf")?; { let editor = doc.editor().unwrap(); editor.set_title("Confidential Report"); editor.set_author("Finance Team"); } // Configure encryption let permissions = Permissions { print: true, print_high_quality: true, modify: false, copy: false, annotate: false, fill_forms: true, accessibility: true, assemble: false, }; let config = EncryptionConfig::new("", "admin2025") .with_algorithm(EncryptionAlgorithm::Aes256) .with_permissions(permissions); doc.save_with_encryption("report-protected.pdf", config)?; ``` ### Re-encrypt with Different Settings **Rust** ```rust use pdf_oxide::editor::{DocumentEditor, EditableDocument, EncryptionConfig, SaveOptions}; // Open with current password let mut editor = DocumentEditor::open("old-protected.pdf")?; // Save with new encryption let config = EncryptionConfig::new("newuser", "newowner"); let options = SaveOptions::with_encryption(config); editor.save_with_options("re-encrypted.pdf", options)?; ``` ## Full API Reference ### Pdf Methods | Method | Returns | Description | |--------|---------|-------------| | `save_encrypted(path, user_pw, owner_pw)` | `Result<()>` | Save with AES-256 and full permissions | | `save_with_encryption(path, config)` | `Result<()>` | Save with custom encryption config | ### DocumentEditor / EditableDocument Methods | Method | Returns | Description | |--------|---------|-------------| | `save(path)` | `Result<()>` | Save with full rewrite (no encryption) | | `save_with_options(path, options)` | `Result<()>` | Save with custom options | ### Configuration Types | Type | Description | |------|-------------| | `EncryptionConfig` | User/owner passwords, algorithm, permissions | | `EncryptionAlgorithm` | `Aes256`, `Aes128`, `Rc4_128`, `Rc4_40` | | `Permissions` | Fine-grained access control flags | | `SaveOptions` | Full rewrite, incremental, or encrypted save | ## Related Pages - [Editing Overview](/docs/editing/overview) -- opening, metadata, and save workflow - [Form Field Editing](/docs/editing/forms) -- restrict form editing with permissions - [Redaction](/docs/editing/redaction) -- redact content before encrypting - [Page Operations](/docs/editing/pages) -- prepare pages before final encryption --- # PDF/A Validation PDF/A (ISO 19005) is the international standard for long-term archival of electronic documents. PDF Oxide validates all major PDF/A levels and can convert non-compliant documents toward compliance. ## Supported Levels | Level | Standard | Structure | Unicode | Transparency | Embedded Files | |-------|----------|-----------|---------|--------------|----------------| | **1a** | ISO 19005-1 | Required | Required | No | No | | **1b** | ISO 19005-1 | No | No | No | No | | **2a** | ISO 19005-2 | Required | Required | Yes | No | | **2b** | ISO 19005-2 | No | No | Yes | No | | **2u** | ISO 19005-2 | No | Required | Yes | No | | **3a** | ISO 19005-3 | Required | Required | Yes | Yes | | **3b** | ISO 19005-3 | No | No | Yes | Yes | | **3u** | ISO 19005-3 | No | Required | Yes | Yes | **Level "a"** (accessible) requires a tagged structure tree and Unicode character mapping. **Level "b"** (basic) requires only visual reproducibility. **Level "u"** (Unicode) requires Unicode text mapping without the full structure tree. ## Quick Validation Use the convenience function for a one-call check: ```rust use pdf_oxide::PdfDocument; use pdf_oxide::compliance::{validate_pdf_a, PdfALevel}; let mut doc = PdfDocument::open("archive.pdf")?; let result = validate_pdf_a(&mut doc, PdfALevel::A1b)?; if result.has_errors() { println!("Not PDF/A-1b compliant:"); for error in &result.errors { println!(" [{}] {} (clause {})", error.code, error.message, error.clause.as_deref().unwrap_or("n/a")); } } else { println!("Document is PDF/A-1b compliant"); } ``` ## Validator API The `PdfAValidator` provides a builder pattern for fine-grained control: ```rust use pdf_oxide::PdfDocument; use pdf_oxide::compliance::{PdfAValidator, PdfALevel}; let mut doc = PdfDocument::open("report.pdf")?; let result = PdfAValidator::new() .stop_on_first_error(false) .include_warnings(true) .validate(&mut doc, PdfALevel::A2b)?; println!("Errors: {}", result.errors.len()); println!("Warnings: {}", result.warnings.len()); ``` ### Targeted Checks Run individual validation categories instead of the full suite: ```rust use pdf_oxide::PdfDocument; use pdf_oxide::compliance::{PdfAValidator, PdfALevel}; let mut doc = PdfDocument::open("report.pdf")?; let validator = PdfAValidator::new(); // Check only metadata let result = validator.check_metadata(&mut doc, PdfALevel::A1b)?; // Check only fonts let result = validator.check_fonts(&mut doc, PdfALevel::A1b)?; // Check only color spaces let result = validator.check_colors(&mut doc, PdfALevel::A1b)?; // Check only transparency let result = validator.check_transparency(&mut doc, PdfALevel::A2b)?; // Check only structure tags let result = validator.check_structure(&mut doc, PdfALevel::A1a)?; ``` ## Standalone Validators Each validation category is also available as a standalone function for maximum flexibility: ```rust use pdf_oxide::PdfDocument; use pdf_oxide::compliance::validators::*; use pdf_oxide::compliance::{PdfALevel, ValidationResult}; let mut doc = PdfDocument::open("document.pdf")?; let mut result = ValidationResult::new(PdfALevel::A1b); // Run each validator independently validate_xmp_metadata(&mut doc, PdfALevel::A1b, &mut result)?; validate_fonts(&mut doc, PdfALevel::A1b, &mut result)?; validate_colors(&mut doc, PdfALevel::A1b, &mut result)?; validate_encryption(&mut doc, PdfALevel::A1b, &mut result)?; validate_transparency(&mut doc, PdfALevel::A1b, &mut result)?; validate_structure(&mut doc, PdfALevel::A1b, &mut result)?; validate_javascript(&mut doc, PdfALevel::A1b, &mut result)?; validate_embedded_files(&mut doc, PdfALevel::A1b, &mut result)?; validate_annotations(&mut doc, PdfALevel::A1b, &mut result)?; println!("Total errors: {}", result.errors.len()); ``` ### Validator Summary | Function | What It Checks | |----------|----------------| | `validate_xmp_metadata()` | XMP stream exists, `pdfaid:part` and `pdfaid:conformance` entries present, metadata consistency | | `validate_fonts()` | All fonts embedded, glyph widths present, Unicode mapping available (for level "a" and "u") | | `validate_colors()` | No device-dependent color operators (`rg`, `RG`, `k`, `K`, `g`, `G`) without output intent | | `validate_encryption()` | No encryption permitted in PDF/A documents | | `validate_transparency()` | No transparency in PDF/A-1; allowed in PDF/A-2 and later | | `validate_structure()` | Tagged structure tree present with valid role mapping (required for level "a") | | `validate_javascript()` | No JavaScript actions or triggers present | | `validate_embedded_files()` | Not allowed in PDF/A-1 or PDF/A-2; PDF/A-3 requires `AFRelationship` key on each file spec | | `validate_annotations()` | Annotation types restricted per the relevant ISO 19005 part | ## ValidationResult The `ValidationResult` struct contains the full outcome of a validation run: ```rust pub struct ValidationResult { pub level: PdfALevel, pub errors: Vec, pub warnings: Vec, pub stats: ValidationStats, } ``` | Field | Type | Description | |-------|------|-------------| | `level` | `PdfALevel` | The target compliance level | | `errors` | `Vec` | Blocking violations that prevent compliance | | `warnings` | `Vec` | Non-blocking issues that may affect quality | | `stats` | `ValidationStats` | Counts of pages, fonts, and objects checked | ### ComplianceError ```rust pub struct ComplianceError { pub code: ErrorCode, pub message: String, pub location: Option, pub clause: Option, } ``` The `code` field uses the `ErrorCode` enum with categories like `MissingXmpMetadata`, `FontNotEmbedded`, `DeviceDependentColor`, `EncryptionPresent`, `TransparencyNotAllowed`, `MissingStructureTree`, `JavaScriptPresent`, and `InvalidEmbeddedFile`. ### ComplianceWarning ```rust pub struct ComplianceWarning { pub code: WarningCode, pub message: String, pub location: Option, } ``` ## PDF/A Conversion Convert a non-compliant document toward PDF/A compliance: ```rust use pdf_oxide::PdfDocument; use pdf_oxide::compliance::{convert_to_pdf_a, PdfALevel}; let mut doc = PdfDocument::open("input.pdf")?; let result = convert_to_pdf_a(&mut doc, PdfALevel::A1b)?; println!("Conversion actions taken:"); for action in &result.actions { println!(" - {}: {}", action.action_type, action.description); } if result.remaining_errors.is_empty() { println!("Document is now PDF/A-1b compliant"); } else { println!("{} issues could not be resolved automatically", result.remaining_errors.len()); } ``` ### Conversion Config Fine-tune the conversion process: ```rust use pdf_oxide::PdfDocument; use pdf_oxide::compliance::{PdfAConverter, PdfALevel, ConversionConfig}; let mut doc = PdfDocument::open("input.pdf")?; let config = ConversionConfig::new() .embed_fonts(true) .remove_javascript(true) .flatten_transparency(true) .add_structure(true); let result = PdfAConverter::new(PdfALevel::A2b) .with_config(config) .convert(&mut doc)?; ``` The converter performs these actions automatically: 1. **XMP metadata injection** -- adds `pdfaid:part` and `pdfaid:conformance` entries 2. **Font embedding** -- embeds any referenced but non-embedded fonts 3. **JavaScript removal** -- strips JavaScript actions and triggers 4. **Transparency flattening** -- renders transparent elements to opaque (PDF/A-1 only) 5. **ICC profile conversion** -- converts device-dependent colors to ICC-based color spaces 6. **Structure tagging** -- adds basic structure tags (for level "a" targets) ## Workflow: Validate, Fix, Re-validate A typical archival workflow validates, attempts automatic conversion, then re-validates: ```rust use pdf_oxide::PdfDocument; use pdf_oxide::compliance::{validate_pdf_a, convert_to_pdf_a, PdfALevel}; let level = PdfALevel::A2b; let mut doc = PdfDocument::open("input.pdf")?; // Step 1: Initial validation let result = validate_pdf_a(&mut doc, level)?; if !result.has_errors() { println!("Already compliant"); return Ok(()); } println!("{} errors found, attempting conversion...", result.errors.len()); // Step 2: Automatic conversion let conversion = convert_to_pdf_a(&mut doc, level)?; println!("{} actions taken", conversion.actions.len()); // Step 3: Re-validate let result = validate_pdf_a(&mut doc, level)?; if result.has_errors() { println!("{} errors remain after conversion:", result.errors.len()); for e in &result.errors { println!(" {} -- {}", e.code, e.message); } } else { println!("Document is now PDF/A-2b compliant"); } ``` ## PdfALevel Methods The `PdfALevel` enum includes helper methods for querying level capabilities: | Method | Return | Description | |--------|--------|-------------| | `part()` | `PdfAPart` | ISO 19005 part (Part1, Part2, Part3) | | `conformance()` | `char` | Conformance letter ('a', 'b', or 'u') | | `requires_structure()` | `bool` | Whether tagged structure tree is mandatory | | `requires_unicode()` | `bool` | Whether Unicode mapping is mandatory | | `allows_transparency()` | `bool` | Whether transparency is permitted | | `allows_jpeg2000()` | `bool` | Whether JPEG 2000 images are permitted | | `allows_embedded_files()` | `bool` | Whether file attachments are permitted | | `xmp_part()` | `&str` | XMP `pdfaid:part` value | | `xmp_conformance()` | `&str` | XMP `pdfaid:conformance` value | | `from_xmp(part, conformance)` | `Option` | Parse level from XMP metadata values | ## Next Steps - [PDF/UA Accessibility](/docs/compliance/pdf-ua) -- accessibility validation - [PDF/X Print Production](/docs/compliance/pdf-x) -- print production validation - [API Reference](/docs/reference/api) -- complete Rust API --- # PDF/UA Accessibility PDF/UA (ISO 14289) defines requirements for universally accessible PDF documents. PDF Oxide validates structure trees, heading sequences, alt text, table headers, language declarations, and more. ## Supported Levels | Level | Standard | Description | |-------|----------|-------------| | **PDF/UA-1** | ISO 14289-1:2014 | Base accessibility requirements | | **PDF/UA-2** | ISO 14289-2:2024 | Enhanced requirements, aligned with WCAG 2.1 | ## Quick Validation ```rust use pdf_oxide::PdfDocument; use pdf_oxide::compliance::{validate_pdf_ua, PdfUaLevel}; let mut doc = PdfDocument::open("accessible.pdf")?; let result = validate_pdf_ua(&mut doc, PdfUaLevel::UA1)?; if result.has_errors() { println!("Not PDF/UA-1 compliant:"); for error in &result.errors { println!(" [{}] {} (clause {})", error.code, error.message, error.clause.as_deref().unwrap_or("n/a")); } } else { println!("Document is PDF/UA-1 compliant"); } ``` ## Validator API The `PdfUaValidator` builder allows configuring specific checks: ```rust use pdf_oxide::PdfDocument; use pdf_oxide::compliance::{PdfUaValidator, PdfUaLevel}; let mut doc = PdfDocument::open("report.pdf")?; let result = PdfUaValidator::new() .check_heading_sequence(true) .check_color_contrast(true) .allow_custom_types(vec!["Caption".into(), "Aside".into()]) .validate(&mut doc, PdfUaLevel::UA1)?; println!("Errors: {}", result.errors.len()); println!("Warnings: {}", result.warnings.len()); println!("Structure elements checked: {}", result.stats.structure_elements_checked); ``` ### Configuration Options | Method | Default | Description | |--------|---------|-------------| | `check_heading_sequence(bool)` | `true` | Validate H1-H6 do not skip levels | | `check_color_contrast(bool)` | `true` | Flag potential contrast issues | | `allow_custom_types(Vec)` | `[]` | Permit non-standard structure types without warning | ## Structure Tree Inspection Before running validation, you can inspect the document's structure tree and mark info: ```rust use pdf_oxide::PdfDocument; let mut doc = PdfDocument::open("tagged.pdf")?; // Check if the document claims to be tagged let mark_info = doc.mark_info()?; println!("Marked: {}", mark_info.marked); println!("Suspects: {}", mark_info.suspects); // Access the structure tree if let Some(tree) = doc.structure_tree()? { println!("Root tag: {}", tree.root_type); println!("Children: {}", tree.children.len()); } ``` The `mark_info()` method returns: | Field | Type | Description | |-------|------|-------------| | `marked` | `bool` | Whether the document declares itself as tagged | | `suspects` | `bool` | Whether tag assignments may be incorrect | | `user_properties` | `bool` | Whether user properties are present | When `suspects` is `true`, PDF Oxide automatically falls back to geometric ordering for text extraction instead of relying on the potentially unreliable structure tree. ## What Gets Checked The validator covers the following PDF/UA requirements: ### Document-Level | Check | Clause | Description | |-------|--------|-------------| | Language | 7.2 | `/Lang` entry present in the catalog | | Title | 7.1 | Document title set and displayed in title bar | | Tagged | 7.1 | MarkInfo dictionary declares `Marked = true` | | XMP metadata | 7.1 | `pdfuaid:part` declared in XMP stream | ### Structure | Check | Clause | Description | |-------|--------|-------------| | Structure tree | 7.1 | Complete structure tree rooted at StructTreeRoot | | Role mapping | 7.5 | Non-standard types mapped to standard structure elements | | Heading hierarchy | 7.4.2 | Headings (H1-H6) do not skip levels | | Artifact marking | 7.3 | Decorative content marked as artifact | | Reading order | 7.2 | Structure tree defines a logical reading order | ### Content | Check | Clause | Description | |-------|--------|-------------| | Alt text for images | 7.3 | `/Alt` or `/ActualText` on Figure elements | | Table headers | 7.5 | `TH` elements present in table structures | | Form labels | 7.6.2 | Form fields have associated labels or tooltips | | Link text | 7.18 | Link annotations have descriptive content | | List structure | 7.4.3 | Lists use `L`, `LI`, `Lbl`, `LBody` structure | ### Font and Text | Check | Clause | Description | |-------|--------|-------------| | Unicode mapping | 7.21.3 | All text has a Unicode representation | | Font embedding | 7.21.4 | Fonts embedded or standard Base14 fonts | | ActualText | 7.21.5 | Ligatures and special glyphs have `/ActualText` | ## UaValidationResult ```rust pub struct UaValidationResult { pub level: PdfUaLevel, pub errors: Vec, pub warnings: Vec, pub stats: UaValidationStats, } ``` ### UaComplianceError Each error includes optional WCAG alignment: ```rust pub struct UaComplianceError { pub code: UaErrorCode, pub message: String, pub location: Option, pub wcag_ref: Option, pub clause: Option, } ``` The `wcag_ref` field maps the PDF/UA violation to the corresponding WCAG success criterion (e.g., `"1.1.1"` for non-text content, `"1.3.1"` for info and relationships). ### UaErrorCode Categories The `UaErrorCode` enum includes error categories such as: - `MissingLanguage` -- no `/Lang` entry on the document catalog - `MissingStructureTree` -- document is not tagged - `MissingAltText` -- Figure element lacks alt text - `HeadingSkipped` -- heading levels jump (e.g., H1 to H3) - `MissingTableHeaders` -- table lacks `TH` elements - `FormFieldNoLabel` -- form field has no associated label - `InvalidRoleMapping` -- non-standard type not mapped to a standard element - `ArtifactNotMarked` -- decorative content not marked as artifact - `MissingUnicode` -- text without Unicode mapping ## Practical Example: Accessibility Report Generate a human-readable accessibility report from validation results: ```rust use pdf_oxide::PdfDocument; use pdf_oxide::compliance::{validate_pdf_ua, PdfUaLevel}; let mut doc = PdfDocument::open("document.pdf")?; let result = validate_pdf_ua(&mut doc, PdfUaLevel::UA1)?; println!("=== PDF/UA Accessibility Report ==="); println!("Level: PDF/UA-{}", result.level.xmp_part()); println!("Status: {}", if result.has_errors() { "FAIL" } else { "PASS" }); println!(); if result.has_errors() { println!("Errors ({}):", result.errors.len()); for (i, error) in result.errors.iter().enumerate() { print!(" {}. [{}] {}", i + 1, error.code, error.message); if let Some(ref wcag) = error.wcag_ref { print!(" (WCAG {})", wcag); } println!(); } } if result.has_warnings() { println!("\nWarnings ({}):", result.warnings.len()); for warning in &result.warnings { println!(" - [{}] {}", warning.code, warning.message); } } println!("\nStats:"); println!(" Structure elements checked: {}", result.stats.structure_elements_checked); ``` ## Next Steps - [PDF/A Validation](/docs/compliance/pdf-a) -- archival compliance - [PDF/X Print Production](/docs/compliance/pdf-x) -- print production compliance - [API Reference](/docs/reference/api) -- complete Rust API --- # PDF/X Print Production PDF/X (ISO 15930) is the standard for reliable exchange of print-ready PDF files. PDF Oxide validates all major PDF/X levels, checking page boxes, color spaces, transparency, ICC profiles, and output intents. ## Supported Levels | Level | Standard | Transparency | RGB | Layers | External ICC | External Graphics | |-------|----------|-------------|-----|--------|-------------|-------------------| | **X-1a:2001** | ISO 15930-1 | No | No | No | No | No | | **X-1a:2003** | ISO 15930-4 | No | No | No | No | No | | **X-3:2002** | ISO 15930-3 | No | Yes | No | No | No | | **X-3:2003** | ISO 15930-6 | No | Yes | No | No | No | | **X-4** | ISO 15930-7 | Yes | Yes | Yes | No | No | | **X-4p** | ISO 15930-7 | Yes | Yes | Yes | Yes | No | | **X-5g** | ISO 15930-8 | Yes | Yes | Yes | No | Yes | | **X-5n** | ISO 15930-8 | Yes | Yes | Yes | No | Yes | | **X-5pg** | ISO 15930-8 | Yes | Yes | Yes | Yes | Yes | | **X-6** | ISO 15930-9 | Yes | Yes | Yes | No | No | | **X-6n** | ISO 15930-9 | Yes | Yes | Yes | No | Yes | | **X-6p** | ISO 15930-9 | Yes | Yes | Yes | Yes | No | **PDF/X-1a** is the most restrictive: CMYK-only, no transparency, no layers. **PDF/X-4** is the most commonly used modern level, allowing transparency and RGB with ICC profiles. ## Quick Validation ```rust use pdf_oxide::PdfDocument; use pdf_oxide::compliance::pdf_x::{validate_pdf_x, PdfXLevel}; let mut doc = PdfDocument::open("print-ready.pdf")?; let result = validate_pdf_x(&mut doc, PdfXLevel::X4)?; if result.has_errors() { println!("Not PDF/X-4 compliant ({} errors):", result.errors.len()); for error in &result.errors { println!(" [{}] {} (clause {})", error.code, error.message, error.clause.as_deref().unwrap_or("n/a")); } } else { println!("Document is PDF/X-4 compliant"); } ``` ## Validator API The `PdfXValidator` builder configures the validation run: ```rust use pdf_oxide::PdfDocument; use pdf_oxide::compliance::pdf_x::{PdfXValidator, PdfXLevel}; let mut doc = PdfDocument::open("artwork.pdf")?; let result = PdfXValidator::new(PdfXLevel::X1a2001) .stop_on_first_error(false) .include_warnings(true) .validate(&mut doc)?; println!("Errors: {}", result.errors.len()); println!("Warnings: {}", result.warnings.len()); println!("Total issues: {}", result.total_issues()); ``` ## What Gets Checked ### XMP Identification The validator confirms that XMP metadata declares the correct PDF/X version: - `pdfxid:GTS_PDFXVersion` must match the target level - The declared version is compared against the `gts_pdfx_version()` for the target level ### Page Box Relationships PDF/X requires specific nesting of page boxes: ``` TrimBox <= BleedBox <= MediaBox ArtBox <= MediaBox ``` The validator checks every page to ensure: - **TrimBox** is present (required by all PDF/X levels) - **TrimBox is contained within BleedBox** (if BleedBox is defined) - **BleedBox is contained within MediaBox** - **ArtBox is contained within MediaBox** (if ArtBox is defined) - Tolerance of 0.01 points for floating-point rounding ### Transparency Detection For PDF/X-1a and PDF/X-3, no transparency is permitted. The validator checks: - **SMask** in ExtGState dictionaries (must be `/None` or absent) - **CA** (stroke opacity) must equal 1.0 - **ca** (fill opacity) must equal 1.0 - **BM** (blend mode) must be `Normal` or `Compatible` PDF/X-4 and later levels permit transparency. ### Color Space Validation The validator checks for device-dependent color usage: - **DeviceRGB** is not allowed in PDF/X-1a (CMYK-only) - **DeviceRGB**, **DeviceCMYK**, and **DeviceGray** used without an output intent trigger errors in stricter levels - Color operators `rg`, `RG`, `k`, `K`, `g`, `G` in page content streams are scanned ### ICC Profile Validation For ICCBased color spaces, the validator checks: - The profile stream contains the required `/N` (number of components) entry - The `/N` value matches the expected color space dimension (1 for gray, 3 for RGB, 4 for CMYK) - The profile data is present and non-empty ### Output Intent PDF/X requires an output intent that describes the intended print condition: - `/OutputIntents` array must be present in the document catalog - At least one entry with subtype `GTS_PDFX` is required - The output intent should reference an ICC profile or a registered print condition ## XValidationResult ```rust pub struct XValidationResult { pub level: PdfXLevel, pub errors: Vec, pub warnings: Vec, pub stats: XValidationStats, } ``` ### XComplianceError ```rust pub struct XComplianceError { pub code: XErrorCode, pub message: String, pub severity: XSeverity, pub page: Option, pub object_id: Option, pub clause: Option, } ``` Errors include the page number and object ID where the violation was found, making it straightforward to locate and fix issues in the source file. ### XErrorCode Categories The `XErrorCode` enum contains 40+ specific error codes organized by category: **Metadata**: `MissingOutputIntent`, `InvalidGtsPdfxVersion`, `MissingXmpIdentification` **Page boxes**: `MissingTrimBox`, `TrimBoxOutsideBleedBox`, `BleedBoxOutsideMediaBox`, `ArtBoxOutsideMediaBox` **Transparency**: `TransparencyNotAllowed`, `InvalidBlendMode`, `InvalidSMask`, `InvalidOpacity` **Color**: `DeviceRgbNotAllowed`, `DeviceDependentColorWithoutIntent`, `InvalidIccProfile`, `MissingIccComponents` **Content**: `ExternalContentNotAllowed`, `EncryptionNotAllowed`, `JavaScriptNotAllowed` ## PdfXLevel Methods | Method | Return | Description | |--------|--------|-------------| | `iso_standard()` | `&str` | ISO standard number (e.g., `"ISO 15930-7"`) | | `required_pdf_version()` | `&str` | Minimum PDF version (e.g., `"1.6"`) | | `allows_transparency()` | `bool` | Whether transparency groups are permitted | | `allows_rgb()` | `bool` | Whether RGB color space is permitted | | `allows_layers()` | `bool` | Whether optional content groups are permitted | | `allows_external_icc()` | `bool` | Whether external ICC profiles are permitted | | `allows_external_graphics()` | `bool` | Whether external graphics references are permitted | | `gts_pdfx_version()` | `&str` | Expected `GTS_PDFXVersion` value | | `xmp_version()` | `&str` | Expected XMP version identifier | | `from_gts_version(version)` | `Option` | Parse level from a GTS version string | ## Practical Example: Prepress Check ```rust use pdf_oxide::PdfDocument; use pdf_oxide::compliance::pdf_x::{validate_pdf_x, PdfXLevel, XSeverity}; let mut doc = PdfDocument::open("magazine-cover.pdf")?; let result = validate_pdf_x(&mut doc, PdfXLevel::X4)?; println!("=== PDF/X-4 Prepress Report ==="); println!("Status: {}", if result.has_errors() { "REJECT" } else { "ACCEPT" }); // Group errors by severity let critical: Vec<_> = result.errors.iter() .filter(|e| e.is_error()) .collect(); let advisory: Vec<_> = result.warnings.iter().collect(); if !critical.is_empty() { println!("\nCritical ({}):", critical.len()); for e in &critical { let page_str = e.page .map(|p| format!("page {}", p + 1)) .unwrap_or_else(|| "document".into()); println!(" [{}] {} ({})", e.code, e.message, page_str); } } if !advisory.is_empty() { println!("\nAdvisory ({}):", advisory.len()); for w in &advisory { println!(" [{}] {}", w.code, w.message); } } ``` ## Next Steps - [PDF/A Validation](/docs/compliance/pdf-a) -- archival compliance - [PDF/UA Accessibility](/docs/compliance/pdf-ua) -- accessibility validation - [API Reference](/docs/reference/api) -- complete Rust API --- # Rust API Reference This page documents every public struct and method in `pdf_oxide`. For the Python bindings, see the [Python API Reference](/docs/reference/python-api). For type and enum details, see [Types & Enums](/docs/reference/types). ## PdfDocument The low-level document handle. Open a PDF file, extract text, images, and metadata. ```rust use pdf_oxide::PdfDocument; ``` ### Opening and Authentication | Method | Signature | Description | |--------|-----------|-------------| | `open` | `fn open(path: impl AsRef) -> Result` | Open a PDF file from disk | | `open_with_config` | `fn open_with_config(path: impl AsRef, config: impl Any) -> Result` | Open with parser configuration | | `authenticate` | `fn authenticate(&mut self, password: &[u8]) -> Result` | Authenticate with user or owner password | ### Metadata | Method | Signature | Description | |--------|-----------|-------------| | `page_count` | `fn page_count(&mut self) -> Result` | Number of pages in the document | | `page_count_u32` | `fn page_count_u32(&mut self) -> u32` | Page count as u32 (0 on error) | | `version` | `fn version(&self) -> (u8, u8)` | PDF version as `(major, minor)` | | `trailer` | `fn trailer(&self) -> &Object` | Raw trailer dictionary | | `catalog` | `fn catalog(&mut self) -> Result` | Document catalog dictionary | ### Text Extraction | Method | Signature | Description | |--------|-----------|-------------| | `extract_text` | `fn extract_text(&mut self, page_index: usize) -> Result` | Plain text from a single page | | `extract_spans` | `fn extract_spans(&mut self, page_index: usize) -> Result>` | Text runs with font metadata | | `extract_spans_with_config` | `fn extract_spans_with_config(&mut self, page_index: usize, config: &TextConfig) -> Result>` | Spans with custom configuration | | `extract_chars` | `fn extract_chars(&mut self, page_index: usize) -> Result>` | Per-character positions and metadata | | `extract_text_with_ocr` | `fn extract_text_with_ocr(&mut self, page_index: usize) -> Result` | Text with OCR fallback for scanned pages | | `extract_spans_with_ocr` | `fn extract_spans_with_ocr(&mut self, page_index: usize) -> Result>` | Spans with OCR fallback | | `apply_intelligent_text_processing` | `fn apply_intelligent_text_processing(&self, spans: Vec) -> Vec` | Ligature expansion, hyphen reconstruction, OCR cleanup | | `extract_hierarchical_content` | `fn extract_hierarchical_content(&mut self, page_index: usize) -> Result>` | Structured content tree from tagged PDFs | ### Conversion | Method | Signature | Description | |--------|-----------|-------------| | `to_markdown` | `fn to_markdown(&mut self, page_index: usize, options: &ConversionOptions) -> Result` | Convert page to Markdown | | `to_html` | `fn to_html(&mut self, page_index: usize, options: &ConversionOptions) -> Result` | Convert page to HTML | | `to_plain_text` | `fn to_plain_text(&mut self, page_index: usize) -> Result` | Convert page to plain text | | `to_markdown_all` | `fn to_markdown_all(&mut self, options: &ConversionOptions) -> Result` | All pages to Markdown | | `to_html_all` | `fn to_html_all(&mut self, options: &ConversionOptions) -> Result` | All pages to HTML | | `to_plain_text_all` | `fn to_plain_text_all(&mut self) -> Result` | All pages to plain text | | `to_markdown_with_ocr` | `fn to_markdown_with_ocr(&mut self, page_index: usize, options: &ConversionOptions) -> Result` | Markdown with OCR fallback | ### Image Extraction | Method | Signature | Description | |--------|-----------|-------------| | `extract_images` | `fn extract_images(&mut self, page_index: usize) -> Result>` | Image metadata and raw data from a page | | `extract_images_to_files` | `fn extract_images_to_files(&mut self, page_index: usize, output_dir: &str) -> Result>` | Extract and save images to disk | ### Path and Graphics Extraction | Method | Signature | Description | |--------|-----------|-------------| | `extract_paths` | `fn extract_paths(&mut self, page_index: usize) -> Result>` | Vector graphics from a page | | `extract_paths_in_rect` | `fn extract_paths_in_rect(&mut self, page_index: usize, rect: Rect) -> Result>` | Paths within a rectangular region | ### Page Information | Method | Signature | Description | |--------|-----------|-------------| | `get_page_info` | `fn get_page_info(&mut self, page_index: usize) -> Result` | Page dimensions, rotation, boxes | | `get_page_resources` | `fn get_page_resources(&mut self, page_index: usize) -> Result` | Raw resources dictionary | | `get_page_content_data` | `fn get_page_content_data(&mut self, page_index: usize) -> Result>` | Raw content stream bytes | ### Structure and Accessibility | Method | Signature | Description | |--------|-----------|-------------| | `structure_tree` | `fn structure_tree(&mut self) -> Result>` | Tagged structure tree | | `mark_info` | `fn mark_info(&mut self) -> Result` | MarkInfo dictionary (tagged, suspects) | ### Low-Level | Method | Signature | Description | |--------|-----------|-------------| | `load_object` | `fn load_object(&mut self, obj_ref: ObjectRef) -> Result` | Load a PDF object by reference | | `resolve_object` | `fn resolve_object(&mut self, obj: &Object) -> Result` | Resolve indirect references | | `resolve_references` | `fn resolve_references(&mut self, obj: &Object, max_depth: usize) -> Result` | Recursively resolve all references | | `check_for_circular_references` | `fn check_for_circular_references(&mut self) -> Vec<(ObjectRef, ObjectRef)>` | Detect circular reference chains | --- ## Pdf The unified high-level API. One type for extraction, creation, editing, search, and compliance. ```rust use pdf_oxide::api::Pdf; ``` ### Constructors | Method | Signature | Description | |--------|-----------|-------------| | `new` | `fn new() -> Self` | Create an empty Pdf instance | | `open` | `fn open(path: impl AsRef) -> Result` | Open existing PDF for reading | | `open_editor` | `fn open_editor(path: impl AsRef) -> Result` | Open for structural editing | | `from_markdown` | `fn from_markdown(content: &str) -> Result` | Create PDF from Markdown | | `from_html` | `fn from_html(content: &str) -> Result` | Create PDF from HTML | | `from_text` | `fn from_text(content: &str) -> Result` | Create PDF from plain text | | `from_image` | `fn from_image(path: impl AsRef) -> Result` | Create PDF from image file | | `from_image_bytes` | `fn from_image_bytes(data: &[u8]) -> Result` | Create PDF from image bytes | | `from_images` | `fn from_images>(paths: &[P]) -> Result` | Multi-page from images | | `from_qrcode` | `fn from_qrcode(data: &str) -> Result` | PDF containing a QR code | | `from_qrcode_with_options` | `fn from_qrcode_with_options(data: &str, size: f32, ecl: &str) -> Result` | QR code with custom size and error correction | | `from_barcode` | `fn from_barcode(data: &str, barcode_type: BarcodeType) -> Result` | PDF containing a barcode | | `from_barcode_with_options` | `fn from_barcode_with_options(data: &str, barcode_type: BarcodeType, width: f32, height: f32) -> Result` | Barcode with custom dimensions | ### Extraction | Method | Signature | Description | |--------|-----------|-------------| | `page_count` | `fn page_count(&mut self) -> Result` | Number of pages | | `page` | `fn page(&mut self, index: usize) -> Result` | DOM-like page handle | | `to_markdown` | `fn to_markdown(&mut self, page: usize) -> Result` | Page to Markdown | | `to_html` | `fn to_html(&mut self, page: usize) -> Result` | Page to HTML | | `to_text` | `fn to_text(&mut self, page: usize) -> Result` | Page to plain text | ### Search | Method | Signature | Description | |--------|-----------|-------------| | `search` | `fn search(&mut self, pattern: &str) -> Result>` | Search all pages | | `search_with_options` | `fn search_with_options(&mut self, pattern: &str, opts: &SearchOptions) -> Result>` | Search with options | | `search_page` | `fn search_page(&mut self, page: usize, pattern: &str) -> Result>` | Search single page | | `highlight_matches` | `fn highlight_matches(&mut self, pattern: &str) -> Result` | Add highlight annotations for matches | ### Metadata | Method | Signature | Description | |--------|-----------|-------------| | `set_title` | `fn set_title(&mut self, title: impl Into) -> Result<()>` | Set document title | | `set_author` | `fn set_author(&mut self, author: impl Into) -> Result<()>` | Set author | | `set_subject` | `fn set_subject(&mut self, subject: impl Into) -> Result<()>` | Set subject | | `set_keywords` | `fn set_keywords(&mut self, keywords: impl Into) -> Result<()>` | Set keywords | ### XMP Metadata | Method | Signature | Description | |--------|-----------|-------------| | `xmp_metadata` | `fn xmp_metadata(&mut self) -> Result>` | Full XMP metadata | | `has_xmp_metadata` | `fn has_xmp_metadata(&mut self) -> Result` | Check XMP presence | | `xmp_title` | `fn xmp_title(&mut self) -> Result>` | XMP dc:title | | `xmp_creators` | `fn xmp_creators(&mut self) -> Result>` | XMP dc:creator list | | `xmp_description` | `fn xmp_description(&mut self) -> Result>` | XMP dc:description | | `xmp_creator_tool` | `fn xmp_creator_tool(&mut self) -> Result>` | XMP xmp:CreatorTool | | `xmp_create_date` | `fn xmp_create_date(&mut self) -> Result>` | XMP xmp:CreateDate | | `xmp_modify_date` | `fn xmp_modify_date(&mut self) -> Result>` | XMP xmp:ModifyDate | | `xmp_producer` | `fn xmp_producer(&mut self) -> Result>` | XMP pdf:Producer | ### Page Labels | Method | Signature | Description | |--------|-----------|-------------| | `page_labels` | `fn page_labels(&mut self) -> Result>` | Page label ranges | | `page_label` | `fn page_label(&mut self, page: usize) -> Result` | Label for a specific page | | `all_page_labels` | `fn all_page_labels(&mut self) -> Result>` | Labels for every page | ### Page Operations | Method | Signature | Description | |--------|-----------|-------------| | `page_rotation` | `fn page_rotation(&mut self, page: usize) -> Result` | Current rotation in degrees | | `set_page_rotation` | `fn set_page_rotation(&mut self, page: usize, degrees: i32) -> Result<()>` | Set absolute rotation | | `rotate_page` | `fn rotate_page(&mut self, page: usize, degrees: i32) -> Result<()>` | Add relative rotation | | `rotate_all_pages` | `fn rotate_all_pages(&mut self, degrees: i32) -> Result<()>` | Rotate all pages | | `page_media_box` | `fn page_media_box(&mut self, page: usize) -> Result<[f32; 4]>` | MediaBox dimensions | | `set_page_media_box` | `fn set_page_media_box(&mut self, page: usize, rect: [f32; 4]) -> Result<()>` | Set MediaBox | | `page_crop_box` | `fn page_crop_box(&mut self, page: usize) -> Result>` | CropBox dimensions | | `set_page_crop_box` | `fn set_page_crop_box(&mut self, page: usize, rect: [f32; 4]) -> Result<()>` | Set CropBox | | `crop_margins` | `fn crop_margins(&mut self, left: f32, right: f32, top: f32, bottom: f32) -> Result<()>` | Crop all pages by margins | ### Content Editing | Method | Signature | Description | |--------|-----------|-------------| | `save_page` | `fn save_page(&mut self, page: PdfPage) -> Result<()>` | Save modified page back | | `erase_region` | `fn erase_region(&mut self, page: usize, rect: [f32; 4]) -> Result<()>` | White-out a rectangular area | | `erase_regions` | `fn erase_regions(&mut self, page: usize, rects: &[[f32; 4]]) -> Result<()>` | White-out multiple areas | | `clear_erase_regions` | `fn clear_erase_regions(&mut self, page: usize)` | Clear pending erase operations | ### Annotations | Method | Signature | Description | |--------|-----------|-------------| | `flatten_page_annotations` | `fn flatten_page_annotations(&mut self, page: usize) -> Result<()>` | Flatten annotations on a page | | `flatten_all_annotations` | `fn flatten_all_annotations(&mut self) -> Result<()>` | Flatten all annotations | | `is_page_marked_for_flatten` | `fn is_page_marked_for_flatten(&self, page: usize) -> bool` | Check flatten status | | `unmark_page_for_flatten` | `fn unmark_page_for_flatten(&mut self, page: usize)` | Unmark a page | ### Forms | Method | Signature | Description | |--------|-----------|-------------| | `flatten_forms_on_page` | `fn flatten_forms_on_page(&mut self, page: usize) -> Result<()>` | Flatten forms on a page | | `flatten_forms` | `fn flatten_forms(&mut self) -> Result<()>` | Flatten all form fields | | `export_form_data_fdf` | `fn export_form_data_fdf(&mut self, output_path: impl AsRef) -> Result<()>` | Export form data as FDF | | `export_form_data_xfdf` | `fn export_form_data_xfdf(&mut self, output_path: impl AsRef) -> Result<()>` | Export form data as XFDF | ### Redactions | Method | Signature | Description | |--------|-----------|-------------| | `apply_page_redactions` | `fn apply_page_redactions(&mut self, page: usize) -> Result<()>` | Apply redactions on a page | | `apply_all_redactions` | `fn apply_all_redactions(&mut self) -> Result<()>` | Apply all pending redactions | ### Images | Method | Signature | Description | |--------|-----------|-------------| | `page_images` | `fn page_images(&mut self, page: usize) -> Result>` | List images on a page | | `reposition_image` | `fn reposition_image(&mut self, page: usize, image_index: usize, x: f32, y: f32) -> Result<()>` | Move an image | | `resize_image` | `fn resize_image(&mut self, page: usize, image_index: usize, width: f32, height: f32) -> Result<()>` | Resize an image | | `set_image_bounds` | `fn set_image_bounds(&mut self, page: usize, image_index: usize, rect: [f32; 4]) -> Result<()>` | Set image bounding box | ### Embedded Files | Method | Signature | Description | |--------|-----------|-------------| | `embed_file` | `fn embed_file(&mut self, name: &str, data: Vec) -> Result<()>` | Attach a file | | `embed_file_with_options` | `fn embed_file_with_options(&mut self, file: EmbeddedFile) -> Result<()>` | Attach with full config | ### Rendering (requires `rendering` feature) | Method | Signature | Description | |--------|-----------|-------------| | `render_page` | `fn render_page(&mut self, page: usize) -> Result` | Render to image | | `render_page_with_options` | `fn render_page_with_options(&mut self, page: usize, opts: &RenderOptions) -> Result` | Render with options | | `render_page_to_file` | `fn render_page_to_file(&mut self, page: usize, path: impl AsRef) -> Result<()>` | Render and save to file | | `render_page_to_file_with_dpi` | `fn render_page_to_file_with_dpi(&mut self, page: usize, path: impl AsRef, dpi: f32) -> Result<()>` | Render with custom DPI | ### Saving | Method | Signature | Description | |--------|-----------|-------------| | `save` | `fn save(&mut self, path: impl AsRef) -> Result<()>` | Save to file | | `save_as` | `fn save_as(&mut self, path: impl AsRef) -> Result<()>` | Save to a different file | | `save_encrypted` | `fn save_encrypted(&mut self, path: impl AsRef, user_password: &str, owner_password: &str) -> Result<()>` | Save with password protection | | `save_with_encryption` | `fn save_with_encryption(&mut self, path: impl AsRef, config: EncryptionConfig) -> Result<()>` | Save with full encryption config | | `as_bytes` | `fn as_bytes(&self) -> &[u8]` | PDF bytes (creation mode) | | `into_bytes` | `fn into_bytes(mut self) -> Vec` | Consume and return PDF bytes | | `to_bytes` | `fn to_bytes(&mut self) -> Result>` | Generate PDF bytes | | `to_markdown_file` | `fn to_markdown_file(&mut self, path: impl AsRef) -> Result<()>` | Save all pages as Markdown file | ### Accessors | Method | Signature | Description | |--------|-----------|-------------| | `source_path` | `fn source_path(&self) -> Option<&Path>` | Path of the opened file | | `editor` | `fn editor(&mut self) -> Option<&mut DocumentEditor>` | Access the underlying editor | | `config` | `fn config(&self) -> &PdfConfig` | Current configuration | | `is_modified` | `fn is_modified(&self) -> bool` | Whether document has unsaved changes | --- ## PdfBuilder Fluent builder for creating PDFs with metadata and layout configuration. ```rust use pdf_oxide::api::PdfBuilder; use pdf_oxide::writer::PageSize; ``` | Method | Signature | Description | |--------|-----------|-------------| | `new` | `fn new() -> Self` | Create a new builder | | `title` | `fn title(self, title: impl Into) -> Self` | Set title | | `author` | `fn author(self, author: impl Into) -> Self` | Set author | | `subject` | `fn subject(self, subject: impl Into) -> Self` | Set subject | | `keywords` | `fn keywords(self, keywords: impl Into) -> Self` | Set keywords | | `page_size` | `fn page_size(self, size: PageSize) -> Self` | Set page size | | `margin` | `fn margin(self, margin: f32) -> Self` | Set uniform margins | | `margins` | `fn margins(self, left: f32, right: f32, top: f32, bottom: f32) -> Self` | Set individual margins | | `font_size` | `fn font_size(self, size: f32) -> Self` | Set font size | | `line_height` | `fn line_height(self, height: f32) -> Self` | Set line height | | `from_markdown` | `fn from_markdown(self, content: &str) -> Result` | Build from Markdown | | `from_html` | `fn from_html(self, content: &str) -> Result` | Build from HTML | | `from_text` | `fn from_text(self, content: &str) -> Result` | Build from plain text | | `from_image` | `fn from_image(self, path: impl AsRef) -> Result` | Build from image | | `from_image_bytes` | `fn from_image_bytes(self, data: &[u8]) -> Result` | Build from image bytes | | `from_images` | `fn from_images>(self, paths: &[P]) -> Result` | Build from multiple images | | `from_qrcode` | `fn from_qrcode(self, data: &str) -> Result` | Build from QR code data | | `from_barcode` | `fn from_barcode(self, data: &str, barcode_type: BarcodeType) -> Result` | Build from barcode data | --- ## DocumentBuilder Low-level builder for pixel-precise page layout. ```rust use pdf_oxide::writer::DocumentBuilder; ``` | Method | Signature | Description | |--------|-----------|-------------| | `new` | `fn new() -> Self` | Create a new builder | | `metadata` | `fn metadata(self, metadata: DocumentMetadata) -> Self` | Set document metadata | | `page` | `fn page(&mut self, size: PageSize) -> FluentPageBuilder` | Add a page with named size | | `letter_page` | `fn letter_page(&mut self) -> FluentPageBuilder` | Add US Letter page | | `a4_page` | `fn a4_page(&mut self) -> FluentPageBuilder` | Add A4 page | | `build` | `fn build(self) -> Result>` | Generate PDF bytes | | `save` | `fn save(self, path: impl AsRef) -> Result<()>` | Save to file | ### FluentPageBuilder Returned by `DocumentBuilder::page()`. Chain calls to add content to a page: | Method | Description | |--------|-------------| | `text(text, x, y, size)` | Place text at exact coordinates | | `heading(level, text)` | Add a heading (H1-H6) | | `paragraph(text)` | Add a paragraph with auto-wrap | | `space(points)` | Add vertical space | | `horizontal_rule()` | Draw a horizontal line | | `link_url(url)` | Add a URL link annotation | | `link_page(page)` | Add internal page link | | `highlight(color)` | Add highlight annotation | | `underline(color)` | Add underline annotation | | `strikeout(color)` | Add strikeout annotation | | `sticky_note(text)` | Add a sticky note | | `stamp(stamp_type)` | Add a stamp annotation | | `freetext(rect, text)` | Add free text annotation | | `watermark(text)` | Add watermark overlay | | `add_annotation(annotation)` | Add any annotation type | | `done()` | Finish page, return to builder | --- ## DocumentEditor Open an existing PDF for structural modifications. ```rust use pdf_oxide::editor::DocumentEditor; ``` ### Core | Method | Signature | Description | |--------|-----------|-------------| | `open` | `fn open(path: impl AsRef) -> Result` | Open file for editing | | `is_modified` | `fn is_modified(&self) -> bool` | Check for unsaved changes | | `source_path` | `fn source_path(&self) -> &str` | Original file path | | `source` | `fn source(&self) -> &PdfDocument` | Underlying document (read) | | `source_mut` | `fn source_mut(&mut self) -> &mut PdfDocument` | Underlying document (write) | | `version` | `fn version(&self) -> (u8, u8)` | PDF version | | `current_page_count` | `fn current_page_count(&self) -> usize` | Page count | ### Metadata | Method | Description | |--------|-------------| | `title()` / `set_title()` | Get/set document title | | `author()` / `set_author()` | Get/set author | | `subject()` / `set_subject()` | Get/set subject | | `keywords()` / `set_keywords()` | Get/set keywords | ### Page Operations | Method | Description | |--------|-------------| | `get_page_rotation()` / `set_page_rotation()` | Get/set rotation | | `rotate_page_by()` | Add relative rotation | | `rotate_all_pages()` | Rotate all pages | | `get_page_media_box()` / `set_page_media_box()` | Get/set MediaBox | | `get_page_crop_box()` / `set_page_crop_box()` | Get/set CropBox | | `crop_margins()` | Crop all pages by margins | | `erase_region()` / `erase_regions()` | White-out content | | `extract_pages()` | Extract pages to separate file | | `merge_from()` / `merge_pages_from()` | Merge pages from another PDF | ### DOM-Like Editing | Method | Signature | Description | |--------|-----------|-------------| | `get_page` | `fn get_page(&mut self, page_index: usize) -> Result` | Get DOM page handle | | `save_page` | `fn save_page(&mut self, page: PdfPage) -> Result<()>` | Save modified page | | `edit_page` | `fn edit_page(&mut self, page_index: usize, f: F) -> Result<()>` | Edit with closure | | `page_editor` | `fn page_editor(&mut self, page_index: usize) -> Result` | Get page editor | | `get_page_content` | `fn get_page_content(&mut self, page_index: usize) -> Result>` | Get page structure | | `set_page_content` | `fn set_page_content(&mut self, page_index: usize, content: StructureElement) -> Result<()>` | Set page structure | | `modify_structure` | `fn modify_structure(&mut self, page_index: usize, f: F) -> Result<()>` | Modify structure with closure | ### Form Fields | Method | Description | |--------|-------------| | `get_form_fields()` | List all form fields | | `get_form_field_value(name)` | Get field value by name | | `has_form_field(name)` | Check field existence | | `add_form_field(widget)` | Add a new form field | | `flatten_forms_on_page(page)` | Flatten forms on a page | | `flatten_forms()` | Flatten all form fields | | `export_form_data_fdf(path)` | Export as FDF | | `export_form_data_xfdf(path)` | Export as XFDF | | `has_xfa()` | Check for XFA forms | | `analyze_xfa()` | Analyze XFA form data | | `convert_xfa_to_acroform()` | Convert XFA to AcroForm | ### Annotations and Flattening | Method | Description | |--------|-------------| | `flatten_page_annotations(page)` | Flatten annotations on a page | | `flatten_all_annotations()` | Flatten all annotations | | `get_page_annotations(page)` | List annotations on a page | ### Embedded Files | Method | Description | |--------|-------------| | `embed_file(name, data)` | Attach a file | | `embed_file_with_options(file)` | Attach with full config | | `pending_embedded_files()` | List pending attachments | | `clear_embedded_files()` | Clear pending attachments | --- ## DOM Types ### PdfPage Represents a single page with queryable and editable elements. | Method | Description | |--------|-------------| | `elements()` | All elements on the page | | `text_elements()` | Only text elements | | `image_elements()` | Only image elements | | `path_elements()` | Only path/graphics elements | | `table_elements()` | Only table elements | | `find_text_containing(needle)` | Find text matching substring | | `set_text(id, new_text)` | Replace text by element ID | ### PdfText | Method | Return | Description | |--------|--------|-------------| | `id()` | `ElementId` | Unique element identifier | | `text()` | `&str` | Text content | | `bbox()` | `Rect` | Bounding rectangle | | `font_name()` | `&str` | Font name | | `font_size()` | `f32` | Font size in points | | `is_bold()` | `bool` | Bold weight | | `is_italic()` | `bool` | Italic style | | `color()` | `Color` | Text color | | `set_text(new)` | | Replace text | | `append(text)` | | Append text | | `replace(old, new)` | `usize` | Replace occurrences | | `clear()` | | Clear text | ### PdfImage | Method | Return | Description | |--------|--------|-------------| | `id()` | `ElementId` | Unique identifier | | `bbox()` | `Rect` | Bounding rectangle | | `format()` | `ImageFormat` | Image format | | `dimensions()` | `(u32, u32)` | Width and height in pixels | | `aspect_ratio()` | `f32` | Width / height ratio | | `is_grayscale()` | `bool` | Grayscale check | | `alt_text()` | `Option<&str>` | Alt text for accessibility | | `resolution()` | `Option<(f32, f32)>` | DPI as (horizontal, vertical) | ### PdfPath | Method | Return | Description | |--------|--------|-------------| | `id()` | `ElementId` | Unique identifier | | `bbox()` | `Rect` | Bounding rectangle | | `operations()` | `&[PathOperation]` | Path drawing operations | | `stroke_color()` | `Option` | Stroke color | | `fill_color()` | `Option` | Fill color | | `stroke_width()` | `f32` | Line width | | `line_cap()` | `LineCap` | Line cap style | | `line_join()` | `LineJoin` | Line join style | | `is_closed()` | `bool` | Whether path is closed | | `to_svg()` | `String` | Convert to SVG path data | | `to_svg_document()` | `String` | Convert to standalone SVG | ### PdfTable | Method | Return | Description | |--------|--------|-------------| | `id()` | `ElementId` | Unique identifier | | `bbox()` | `Rect` | Bounding rectangle | | `row_count()` | `usize` | Number of rows | | `column_count()` | `usize` | Number of columns | | `has_header()` | `bool` | Whether first row is a header | | `get_cell(row, col)` | `Option<&TableCellContent>` | Cell contents | | `caption()` | `Option<&str>` | Table caption | --- ## TextSearcher ```rust use pdf_oxide::search::{TextSearcher, SearchOptions, SearchResult}; ``` ### SearchOptions | Field | Type | Default | Description | |-------|------|---------|-------------| | `case_sensitive` | `bool` | `true` | Case-sensitive matching | | `literal` | `bool` | `false` | Treat pattern as literal (not regex) | | `whole_word` | `bool` | `false` | Match whole words only | | `max_results` | `Option` | `None` | Limit number of results | | `page_range` | `Option<(usize, usize)>` | `None` | Restrict to page range | ### SearchResult | Field | Type | Description | |-------|------|-------------| | `page` | `usize` | Page index | | `text` | `String` | Matched text | | `x` | `f64` | X position in points | | `y` | `f64` | Y position in points | --- ## FormField and XmpExtractor ### FormField (read) | Field | Type | Description | |-------|------|-------------| | `name` | `String` | Fully qualified field name | | `field_type` | `FieldType` | Text, Button, Choice, Signature | | `value` | `Option` | Current value | | `rect` | `Option` | Widget bounds | | `flags` | `u32` | Field flags | ### XmpExtractor ```rust use pdf_oxide::extractors::xmp::XmpExtractor; ``` Static methods that operate on a `PdfDocument`: | Method | Return | Description | |--------|--------|-------------| | `extract(doc)` | `Result>` | Extract XMP metadata | ### XmpMetadata | Field | Type | Description | |-------|------|-------------| | `title` | `Option` | dc:title | | `creators` | `Vec` | dc:creator | | `description` | `Option` | dc:description | | `creator_tool` | `Option` | xmp:CreatorTool | | `create_date` | `Option` | xmp:CreateDate | | `modify_date` | `Option` | xmp:ModifyDate | | `producer` | `Option` | pdf:Producer | ### PageLabelExtractor ```rust use pdf_oxide::extractors::page_labels::PageLabelExtractor; ``` | Method | Return | Description | |--------|--------|-------------| | `extract(doc)` | `Result>` | Extract page label definitions | | `label_for_page(doc, page)` | `Result` | Compute label for a specific page | | `all_labels(doc)` | `Result>` | Compute labels for every page | --- ## Standalone Functions ```rust use pdf_oxide::document::{parse_header, parse_trailer}; ``` | Function | Signature | Description | |----------|-----------|-------------| | `parse_header` | `fn parse_header(reader: &mut R, lenient: bool) -> Result<(u8, u8, u64)>` | Parse PDF header, returns (major, minor, byte_offset) | | `parse_trailer` | `fn parse_trailer(reader: &mut R) -> Result` | Parse the trailer dictionary | ## Next Steps - [Python API Reference](/docs/reference/python-api) -- Python bindings reference - [Types & Enums](/docs/reference/types) -- all types, enums, and configuration structs - [Getting Started with Rust](/docs/getting-started/rust) -- tutorial with examples --- # Python API Reference PDF Oxide provides native Python bindings built with PyO3. Pre-built wheels are available for Python 3.8--3.14 on Linux, macOS, and Windows (x86_64 and ARM64). ```bash pip install pdf_oxide ``` For the Rust API, see the [Rust API Reference](/docs/reference/api). For type details, see [Types & Enums](/docs/reference/types). --- ## PdfDocument The primary class for opening and extracting content from PDF files. ```python from pdf_oxide import PdfDocument ``` ### Constructor ```python PdfDocument(path: str, password: str | None = None) ``` | Parameter | Type | Description | |-----------|------|-------------| | `path` | `str` | Path to the PDF file | | `password` | `str`, optional | User or owner password for encrypted PDFs | Raises `FileNotFoundError` if the file does not exist. Raises `PdfError` if the file is not a valid PDF. ### Properties | Property | Type | Description | |----------|------|-------------| | `page_count` | `int` | Number of pages in the document | | `version` | `str` | PDF version string (e.g., `"1.7"`) | ### Methods #### Text Extraction ```python doc.extract_text(page_index: int) -> str ``` Extract plain text from a single page. Pages are zero-indexed. ```python doc.extract_chars(page_index: int) -> list[TextChar] ``` Extract per-character positioning and font metadata. Returns a list of `TextChar` objects. ```python doc.extract_spans(page_index: int) -> list[TextSpan] ``` Extract text spans with font metadata. Each span is a run of identically-styled text. #### Conversion ```python doc.to_markdown(page_index: int, detect_headings: bool = False) -> str ``` Convert a page to Markdown. Set `detect_headings=True` to infer heading levels from font sizes. ```python doc.to_html(page_index: int) -> str ``` Convert a page to HTML. #### Image Extraction ```python doc.extract_images(page_index: int) -> list[ImageInfo] ``` Extract all images from a page, including images in content streams and nested Form XObjects. #### Search ```python doc.search(pattern: str) -> list[SearchResult] ``` Search for text across all pages. Returns a list of matches with page number and coordinates. ```python doc.search_page(page_index: int, pattern: str) -> list[SearchResult] ``` Search for text on a single page. #### Authentication ```python doc.authenticate(password: str) -> bool ``` Authenticate with a password after opening. Returns `True` if authentication succeeded. --- ## Pdf The unified class for creating PDFs from various source formats. ```python from pdf_oxide import Pdf ``` ### Factory Methods ```python Pdf.from_markdown(content: str) -> Pdf ``` Create a PDF from Markdown content. ```python Pdf.from_html(content: str) -> Pdf ``` Create a PDF from HTML content. ```python Pdf.from_text(content: str) -> Pdf ``` Create a PDF from plain text. ```python Pdf.from_image(path: str) -> Pdf ``` Create a single-page PDF from an image file (JPEG, PNG). ### Opening ```python Pdf.open(path: str) -> Pdf ``` Open an existing PDF for reading, searching, and editing. ### Properties | Property | Type | Description | |----------|------|-------------| | `page_count` | `int` | Number of pages | ### Methods #### Saving ```python pdf.save(path: str) -> None ``` Save the PDF to a file. For created PDFs, this writes the generated content. For opened PDFs, this writes any modifications. ```python pdf.save_encrypted( path: str, user_password: str, owner_password: str, allow_printing: bool = True, allow_copying: bool = True, allow_modifying: bool = True, allow_annotating: bool = True ) -> None ``` Save with password protection and permission controls. #### Search ```python pdf.search(pattern: str) -> list[SearchResult] ``` Search for text across all pages. --- ## TextChar Represents a single character with positioning and font metadata. Returned by `PdfDocument.extract_chars()`. ```python from pdf_oxide import TextChar # or access via PdfDocument ``` | Attribute | Type | Description | |-----------|------|-------------| | `char` | `str` | The Unicode character | | `x` | `float` | Horizontal position in PDF points | | `y` | `float` | Vertical position in PDF points | | `font_size` | `float` | Font size in points | | `font_name` | `str` | PostScript font name | | `bbox` | `tuple[float, float, float, float]` | Bounding box `(x0, y0, x1, y1)` | ### Example ```python from pdf_oxide import PdfDocument doc = PdfDocument("paper.pdf") chars = doc.extract_chars(0) for ch in chars[:5]: print(f"'{ch.char}' at ({ch.x:.1f}, {ch.y:.1f}) " f"font={ch.font_name} size={ch.font_size:.1f} " f"bbox={ch.bbox}") ``` --- ## TextSpan Represents a run of text sharing the same font and size. Returned by `PdfDocument.extract_spans()`. | Attribute | Type | Description | |-----------|------|-------------| | `text` | `str` | The text content | | `x` | `float` | Horizontal position in PDF points | | `y` | `float` | Vertical position in PDF points | | `font_name` | `str` | PostScript font name | | `font_size` | `float` | Font size in points | ### Example ```python from pdf_oxide import PdfDocument doc = PdfDocument("paper.pdf") spans = doc.extract_spans(0) for span in spans: print(f"'{span.text}' font={span.font_name} size={span.font_size:.1f}") ``` --- ## ImageInfo Represents an extracted image with metadata and raw pixel data. Returned by `PdfDocument.extract_images()`. | Attribute | Type | Description | |-----------|------|-------------| | `width` | `int` | Image width in pixels | | `height` | `int` | Image height in pixels | | `color_space` | `str` | Color space (e.g., `"DeviceRGB"`, `"DeviceGray"`) | | `bits_per_component` | `int` | Bits per color channel | | `data` | `bytes` | Raw image data | ### Methods ```python img.save(path: str) -> None ``` Save the image to a file. The format is inferred from the file extension. ### Example ```python from pdf_oxide import PdfDocument doc = PdfDocument("brochure.pdf") images = doc.extract_images(0) for i, img in enumerate(images): print(f"Image {i}: {img.width}x{img.height} " f"{img.color_space} {img.bits_per_component}bpc") img.save(f"image_{i}.png") ``` --- ## SearchResult Represents a text search match. Returned by `search()` and `search_page()`. | Attribute | Type | Description | |-----------|------|-------------| | `page` | `int` | Zero-based page index | | `text` | `str` | Matched text | | `x` | `float` | X position in PDF points | | `y` | `float` | Y position in PDF points | --- ## OfficeConverter Convert Office documents (DOCX, XLSX, PPTX) to PDF. Requires the `office` feature in the Rust build. ```python from pdf_oxide import OfficeConverter ``` ### Methods ```python OfficeConverter.from_docx(path: str) -> bytes ``` Convert a Word document to PDF bytes. ```python OfficeConverter.from_xlsx(path: str) -> bytes ``` Convert an Excel spreadsheet to PDF bytes. ```python OfficeConverter.from_pptx(path: str) -> bytes ``` Convert a PowerPoint presentation to PDF bytes. ### Example ```python from pdf_oxide import OfficeConverter pdf_bytes = OfficeConverter.from_docx("report.docx") with open("report.pdf", "wb") as f: f.write(pdf_bytes) ``` --- ## Graphics Classes These classes are available for advanced PDF creation with graphics: ### Color ```python from pdf_oxide import Color Color(r: float, g: float, b: float) # RGB, values 0.0-1.0 Color.rgb(r, g, b) Color.gray(level) Color.cmyk(c, m, y, k) ``` ### BlendMode ```python from pdf_oxide import BlendMode BlendMode.normal() BlendMode.multiply() BlendMode.screen() BlendMode.overlay() BlendMode.darken() BlendMode.lighten() ``` ### ExtGState ```python from pdf_oxide import ExtGState gs = ExtGState() gs.stroke_opacity = 0.5 gs.fill_opacity = 0.8 gs.blend_mode = BlendMode.multiply() ``` ### LineCap / LineJoin ```python from pdf_oxide import LineCap, LineJoin LineCap.butt() # Default LineCap.round() LineCap.square() LineJoin.miter() # Default LineJoin.round() LineJoin.bevel() ``` ### Gradients ```python from pdf_oxide import LinearGradient, RadialGradient grad = LinearGradient( x0=0, y0=0, x1=100, y1=0, color0=Color(1, 0, 0), color1=Color(0, 0, 1) ) rgrad = RadialGradient( cx=50, cy=50, radius=50, color0=Color(1, 1, 0), color1=Color(1, 0, 0) ) ``` --- ## Error Handling ### PdfError All PDF-specific errors raise `PdfError`: ```python from pdf_oxide import PdfDocument, PdfError try: doc = PdfDocument("file.pdf") text = doc.extract_text(0) except PdfError as e: print(f"PDF error: {e}") except FileNotFoundError: print("File not found") except IndexError: print("Page index out of range") ``` Common error scenarios: | Exception | Cause | |-----------|-------| | `PdfError` | Malformed PDF, encrypted without password, parse failure | | `FileNotFoundError` | File does not exist | | `IndexError` | Page index exceeds `page_count` | | `ValueError` | Invalid argument (e.g., negative page index) | --- ## Complete Example ```python from pdf_oxide import PdfDocument, Pdf # --- Extraction --- doc = PdfDocument("input.pdf") print(f"Pages: {doc.page_count}") for i in range(doc.page_count): text = doc.extract_text(i) print(f"Page {i + 1}: {len(text)} characters") # Character-level analysis chars = doc.extract_chars(0) fonts = set(ch.font_name for ch in chars) print(f"Fonts on page 1: {fonts}") # Image extraction images = doc.extract_images(0) for i, img in enumerate(images): img.save(f"extracted_{i}.png") # --- Creation --- pdf = Pdf.from_markdown("# Report\n\nGenerated by PDF Oxide.") pdf.save("report.pdf") # --- Search --- pdf = Pdf.open("manual.pdf") results = pdf.search("configuration") for r in results: print(f"Page {r.page + 1}: '{r.text}' at ({r.x:.0f}, {r.y:.0f})") ``` ## Next Steps - [Rust API Reference](/docs/reference/api) -- complete Rust API - [Types & Enums](/docs/reference/types) -- all types and enums - [Getting Started with Python](/docs/getting-started/python) -- tutorial --- # Types & Enums All public types in `pdf_oxide`, organized by category. For method-level documentation, see the [Rust API Reference](/docs/reference/api). --- ## Geometry ### Point ```rust pub struct Point { pub x: f64, pub y: f64, } ``` A 2D point in PDF coordinate space (origin at bottom-left, Y increases upward). ### Rect ```rust pub struct Rect { pub x0: f64, pub y0: f64, pub x1: f64, pub y1: f64, } ``` An axis-aligned rectangle. `(x0, y0)` is the lower-left corner, `(x1, y1)` is the upper-right corner. All coordinates are in PDF points (1 point = 1/72 inch). **Python:** Accessible as a tuple `(x0, y0, x1, y1)`. ### Matrix ```rust pub struct Matrix { pub a: f64, pub b: f64, pub c: f64, pub d: f64, pub e: f64, pub f: f64, } ``` A 3x3 affine transformation matrix in the PDF spec format: ``` | a b 0 | | c d 0 | | e f 1 | ``` Used for text positioning, image placement, and Form XObject transforms. --- ## Text ### TextSpan ```rust pub struct TextSpan { pub text: String, pub x: f64, pub y: f64, pub font_name: String, pub font_size: f64, pub bbox: Rect, } ``` A run of identically-styled text. Returned by `extract_spans()`. ### TextChar ```rust pub struct TextChar { pub char: char, pub x: f64, pub y: f64, pub font_size: f64, pub font_name: String, pub bbox: Rect, } ``` A single character with precise positioning. Returned by `extract_chars()`. **Python Fields:** | Field | Type | Description | |-------|------|-------------| | `char` | `str` | The character | | `bbox` | `tuple[float, float, float, float]` | Bounding box (x0, y0, x1, y1) | | `font_name` | `str` | Font name | | `font_size` | `float` | Font size in points | | `origin_x` | `float` | Baseline origin X | | `origin_y` | `float` | Baseline origin Y | | `rotation_degrees` | `float` | Rotation angle (0--360) | | `advance_width` | `float` | Distance to next character position | ### FontWeight ```rust pub enum FontWeight { Thin, // 100 ExtraLight, // 200 Light, // 300 Normal, // 400 Medium, // 500 SemiBold, // 600 Bold, // 700 ExtraBold, // 800 Black, // 900 } ``` ### Color ```rust pub struct Color { pub r: f64, pub g: f64, pub b: f64, } ``` RGB color with components in the range 0.0 to 1.0. --- ## Page ### PageSize ```rust pub enum PageSize { Letter, // 612 x 792 pt (8.5 x 11 in) A4, // 595.28 x 841.89 pt (210 x 297 mm) Legal, // 612 x 1008 pt (8.5 x 14 in) A3, // 841.89 x 1190.55 pt A5, // 419.53 x 595.28 pt Custom(f32, f32), // Custom width x height in points } ``` Methods: | Method | Return | Description | |--------|--------|-------------| | `dimensions()` | `(f32, f32)` | Width and height in points | ### PageInfo ```rust pub struct PageInfo { pub width: f64, pub height: f64, pub rotation: i32, pub media_box: Rect, pub crop_box: Option, pub trim_box: Option, pub bleed_box: Option, pub art_box: Option, } ``` ### PageLabelStyle ```rust pub enum PageLabelStyle { Decimal, // 1, 2, 3, ... UpperRoman, // I, II, III, ... LowerRoman, // i, ii, iii, ... UpperAlpha, // A, B, C, ... LowerAlpha, // a, b, c, ... None, // No numbering } ``` ### PageLabelRange ```rust pub struct PageLabelRange { pub start_page: usize, pub style: PageLabelStyle, pub prefix: Option, pub start_number: Option, } ``` Defines a page label range. For example, a range starting at page 0 with `LowerRoman` style and start number 1 labels pages as i, ii, iii, and so on. --- ## Images ### ImageFormat ```rust pub enum ImageFormat { Jpeg, Png, Tiff, Bmp, Gif, Jp2, Jbig2, Ccitt, Raw, Unknown, } ``` ### ColorSpace ```rust pub enum ColorSpace { DeviceRGB, DeviceCMYK, DeviceGray, ICCBased, CalRGB, CalGray, Lab, Indexed, Pattern, Separation, DeviceN, Unknown, } ``` ### ImageInfo ```rust pub struct ImageContent { pub width: u32, pub height: u32, pub bits_per_component: u8, pub color_space: ColorSpace, pub format: ImageFormat, pub data: Vec, pub bbox: Rect, pub horizontal_dpi: Option, pub vertical_dpi: Option, } ``` Methods: | Method | Return | Description | |--------|--------|-------------| | `resolution()` | `Option<(f32, f32)>` | DPI as (horizontal, vertical) | | `is_high_resolution()` | `bool` | DPI >= 300 | | `is_medium_resolution()` | `bool` | DPI 150--299 | | `is_low_resolution()` | `bool` | DPI < 150 | | `calculate_dpi()` | `Option<(f32, f32)>` | Compute from pixel dimensions and bbox | --- ## Paths ### PathContent ```rust pub struct PathContent { pub operations: Vec, pub stroke_color: Option, pub fill_color: Option, pub stroke_width: f32, pub line_cap: LineCap, pub line_join: LineJoin, pub bbox: Rect, } ``` ### PathOperation ```rust pub enum PathOperation { MoveTo { x: f64, y: f64 }, LineTo { x: f64, y: f64 }, CurveTo { x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64 }, ClosePath, Rect { x: f64, y: f64, width: f64, height: f64 }, } ``` ### LineCap ```rust pub enum LineCap { Butt, // Square end at endpoint (default) Round, // Semicircle at endpoint Square, // Square extends half line width past endpoint } ``` ### LineJoin ```rust pub enum LineJoin { Miter, // Sharp corner (default) Round, // Rounded corner Bevel, // Flat corner } ``` ### BlendMode ```rust pub enum BlendMode { Normal, Multiply, Screen, Overlay, Darken, Lighten, ColorDodge, ColorBurn, HardLight, SoftLight, Difference, Exclusion, } ``` Used in graphics state for transparency compositing. Set via `ExtGState` or the graphics API. --- ## Content Elements ### ContentElement The union type for all page content elements: ```rust pub enum ContentElement { Text(TextContent), Image(ImageContent), Path(PathContent), Table(TableContent), Structure(StructureElement), } ``` ### TextContent ```rust pub struct TextContent { pub text: String, pub font_name: String, pub font_size: f64, pub font_weight: FontWeight, pub font_style: FontStyle, pub color: Color, pub bbox: Rect, } ``` ### FontStyle ```rust pub enum FontStyle { Normal, Italic, Oblique, } ``` ### TextStyle ```rust pub struct TextStyle { pub font_name: String, pub font_size: f64, pub font_weight: FontWeight, pub font_style: FontStyle, pub color: Color, } ``` ### TableContent ```rust pub struct TableContent { pub rows: Vec, pub column_count: usize, pub has_header: bool, pub caption: Option, pub style: Option, pub source: TableSource, pub bbox: Rect, } ``` ### TableRowContent ```rust pub struct TableRowContent { pub cells: Vec, pub is_header: bool, } ``` ### TableCellContent ```rust pub struct TableCellContent { pub text: String, pub row_span: usize, pub col_span: usize, pub alignment: TableCellAlign, pub vertical_alignment: TableCellVAlign, pub bbox: Rect, } ``` ### TableCellAlign / TableCellVAlign ```rust pub enum TableCellAlign { Left, Center, Right, } pub enum TableCellVAlign { Top, Middle, Bottom, } ``` ### TableSource ```rust pub enum TableSource { StructureTree, // From tagged PDF structure Geometric, // Detected from layout analysis Manual, // User-created } ``` ### StructureElement ```rust pub struct StructureElement { pub structure_type: String, pub children: Vec, pub alt_text: Option, pub actual_text: Option, pub language: Option, } ``` --- ## Annotations ### AnnotationType ```rust pub enum AnnotationSubtype { Text, Link, FreeText, Line, Square, Circle, Polygon, PolyLine, Highlight, Underline, Squiggly, StrikeOut, Stamp, Caret, Ink, Popup, FileAttachment, Sound, Movie, Screen, Widget, PrinterMark, TrapNet, Watermark, ThreeD, Redact, RichMedia, Unknown, } ``` ### TextAnnotationIcon ```rust pub enum TextAnnotationIcon { Comment, Key, Note, Help, NewParagraph, Paragraph, Insert, Check, Circle, Cross, RightArrow, RightPointer, Star, UpArrow, UpLeftArrow, } ``` ### StampType ```rust pub enum StampType { Approved, Experimental, NotApproved, AsIs, Expired, NotForPublicRelease, Confidential, Final, Sold, Departmental, ForComment, TopSecret, Draft, ForPublicRelease, } ``` --- ## Barcodes ### BarcodeType Requires the `barcodes` feature flag. ```rust pub enum BarcodeType { QrCode, Code128, Ean13, UpcA, Code39, Itf, } ``` **Python:** Barcode types are passed as strings: `"code128"`, `"ean13"`, `"upca"`, `"code39"`, `"ean8"`, `"itf"`. --- ## Forms ### FieldType / FormFieldType ```rust pub enum FormFieldType { Text, Button, Choice, Signature, } ``` ### FormField ```rust pub struct FormField { pub name: String, pub field_type: FormFieldType, pub value: FormFieldValue, pub rect: Option, pub page_index: Option, pub readonly: bool, pub required: bool, } ``` ### FormFieldValue ```rust pub enum FormFieldValue { None, Text(String), Boolean(bool), Choice(String), MultiChoice(Vec), } ``` Methods: | Method | Return | Description | |--------|--------|-------------| | `is_none()` | `bool` | Check if value is None | | `as_text()` | `Option<&str>` | Get text value | | `as_bool()` | `Option` | Get boolean value | | `as_choice()` | `Option<&str>` | Get choice value | | `as_multi_choice()` | `Option<&[String]>` | Get multi-choice values | --- ## Search ### SearchOptions ```rust pub struct SearchOptions { pub case_sensitive: bool, pub literal: bool, pub whole_word: bool, pub max_results: Option, pub page_range: Option<(usize, usize)>, } ``` ### SearchResult ```rust pub struct SearchResult { pub page_index: usize, pub text: String, pub bbox: Rect, pub context: Option, } ``` Returned by `Pdf::search()` and `PdfDocument::search()`. Each result includes the page where the match was found, the matched text, its bounding box coordinates, and optional surrounding context. --- ## Configuration ### ConversionOptions ```rust pub struct ConversionOptions { pub preserve_layout: bool, pub detect_headings: bool, pub extract_tables: bool, pub include_images: bool, pub image_output_dir: Option, pub embed_images: bool, // ... additional fields } ``` ### TextConfig ```rust pub struct TextConfig { pub detect_headings: bool, pub detect_lists: bool, pub detect_tables: bool, pub merge_spans: bool, } ``` ### RenderOptions Requires the `rendering` feature flag. ```rust pub struct RenderOptions { pub dpi: f32, pub background_color: Option, pub format: ImageFormat, } ``` ### EncryptionConfig ```rust pub struct EncryptionConfig { pub user_password: String, pub owner_password: String, pub algorithm: EncryptionAlgorithm, pub permissions: Permissions, } ``` Constructor: ```rust EncryptionConfig::new("user_pass", "owner_pass") .with_algorithm(EncryptionAlgorithm::Aes256) .with_permissions(Permissions::read_only()) ``` ### EncryptionAlgorithm ```rust pub enum EncryptionAlgorithm { Rc4_40, // V=1, R=2, 40-bit (legacy) Rc4_128, // V=2, R=3, 128-bit (legacy) Aes128, // V=4, R=4, 128-bit Aes256, // V=5, R=6, 256-bit (default, recommended) } ``` ### Permissions ```rust pub struct Permissions { pub print: bool, pub copy: bool, pub modify: bool, pub annotate: bool, pub fill_forms: bool, pub extract: bool, } ``` Factory methods: | Method | Description | |--------|-------------| | `Permissions::all()` | All permissions enabled | | `Permissions::read_only()` | Only viewing allowed | ### PdfConfig ```rust pub struct PdfConfig { pub page_size: PageSize, pub margins: (f32, f32, f32, f32), // left, right, top, bottom pub font_size: f32, pub line_height: f32, pub title: Option, pub author: Option, pub subject: Option, pub keywords: Option, } ``` --- ## Text Layout ### TextAlign ```rust pub enum TextAlign { Left, Center, Right, Justify, } ``` ### RectFilterMode For spatial text extraction: ```rust pub enum RectFilterMode { Intersects, // Any overlap (default) FullyContained, // Completely within bounds MinOverlap(f32), // Minimum overlap fraction (0.0-1.0) } ``` --- ## Compliance Types See the [PDF/A](/docs/compliance/pdf-a), [PDF/UA](/docs/compliance/pdf-ua), and [PDF/X](/docs/compliance/pdf-x) pages for full compliance documentation. ### PdfALevel ```rust pub enum PdfALevel { A1a, A1b, A2a, A2b, A2u, A3a, A3b, A3u, } ``` ### PdfUaLevel ```rust pub enum PdfUaLevel { UA1, UA2, } ``` ### PdfXLevel ```rust pub enum PdfXLevel { X1a2001, X1a2003, X3_2002, X3_2003, X4, X4p, X5g, X5n, X5pg, X6, X6n, X6p, } ``` --- ## Error Types ### PdfError ```rust pub enum PdfError { Io(std::io::Error), Parse(String), Password, PageOutOfRange { index: usize, count: usize }, } ``` | Variant | Description | |---------|-------------| | `Io` | File system or I/O failure | | `Parse` | Malformed PDF structure | | `Password` | Document is encrypted and no password was given | | `PageOutOfRange` | Requested page index exceeds page count | ## Next Steps - [Rust API Reference](/docs/reference/api) -- method-level documentation - [Python API Reference](/docs/reference/python-api) -- Python bindings - [Getting Started with Rust](/docs/getting-started/rust) -- tutorial --- # Performance v0.3.6 eliminated two critical O(n) bottlenecks, reducing mean text extraction time by 91% across a 3,830-PDF corpus. ## Benchmark Results ### Corpus: 3,830 PDFs Three independent public test suites combined: | Suite | PDFs | Source | |-------|------|--------| | veraPDF | 2,907 | PDF/A conformance test corpus | | Mozilla pdf.js | 897 | Browser PDF rendering test suite | | SafeDocs | 26 | DARPA SafeDocs malformed PDF corpus | ### v0.3.5 vs v0.3.6 | Metric | v0.3.5 | v0.3.6 | Change | |--------|--------|--------|--------| | **Pass rate** | 99.8% | 99.8% | 3,823 of 3,830 valid PDFs | | **Slow (>5s)** | 2 | **0** | Eliminated | | **Mean** | 23.3ms | **2.1ms** | **-91%** | | **p50** | 0.6ms | 0.6ms | -- | | **p90** | 3.0ms | **2.6ms** | -13% | | **p95** | 5.1ms | **4.7ms** | -8% | | **p99** | 33.2ms | **18.0ms** | **-46%** | | **Max** | 68,722ms | **625ms** | **-99%** | | **Sum (all PDFs)** | 89.1s | **8.0s** | **-91%** | The 7 non-passing files are intentionally broken test fixtures (missing PDF header, fuzz-corrupted catalogs, invalid xref streams). Text output was verified byte-identical on 11 PDFs (862 KB of extracted text). 4 PDFs showed improved extraction quality from adaptive spacing (more complete words recovered). ### Headline Example **isartor-6-1-12-t01-fail-a.pdf** (10,000-page veraPDF test file): | | v0.3.5 | v0.3.6 | |-|--------|--------| | **Time** | 55,667ms | **332ms** | | **Speedup** | -- | **168x faster** | This was the slowest PDF in the entire corpus and the primary contributor to the mean time skew. ## What Changed in v0.3.6 ### 1. Bulk Page Tree Cache **Before**: `get_page()` traversed the page tree from root for every uncached page. For sequential extraction of all pages, this was O(n) per page and O(n^2) total. **After**: On first page access, the entire page tree is walked once and all pages are cached in a `HashMap`. Every subsequent access is O(1). This is the fix that brought the 10,000-page PDF from 55 seconds to 332 milliseconds. ### 2. Scan-for-Object Offset Cache **Before**: When objects were missing from the xref table, `scan_for_object()` read the entire PDF file for each missing object. Tagged PDFs with hundreds of structure tree elements not in xref triggered hundreds of full file reads. **After**: The file is scanned once and all object offsets are cached in a `HashMap`. Subsequent lookups are O(1). ### 3. Single-Pass Text Extraction **Before**: `extract_spans()` ran two passes over the page content -- first to classify the document type (academic, newspaper, form, etc.), then to extract text. The classification pass had been used to select different spacing thresholds. **After**: The classification pass was eliminated entirely. Adaptive font-aware thresholds now produce equal or better results in a single pass. ### 4. Content Stream Pre-Allocation **Before**: `parse_content_stream()` built the operator `Vec` starting from default capacity, causing repeated reallocations on large content streams. **After**: The Vec is pre-allocated based on stream size (`data.len() / 20`), which estimates roughly one operator per 20 bytes. This reduces allocations for content-heavy pages. ## Python Library Comparison Mean text extraction time per PDF, measured on the same 100-PDF subset (mixed academic papers, reports, and forms): | Library | Mean Time | Relative | License | |---------|-----------|----------|---------| | **PDF Oxide** | **0.8ms** | **1×** | MIT | | PyMuPDF | 4.6ms | 5.8x | AGPL-3.0 | | pypdf | 12.1ms | 15.1x | BSD-3 | | pdfplumber | 23.2ms | 29x | MIT | | pdfminer | 16.8ms | 21x | MIT | PDF Oxide is the fastest Python PDF library available. Unlike PyMuPDF, it uses the MIT license -- no AGPL restrictions for commercial use. For a detailed feature-by-feature comparison, see [Python Library Comparison](/docs/comparison/python). ## Methodology All 3,830 PDFs were processed sequentially on a single thread. For each PDF: 1. Open the file with `PdfDocument::open()` 2. Call `extract_text()` on every page 3. Record wall-clock time from open to final extraction No warm-up runs. No parallelism. No caching between files. This measures worst-case sequential throughput. The benchmark binary (`verify_corpus`) outputs CSV with per-file timing, timeout handling (120s per file), and per-corpus breakdown. ## Reproducing the Benchmarks The public test corpora are freely available: - **veraPDF**: [github.com/veraPDF/veraPDF-corpus](https://github.com/veraPDF/veraPDF-corpus) - **Mozilla pdf.js**: [github.com/mozilla/pdf.js/tree/master/test/pdfs](https://github.com/mozilla/pdf.js/tree/master/test/pdfs) - **SafeDocs**: [github.com/pdf-association/safedocs](https://github.com/pdf-association/safedocs) Run the verification: ```bash cargo run --release --example verify_corpus -- \ --corpus-dir /path/to/veraPDF-corpus \ --corpus-dir /path/to/pdfjs-test \ --corpus-dir /path/to/safedocs \ --output results.csv ``` ## Performance Characteristics ### What PDF Oxide Is Fast At - **Text extraction**: The primary optimization target. Sub-millisecond for typical documents. - **Sequential multi-page extraction**: The page tree cache makes extracting all pages from a large document nearly as fast as extracting one. - **Tagged PDFs**: Structure tree traversal and object resolution are now cached. - **Malformed PDFs**: Lenient parsing with fallback strategies avoids expensive retries. ### What Scales Linearly - **Page count**: Each page is processed independently. 100 pages takes roughly 100x one page. - **Content stream size**: Parsing operators is linear in stream length. - **Image extraction**: Proportional to the number and size of images. ### When to Expect Slower Results - **Scanned PDFs with OCR**: OCR processing (if enabled) is significantly slower than text extraction. - **Rendering**: Page rendering to images depends on content complexity and target DPI. - **Heavily encrypted PDFs**: AES-256 decryption adds overhead per stream. - **PDFs with thousands of fonts**: Font parsing is cached per document, but initial parsing scales with font count. ## Next Steps - [Changelog](/docs/changelog) -- full version history - [Python Library Comparison](/docs/comparison/python) -- detailed comparison with PyMuPDF, pypdf, pdfplumber, pdfminer - [Getting Started with Rust](/docs/getting-started/rust) -- installation and first extraction - [Rust API Reference](/docs/reference/api) -- complete API documentation --- # Changelog All notable changes to PDF Oxide are documented here. --- ## v0.3.6 -- 2026-02-16 > 10x faster -- two O(n) bottlenecks eliminated ### Performance - **Bulk page tree cache** -- On first page access, the entire page tree is walked once and all pages are cached. Previously `get_page()` traversed from root for every uncached page, resulting in O(n) per page and O(n^2) total for sequential access. Now O(1) per page after a single O(n) walk. A 10,000-page veraPDF test file went from 55,667ms to 332ms (168x faster). - **Scan-for-object offset cache** -- When objects are missing from the xref table, `scan_for_object()` previously read the entire PDF file for each missing object. Tagged PDFs with hundreds of structure tree elements not in xref triggered hundreds of full file reads. Now the file is scanned once and all object offsets are cached. A 10-page tagged PDF went from ~10s to 68ms (146x faster). A 154-page academic PDF with 571 fonts went from ~18s to 405ms (44x faster). - **Single-pass text extraction** -- `extract_spans()` no longer runs two passes (classify document type, then extract). The classification pass was eliminated entirely; adaptive font-aware thresholds now produce equal or better results in a single pass. - **Content stream Vec pre-allocation** -- `parse_content_stream()` pre-allocates operator Vec capacity based on stream size, reducing reallocations for large content streams. ### Verified -- 3,830-PDF Corpus (v0.3.5 to v0.3.6) | Metric | v0.3.5 | v0.3.6 | Change | |--------|--------|--------|--------| | **Pass rate** | 99.8% | 99.8% | 3,823 of 3,830 valid PDFs | | **Slow (>5s)** | 2 | **0** | Eliminated | | **Mean** | 23.3ms | **2.1ms** | **-91%** | | **p50** | 0.6ms | 0.6ms | -- | | **p90** | 3.0ms | **2.6ms** | -13% | | **p99** | 33.2ms | **18.0ms** | **-46%** | | **Max** | 68,722ms | **625ms** | **-99%** | | **Sum (all PDFs)** | 89.1s | **8.0s** | **-91%** | Text output verified byte-identical on 11 PDFs (862 KB of extracted text). 4 PDFs showed improved extraction quality from adaptive spacing. --- ## v0.3.5 -- 2026-02-15 > Performance, 3,830-PDF stability, and error recovery ### Performance - **Font caching across pages** -- Document-level font cache keyed by ObjectRef avoids re-parsing shared fonts on every page - **Page object caching** -- `get_page()` caches resolved page objects, eliminating repeated page tree traversal for multi-page extraction - **Structure tree caching** -- Structure tree result cached after first access, avoiding redundant parsing on every `extract_text()` call - **BT operator early-out** -- Text extraction skips the full pipeline for image-only pages that contain no BT (Begin Text) operators - **Larger I/O buffer for big files** -- BufReader capacity increased from 8 KB to 256 KB for files over 100 MB - **Xref reconstruction threshold removed** -- Eliminated the heuristic that triggered full-file reconstruction on valid portfolio PDFs with few objects ### Verified -- 3,830-PDF Corpus - 100% pass rate on 3,830 PDFs across veraPDF (2,907), Mozilla pdf.js (897), SafeDocs (26) - Zero timeouts, zero panics - p50 = 0.6ms, p90 = 3.0ms, p99 = 33ms ### Added -- Encryption - **Owner password authentication** -- Algorithm 7 for R<=4, Algorithm 12 for R>=5 - **R>=5 user password verification with SASLprep** -- Full AES-256 password verification using SHA-256 - **Public password authentication API** -- `Pdf::authenticate(password)` and `PdfDocument::authenticate(password)` ### Added -- PDF/A Compliance Validation - **XMP metadata validation** -- Checks for `pdfaid:part` and `pdfaid:conformance` entries - **Color space validation** -- Scans page content streams for device-dependent color operators without output intent - **AFRelationship validation** -- PDF/A-3 embedded file spec validation ### Added -- PDF/X Compliance Validation - **XMP PDF/X identification** -- Validates `pdfxid:GTS_PDFXVersion` - **Page box relationship validation** -- TrimBox within BleedBox within MediaBox - **ExtGState transparency detection** -- SMask, CA/ca, BM checks - **Device-dependent color detection** -- Flags unsupported color spaces - **ICC profile validation** -- Validates ICCBased profile streams ### Added -- Rendering - **Spec-correct clipping** -- Clip state scoped to q/Q save/restore - **Glyph advance width calculation** -- Per PDF spec section 9.4.4 - **Form XObject rendering** -- Parses /Matrix transform, uses form's /Resources ### Fixed -- Error Recovery (28+ real-world PDFs) - Missing objects resolve to Null per PDF spec section 7.3.10 - Lenient header version parsing for unusual version strings - Non-standard encryption algorithm matching (V=1, R=3 combinations) - Non-dictionary Resources treated as empty instead of erroring - Null nodes in page tree gracefully skipped - Corrupt content streams return empty content instead of errors - Enhanced page tree scanning with /Resources+/Parent heuristic ### Fixed -- DoS Protection - Page count validated against PDF spec Annex C.2 limit (8,388,607) ### Fixed -- Image Extraction - Content stream image extraction via Do operators - Nested Form XObject images with cycle detection - Inline images (BI...ID...EI sequences) - CTM transformations for image positioning - ColorSpace indirect reference resolution ### Fixed -- Parser Robustness - Multi-line object headers (`1 0\nobj` format used by Google-generated PDFs) - Extended header search from 1024 to 8192 bytes - Lenient version parsing for malformed headers ### Fixed -- Page Access Robustness - Pages without /Contents return empty content - Cyclic page tree detection prevents stack overflow - Null stream references handled gracefully - Pages without /Type entry found by /MediaBox or /Contents keys ### Fixed -- Encryption Robustness - AES decryption with undersized keys returns error instead of panic - Xref stream parsing hardened against malformed entries - Indirect /Encrypt references resolved before parsing ### Fixed -- Content Stream Processing - Dictionary-as-Stream fallback for bare dictionaries - Abbreviated filter names (AHx, A85, LZW, Fl, RL, CCF, DCT) - Content stream operator limit (default 1,000,000) ### Fixed -- Code Quality - Structure tree indirect object references resolved at parse time - Lexer R/RG token disambiguation - Stream whitespace trimming no longer strips NUL bytes or spaces from binary data ### Tests - 8 previously ignored tests un-ignored and fixed ### Removed - Empty `PdfImage` stub (extraction uses `ImageInfo`) - Commented-out `DocumentType::detect()` test block --- ## v0.3.4 -- 2026-02-12 > Parsing robustness, character extraction, and XObject paths ### Breaking Changes - `parse_header()` signature changed from `(u8, u8)` to `(u8, u8, u64)` to include byte offset ### Fixed -- PDF Parsing Robustness (Issue #41) - PDFs with binary prefixes or BOM headers now open successfully - Header search scans first 1024 bytes for `%PDF-` marker - Supports UTF-8 BOM, email headers, and other leading binary data - Lenient mode handles real-world malformed PDFs; strict mode for compliance testing ### Added -- Character-Level Text Extraction (Issue #39) - `extract_chars()` returns `Vec` with per-character positioning - Includes transformation matrix, rotation angle, advance width - Sorted in reading order with overlapping character deduplication - 30-50% faster than span extraction for character-only use cases - Exposed in both Rust and Python APIs ### Added -- XObject Path Extraction (Issue #40) - `extract_paths()` recursively processes Form XObjects via Do operator - Coordinate transformations via /Matrix properly applied - Graphics state properly isolated (save/restore) - Duplicate XObject detection prevents infinite loops - Nested XObjects supported ### Changed - Upgraded nom parser library from 7.1 to 8.0 --- ## v0.3.3 -- 2026-02-11 > CJK support, structure tree enhancements, and compliance foundations Includes all changes from v0.2.5 and v0.2.6 as a consolidated release. ### Highlights - **TagSuspect/MarkInfo support** -- Parse MarkInfo dictionary from document catalog - **Word Break /WB structure element** for CJK text - **Predefined CMap support** for Adobe-GB1 (Simplified Chinese), Adobe-Japan1 (Japanese), Adobe-CNS1 (Traditional Chinese), Adobe-Korea1 (Korean) - **Abbreviation expansion /E support** - **Type 0 /W array parsing** for CIDFont glyph widths - Soft hyphen (U+00AD) handling fix - Enhanced artifact filtering with subtype support - **Image embedding** in HTML and Markdown output (base64 data URIs) - **Image file export** with `embed_images=false` and `image_output_dir` - `PdfImage::to_base64_data_uri()` and `to_png_bytes()` methods --- ## v0.3.2 -- 2026-02-01 > Editing, encryption, and document security ### Added -- PDF Editing - `DocumentEditor` for modifying existing PDFs - Full annotation support (text markup, shapes, stamps, ink, file attachments, redactions) - Interactive form field creation (text, checkbox, radio, dropdown, list, button) - Form flattening - Link annotations (URLs, internal page navigation) - Outline/bookmark builder - PDF layers (Optional Content Groups) ### Added -- Encryption - Encryption on write (AES-256, AES-128, RC4-128, RC4-40) - Permission controls (print, copy, modify, annotate) - `EncryptionConfig` builder with `EncryptionAlgorithm` and `Permissions` - Digital signature foundation --- ## v0.3.1 -- 2026-01-14 > Form fields, multimedia, creation tools, and search ### Added -- PDF Creation - `Pdf::from_markdown()`, `Pdf::from_html()`, `Pdf::from_text()`, `Pdf::from_image()` - `PdfBuilder` fluent pattern for metadata and layout configuration - `DocumentBuilder` for programmatic PDF generation - Table rendering with `TableRenderer` - Graphics API: colors, gradients, patterns, blend modes, transparency - Page templates with headers, footers, page numbering, watermarks - Barcode generation (QR, Code128, EAN-13, UPC-A, Code39, ITF) ### Added -- Search - Text search with regex, case-sensitive/insensitive, whole word, page ranges - `SearchOptions` and `SearchResult` types - Position tracking with page/coordinates ### Added -- Form Field Coverage (95%) - Hierarchical field creation (parent/child structures with dotted names) - Field property modification (readonly, required, rect, tooltip, max length, alignment, default value) - FDF/XFDF export for form data exchange ### Added -- Multimedia Annotations - MovieAnnotation, SoundAnnotation, ScreenAnnotation, RichMediaAnnotation - ThreeDAnnotation with U3D and PRC format support ### Added -- XFA Form Support - XfaExtractor, XfaParser, XfaConverter (XFA to AcroForm conversion) ### Changed -- Python Bindings - True Python 3.8-3.14 support via abi3-py38 - Modern tooling: uv, pdm, ruff integration --- ## v0.3.0 -- 2026-01-10 > Extraction foundation -- unified API and core capabilities ### Added -- Unified Pdf API - `Pdf::open()` for reading existing PDFs - DOM-like page navigation with `pdf.page(0)` - `PdfDocument` low-level handle for advanced use cases ### Added -- Text Extraction - `extract_text()` -- full-page plain text - `extract_spans()` -- styled text runs with font metadata - Structure tree-based reading order for tagged PDFs - Intelligent line-break and space detection for untagged PDFs ### Added -- Image Extraction - `extract_images()` -- extract all images from a page - Format detection (JPEG, PNG, TIFF, JBIG2, CCITT) - Color space handling (DeviceRGB, DeviceCMYK, DeviceGray, ICCBased) ### Added -- Metadata Extraction - Document info dictionary (title, author, subject, keywords) - XMP metadata read/write - Page info (dimensions, rotation, media/crop/trim boxes) ### Added -- Form Extraction - `extract_form_fields()` for AcroForm field enumeration - Text, button, choice, and signature field types ### Added -- Conversion - `to_markdown()` -- page-level Markdown conversion - `to_html()` -- page-level HTML conversion - `to_plain_text()` -- configurable plain text output ### Added -- Compliance - PDF/A validation (ISO 19005, levels 1a through 3b) - PDF/X validation (ISO 15930, levels X-1a through X-6p) - PDF/UA validation (ISO 14289, levels UA-1 and UA-2) ### Added -- Rendering (requires `rendering` feature) - Render pages to PNG/JPEG via tiny-skia - Configurable DPI and scale ### Added -- Python Bindings - `PdfDocument` class with full extraction API - `Pdf` class with creation and high-level API - PyO3-based, published to PyPI as `pdf_oxide` --- ## v0.2.4 -- 2026-01-09 - CTM transformation fix for text positioning - Structure tree `/Alt` and `/Pg` parsing - FormulaRenderer for formula images ## v0.2.3 -- 2026-01-07 - BT/ET matrix reset per PDF spec - Geometric spacing detection in Markdown converter - `apply_intelligent_text_processing()` for ligatures and hyphenation ## v0.2.2 -- 2025-12-15 - Keyword optimization for discoverability ## v0.2.1 -- 2025-12-15 - Encrypted stream decoding improvements ## v0.1.4 -- 2025-12-12 - Encrypted stream decoding fixes ## v0.1.0 -- 2025-11-06 - Initial release - PDF text extraction with spec-compliant Unicode mapping - Intelligent reading order detection - Python bindings via PyO3 - Encrypted PDF support - Form field extraction - Image extraction --- # vs Python PDF Libraries PDF Oxide compared with PyMuPDF (fitz), pypdf, pdfplumber, and pdfminer. This page covers performance, feature coverage, licensing, and API differences to help you choose the right library. ## Summary | | PDF Oxide | PyMuPDF | pypdf | pdfplumber | pdfminer | |--|-----------|---------|-------|------------|----------| | **Mean extraction time** | **0.8ms** | 4.6ms | 4.1ms | 12.1ms | 23.2ms | 16.8ms | | **License** | **MIT** | AGPL-3.0 | BSD-3 | MIT | MIT | | **Language** | Rust + PyO3 | C (MuPDF) | Pure Python | Pure Python | Pure Python | | **Text extraction** | Yes | Yes | Yes | Yes | Yes | | **Character positions** | Yes | Yes | Partial | Yes | Yes | | **Image extraction** | Yes | Yes | Yes | No | No | | **Form fields** | Read + Write | Read + Write | Read + Write | Read only | No | | **PDF creation** | Yes | Yes | Limited | No | No | | **PDF editing** | Yes | Yes | Yes | No | No | | **Markdown output** | Yes | No | No | No | No | | **HTML output** | Yes | No | No | No | No | | **Encryption** | Read + Write | Read + Write | Read + Write | No | No | | **PDF/A validation** | Yes | No | No | No | No | | **Rendering** | Yes | Yes | No | No | No | | **Search** | Regex + spatial | Yes | No | No | No | | **Python versions** | 3.8--3.14 | 3.8--3.12 | 3.6+ | 3.8+ | 3.6+ | | **Install size** | ~5 MB wheel | ~20 MB wheel | ~1 MB | ~1 MB | ~1 MB | ## Performance Comparison Mean text extraction time per PDF, measured on 100 PDFs (mixed academic papers, reports, and forms): | Library | Mean Time | Relative | p99 | Pass Rate | |---------|-----------|----------|-----|-----------| | **PDF Oxide** | **0.8ms** | **1×** | **9ms** | **100%** | | PyMuPDF | 4.6ms | 5.8x | 28ms | 99.3% | | pypdfium2 | 4.1ms | 5.1x | 42ms | 99.2% | | pdfminer | 16.8ms | 21x | 124ms | 98.8% | | pdfplumber | 23.2ms | 29x | 189ms | 98.8% | | pypdf | 12.1ms | 15.1x | 97ms | 98.4% | PDF Oxide achieves its speed through a native Rust core compiled to a Python extension module via PyO3. There is no subprocess overhead or C library bridging -- the Rust code runs directly in the Python process. ## License Comparison | Library | License | Commercial Use | Copyleft | |---------|---------|----------------|----------| | **PDF Oxide** | **MIT** | Unrestricted | No | | PyMuPDF | AGPL-3.0 | Requires commercial license ($) | **Yes** | | pypdf | BSD-3 | Unrestricted | No | | pdfplumber | MIT | Unrestricted | No | | pdfminer | MIT | Unrestricted | No | PyMuPDF uses MuPDF under the AGPL-3.0 license. If you distribute software that uses PyMuPDF, your software must also be released under AGPL-3.0 -- or you must purchase a commercial license from Artifex. This applies to SaaS products, web applications, and any distributed binaries. PDF Oxide is MIT-licensed with no restrictions. Use it in proprietary products, SaaS platforms, or closed-source applications without any licensing obligations. | Use Case | PDF Oxide (MIT) | PyMuPDF (AGPL) | pypdf (BSD) | pdfplumber (MIT) | pdfminer (MIT) | |----------|----------------|----------------|-------------|-----------------|----------------| | Commercial product | Yes | Requires license | Yes | Yes | Yes | | Closed source | Yes | No (unless licensed) | Yes | Yes | Yes | | SaaS/cloud | Yes | Requires license | Yes | Yes | Yes | | Internal tools | Yes | Yes | Yes | Yes | Yes | ## API Comparison ### Text Extraction **PDF Oxide:** ```python from pdf_oxide import PdfDocument doc = PdfDocument("report.pdf") text = doc.extract_text(0) print(text) ``` **PyMuPDF:** ```python import fitz doc = fitz.open("report.pdf") page = doc[0] text = page.get_text() print(text) ``` **pypdf:** ```python from pypdf import PdfReader reader = PdfReader("report.pdf") page = reader.pages[0] text = page.extract_text() print(text) ``` **pdfplumber:** ```python import pdfplumber with pdfplumber.open("report.pdf") as pdf: page = pdf.pages[0] text = page.extract_text() print(text) ``` **pdfminer:** ```python from pdfminer.high_level import extract_text text = extract_text("report.pdf", page_numbers=[0]) print(text) ``` ### Character-Level Extraction **PDF Oxide:** ```python from pdf_oxide import PdfDocument doc = PdfDocument("report.pdf") chars = doc.extract_chars(0) for ch in chars: print(f"'{ch.char}' at ({ch.bbox[0]:.1f}, {ch.bbox[1]:.1f}) " f"size={ch.font_size:.1f}") ``` **PyMuPDF:** ```python import fitz doc = fitz.open("report.pdf") page = doc[0] blocks = page.get_text("dict")["blocks"] for block in blocks: if "lines" in block: for line in block["lines"]: for span in line["spans"]: print(f"'{span['text']}' size={span['size']:.1f}") ``` **pdfplumber:** ```python import pdfplumber with pdfplumber.open("report.pdf") as pdf: page = pdf.pages[0] for char in page.chars: print(f"'{char['text']}' at ({char['x0']:.1f}, {char['top']:.1f}) " f"size={char['size']:.1f}") ``` **pdfminer:** ```python from pdfminer.high_level import extract_pages from pdfminer.layout import LTChar for page_layout in extract_pages("report.pdf"): for element in page_layout: if hasattr(element, '__iter__'): for text_line in element: if hasattr(text_line, '__iter__'): for char in text_line: if isinstance(char, LTChar): print(f"'{char.get_text()}' at ({char.x0:.1f}, {char.y0:.1f}) " f"size={char.size:.1f}") ``` ### Image Extraction **PDF Oxide:** ```python from pdf_oxide import PdfDocument doc = PdfDocument("report.pdf") images = doc.extract_images(0) for i, img in enumerate(images): with open(f"image_{i}.{img.format}", "wb") as f: f.write(img.data) ``` **PyMuPDF:** ```python import fitz doc = fitz.open("report.pdf") page = doc[0] for i, img in enumerate(page.get_images()): xref = img[0] base_image = doc.extract_image(xref) with open(f"image_{i}.{base_image['ext']}", "wb") as f: f.write(base_image["image"]) ``` **pypdf:** ```python from pypdf import PdfReader reader = PdfReader("report.pdf") page = reader.pages[0] for i, image in enumerate(page.images): with open(f"image_{i}.{image.name.split('.')[-1]}", "wb") as f: f.write(image.data) ``` ### PDF Creation **PDF Oxide:** ```python from pdf_oxide import Pdf pdf = Pdf.from_markdown("# Hello World\n\nThis is a PDF.") pdf.save("output.pdf") # Also supports HTML pdf = Pdf.from_html("

      Hello

      World

      ") pdf.save("output.pdf") ``` **PyMuPDF:** ```python import fitz doc = fitz.open() page = doc.new_page() text_point = fitz.Point(72, 72) page.insert_text(text_point, "Hello World", fontsize=24) doc.save("output.pdf") ``` **pypdf:** ```python # pypdf can merge/modify PDFs but cannot create from scratch with text content. # Use reportlab or fpdf2 for creation, then merge with pypdf. ``` ### Encrypted PDFs **PDF Oxide:** ```python from pdf_oxide import PdfDocument doc = PdfDocument("encrypted.pdf") doc.authenticate("password") text = doc.extract_text(0) ``` **PyMuPDF:** ```python import fitz doc = fitz.open("encrypted.pdf") doc.authenticate("password") page = doc[0] text = page.get_text() ``` **pypdf:** ```python from pypdf import PdfReader reader = PdfReader("encrypted.pdf") reader.decrypt("password") text = reader.pages[0].extract_text() ``` ### Markdown and HTML Output **PDF Oxide (unique feature):** ```python from pdf_oxide import PdfDocument doc = PdfDocument("paper.pdf") # Convert to Markdown with heading detection md = doc.to_markdown(0, detect_headings=True) print(md) # Convert to HTML html = doc.to_html(0) print(html) ``` No other Python PDF library provides built-in Markdown or HTML conversion. ## Library Profiles ### PDF Oxide **Strengths:** - Fastest text extraction in benchmarks due to Rust core - Unified API for extraction, creation, and editing in a single library - Built-in Markdown and HTML export with heading detection - MIT licensed with no copyleft restrictions - Native compliance validation (PDF/A, PDF/UA, PDF/X) - Pre-built wheels for all major platforms and Python 3.8--3.14 - No system dependencies -- the wheel includes everything **Limitations:** - Newer library with a smaller community - Table extraction is basic compared to pdfplumber's algorithms - Rendering engine is less mature than MuPDF ### PyMuPDF (fitz) **Strengths:** - Mature and battle-tested (backed by MuPDF, in development since 2005) - Excellent rendering quality for complex PDFs - Built-in OCR integration (Tesseract) - Rich feature set: SVG export, page manipulation, table detection **Limitations:** - AGPL-3.0 license requires open-sourcing your application or purchasing a commercial license - Large wheel size (~20 MB) due to bundled MuPDF - No built-in Markdown export - No compliance validation ### pypdf **Strengths:** - Pure Python -- installs anywhere, no compiled dependencies - Lightweight and well-maintained - Good for PDF manipulation (merge, split, rotate, encrypt) - Large community and extensive documentation **Limitations:** - Slowest text extraction among compiled alternatives - Text extraction quality struggles with complex layouts - No rendering, no Markdown/HTML export, no table extraction ### pdfplumber **Strengths:** - Best table extraction of any Python PDF library - Excellent character-level positioning data - Visual debugging tools (annotated page images) - MIT licensed **Limitations:** - Pure Python -- significantly slower than compiled alternatives - Read-only -- no PDF creation or editing - No encryption or rendering ### pdfminer **Strengths:** - Detailed character and layout analysis - Good CJK text support - Foundation for pdfplumber and other tools - MIT licensed **Limitations:** - Slowest of all five libraries (pure Python, unoptimized) - Read-only, no creation or editing - Verbose API for common tasks - Less actively maintained ## When to Use Each | Use Case | Recommended Library | |----------|---------------------| | **Fast text extraction** | PDF Oxide | | **Commercial / proprietary product** | PDF Oxide, pypdf, pdfplumber, or pdfminer | | **PDF creation from Markdown/HTML** | PDF Oxide | | **Compliance validation (PDF/A, PDF/X)** | PDF Oxide | | **Table extraction from invoices** | pdfplumber | | **Visual debugging of extraction** | pdfplumber | | **Existing MuPDF investment** | PyMuPDF (if AGPL-compatible) | | **Minimal dependencies** | pypdf (pure Python) | | **Detailed layout analysis** | pdfminer | | **OCR for scanned documents** | PyMuPDF | ## Installation ```bash # PDF Oxide pip install pdf_oxide # PyMuPDF pip install pymupdf # pypdf pip install pypdf # pdfplumber pip install pdfplumber # pdfminer pip install pdfminer.six ``` PDF Oxide ships pre-built wheels for Linux (x86_64, aarch64), macOS (x86_64, arm64), and Windows (x86_64). No compiler or system libraries required. ## Related Pages - [Performance Benchmarks](/docs/performance) -- full corpus benchmark results - [Getting Started with Python](/docs/getting-started/python) -- installation and first extraction - [Python API Reference](/docs/reference/python-api) -- complete Python API - [vs Rust PDF Libraries](/docs/comparison/rust) -- Rust ecosystem comparison --- # vs Rust PDF Libraries PDF Oxide compared with the three most-used Rust PDF crates: lopdf, printpdf, and pdf-rs. Each targets a different level of abstraction and a different set of use cases. ## Summary | | PDF Oxide | lopdf | printpdf | pdf-rs | |--|-----------|-------|----------|--------| | **API level** | **High-level** | Low-level | Mid-level (creation) | Low-level (read) | | **Read PDFs** | Yes | Yes | No | Yes | | **Write PDFs** | Yes | Yes | Yes | No | | **Text extraction** | Yes (high-level) | Manual | No | Manual | | **Image extraction** | Yes (high-level) | Manual | No | Manual | | **Form fields** | Read + Write | Manual | No | Read only | | **PDF creation** | Yes | Yes | Yes | No | | **Markdown/HTML input** | Yes | No | No | No | | **Editing existing PDFs** | Yes | Yes (low-level) | No | No | | **Annotations** | Read + Write | Manual | No | Read only | | **Encryption** | Read + Write | No | No | No | | **PDF/A validation** | Yes | No | No | No | | **Rendering** | Yes (tiny-skia) | No | No | Partial | | **Python bindings** | Yes | No | No | No | | **License** | MIT | MIT | MIT | MIT | All four libraries are MIT-licensed. The differences are in scope and abstraction level. ## API Design Comparison ### PDF Oxide: High-Level, Task-Oriented PDF Oxide provides purpose-built methods for common tasks. You work with text, images, and form fields -- not PDF objects and dictionaries. ```rust use pdf_oxide::PdfDocument; let mut doc = PdfDocument::open("report.pdf")?; // Text extraction -- one call let text = doc.extract_text(0)?; println!("{}", text); // Styled spans with font metadata let spans = doc.extract_spans(0)?; for span in &spans { println!("'{}' font={} size={:.1}pt", span.text, span.font_name, span.font_size); } // Image extraction let images = doc.extract_images(0)?; for img in &images { println!("{}x{} {:?}", img.width, img.height, img.format); } // Form fields let fields = doc.extract_form_fields()?; for field in &fields { println!("{}: {:?}", field.name, field.value); } ``` PDF creation is equally straightforward: ```rust use pdf_oxide::api::Pdf; // From Markdown let pdf = Pdf::from_markdown("# Report\n\n| A | B |\n|---|---|\n| 1 | 2 |")?; pdf.save("report.pdf")?; // From HTML let pdf = Pdf::from_html("

      Report

      Content here.

      ")?; pdf.save("report.pdf")?; ``` ### lopdf: Low-Level Object Manipulation lopdf gives you direct access to PDF objects, streams, and the cross-reference table. You must understand the PDF specification to use it effectively. There is no built-in text extraction -- you navigate dictionaries and decode streams yourself. ```rust use lopdf::Document; let doc = Document::load("report.pdf")?; // Get page dictionary let page_id = doc.page_iter().next().unwrap(); let page = doc.get_dictionary(page_id)?; // Get content stream -- manual work let contents = page.get("Contents")?; let stream = doc.get_object(contents.as_reference()?)?; // To extract text you must: // 1. Parse the content stream operators // 2. Resolve font references from /Resources // 3. Decode CMap/ToUnicode mappings // 4. Apply text matrix transformations // 5. Handle encoding differences // // lopdf does not provide any of this -- it is raw object access println!("Page has {} objects", doc.objects.len()); ``` lopdf is the right tool when you need to manipulate PDF structure directly: merging documents, rewriting object streams, or building specialized PDF processors. ### printpdf: PDF Creation Only printpdf is a creation-only library. It cannot read or parse existing PDFs. It provides a typed API for building PDF documents from scratch with text, images, and vector graphics. ```rust use printpdf::*; let (doc, page1, layer1) = PdfDocument::new( "Report", Mm(210.0), Mm(297.0), "Layer 1" ); let current_layer = doc.get_page(page1).get_layer(layer1); // Add text -- requires manual font loading let font = doc.add_builtin_font(BuiltinFont::Helvetica)?; current_layer.use_text("Hello World", 24.0, Mm(10.0), Mm(280.0), &font); // Save doc.save(&mut std::io::BufWriter::new( std::fs::File::create("output.pdf")?, ))?; // Cannot read existing PDFs // Cannot extract text, images, or form fields ``` printpdf is the right tool when you only need to generate new PDFs and want a clean, focused creation API. ### pdf-rs: Low-Level PDF Reading pdf-rs parses PDF structure into Rust types but provides minimal high-level functionality. You get typed access to PDF objects but must still handle text decoding, font resolution, and content stream parsing. ```rust use pdf::file::FileOptions; let file = FileOptions::cached().open("report.pdf")?; // Access page objects let page = file.get_page(0)?; let media_box = page.media_box()?; println!("Page size: {:?}", media_box); // Content stream access -- low-level if let Some(ref contents) = page.contents { // Returns raw operations -- you must interpret them // No built-in text assembly, font decoding, or layout analysis } // Cannot write or modify PDFs ``` pdf-rs is the right tool when you need a type-safe PDF parser for analysis, validation, or building a custom renderer. ## Feature Comparison by Task ### Text Extraction | Library | Built-in | Quality | Effort Required | |---------|----------|---------|-----------------| | **PDF Oxide** | **Yes** | Production-grade (Unicode, CJK, reading order) | One method call | | lopdf | No | N/A | Hundreds of lines of custom code | | printpdf | No | N/A | Not possible (write-only) | | pdf-rs | No | N/A | Significant custom code required | PDF Oxide handles CMap/ToUnicode decoding, font metric-based spacing, structure tree reading order, and ligature reconstruction. Implementing equivalent functionality on top of lopdf or pdf-rs requires thousands of lines of code and deep PDF specification knowledge. ### PDF Creation | Library | Approach | Markdown/HTML Input | Tables | Barcodes | |---------|----------|---------------------|--------|----------| | **PDF Oxide** | High-level + low-level | **Yes** | **Yes** | **Yes** | | lopdf | Raw object construction | No | No | No | | printpdf | Typed layer API | No | No | No | | pdf-rs | N/A (read-only) | N/A | N/A | N/A | ### Encryption | Library | Read Encrypted | Write Encrypted | Algorithms | |---------|---------------|-----------------|------------| | **PDF Oxide** | **Yes** | **Yes** | RC4-40, RC4-128, AES-128, AES-256 | | lopdf | No | No | -- | | printpdf | No | No | -- | | pdf-rs | Partial | No | RC4 only | ### Compliance | Library | PDF/A | PDF/X | PDF/UA | |---------|-------|-------|--------| | **PDF Oxide** | **Validate + Convert** | **Validate** | **Validate** | | lopdf | No | No | No | | printpdf | Partial (PDF/A-1b output) | No | No | | pdf-rs | No | No | No | ## Performance Comparison Single-threaded text extraction from a 50-page document: | Library | Time | Notes | |---------|------|-------| | **PDF Oxide** | **12ms** | High-level `extract_text()` on all 50 pages | | lopdf | N/A | No built-in text extraction to benchmark | | printpdf | N/A | Cannot read PDFs | | pdf-rs | N/A | No built-in text extraction to benchmark | For raw PDF parsing (loading the file and resolving the cross-reference table), all four libraries are fast. The performance difference becomes significant when you need to extract text, because PDF Oxide does the font decoding, spacing analysis, and reading order detection in optimized Rust code. ### PDF Oxide v0.3.8 Corpus Results Tested against 3,830 public PDFs (veraPDF, Mozilla pdf.js, DARPA SafeDocs): | Metric | Value | |--------|-------| | Mean | 0.8ms | | Median (p50) | 0.6ms | | p99 | 9ms | | Pass rate | 100% | | Slow (>5s) | 0 | ## Use Case Matrix ### "I need to extract text from PDFs" | Crate | Suitable? | Notes | |-------|-----------|-------| | **PDF Oxide** | **Yes** | Best extraction quality, reading order, font metadata | | lopdf | No | No text extraction | | printpdf | No | Cannot read PDFs | | pdf-rs | Partial | Basic parsing, no high-level text extraction | ### "I need to create PDFs" | Crate | Suitable? | Notes | |-------|-----------|-------| | **PDF Oxide** | **Yes** | High-level (Markdown/HTML) and low-level APIs | | lopdf | Partial | Low-level object construction | | **printpdf** | **Yes** | Clean creation API, no reading | | pdf-rs | No | Read-only | ### "I need to edit existing PDFs" | Crate | Suitable? | Notes | |-------|-----------|-------| | **PDF Oxide** | **Yes** | DOM-like editing, annotations, forms | | lopdf | Partial | Low-level object manipulation | | printpdf | No | Cannot read PDFs | | pdf-rs | No | Read-only | ### "I need the full lifecycle (extract + create + edit)" | Crate | Suitable? | Notes | |-------|-----------|-------| | **PDF Oxide** | **Yes** | Only crate covering all three | | lopdf + printpdf | Partial | Two crates, no text extraction | | pdf-rs + printpdf | Partial | Two crates, no editing | ## Dependency Footprint | Library | Dependencies | Compile Time | Binary Size | |---------|-------------|--------------|-------------| | **PDF Oxide** | ~40 (core) | ~30s | ~4 MB | | lopdf | ~15 | ~10s | ~1 MB | | printpdf | ~20 | ~15s | ~2 MB | | pdf-rs | ~25 | ~20s | ~2 MB | PDF Oxide has more dependencies because it includes font parsing, image decoding, content stream interpretation, and encryption -- features that the other libraries leave to the user or omit entirely. With all optional features (`rendering`, `barcodes`, `office`), the count rises to ~100. ## Combining Libraries Since all four are MIT-licensed, you can combine them in a single project: ```toml [dependencies] pdf_oxide = "0.3" lopdf = "0.32" # Optional: raw object access for edge cases ``` Common patterns: - **PDF Oxide + lopdf**: Use PDF Oxide for extraction and creation, fall back to lopdf for edge cases requiring raw object manipulation. - **PDF Oxide + printpdf**: Use PDF Oxide for reading and printpdf for specialized creation workflows. ## When to Use Each **Choose PDF Oxide if** you need more than one PDF capability (extraction + creation, or extraction + editing) and want a single, well-tested dependency. **Choose lopdf if** you need low-level PDF structure manipulation and are comfortable working with the PDF spec directly. Good for merging, splitting, and batch PDF processing. **Choose printpdf if** you only create PDFs and never need to read them. The cleanest API for report and document generation. **Choose pdf-rs if** you need a spec-compliant parser for PDF analysis or are building your own rendering pipeline. ## Related Pages - [Performance Benchmarks](/docs/performance) -- full corpus benchmark results - [Getting Started with Rust](/docs/getting-started/rust) -- installation and first extraction - [Rust API Reference](/docs/reference/api) -- complete Rust API - [vs Python PDF Libraries](/docs/comparison/python) -- Python ecosystem comparison ---