oxidize-pdf

repository·main·Indexed 41 days ago

https://github.com/bzsanti/oxidizepdf

A comprehensive, pure-Rust PDF toolkit for standard workflows (generation, parsing, manipulation) and advanced AI/RAG applications. It features structure-aware, metadata-rich chunking via the `rag_chunks()` API, providing heading context, page numbers, and element types. The project includes official bindings for Python (v0.5.x), .NET (OxidizePdf.NET v0.9.x), LlamaIndex (v0.1.x), and an MCP server.

Tokens
164.9K
Snippets
455
Records
653
Agent score
47%

What's inside oxidize-pdf

  1. Overview of Test Corpus Tiers

    main

    The oxidize-pdf test corpus is organized into 7 tiers (T0–T6) designed for different testing objectives ranging from regression prevention to adversarial safety.

    TierPDFsTriggerPurpose
    T0749Every commitRegression prevention
    T1~2,000Every commitSpec conformance
    T22,000NightlyGenerator diversity
    T3750NightlyError recovery
    T4500WeeklyAI/RAG accuracy
    T5~900WeeklyQuality benchmarking
    T6200WeeklyAdversarial safety
  2. Font Subsetting Improvements Implementation Plan

    main

    This plan outlines a major refactor of the font subsetting system to fix CJK character rendering bugs and significantly reduce font subset output size (by 3-14x).

    Key Architectural Changes:

    • Desubroutinization: Replacing the 'Local Subr' stub-replacement strategy with full charstring desubroutinization.
    • CFF Unification: All CFF output will be unified as raw CID-keyed CFF (removing the OTF wrapper).
    • TTF Optimization: Stripping unnecessary tables (cmap, OS/2, name) from TTF subsets.
    • Modularization: Breaking down the monolithic cff_subsetter.rs into a modular cff/ directory structure.

    Technical Specifications:

    • Standards: Adobe Technical Note #5176 (CFF), OpenType spec, and ISO 32000-1.
    • Stack: Rust.
  3. Understand the OxidizePDF Edition Features

    main

    OxidizePDF is distributed in three editions, separating open-source core functionality from commercial enhancements.

    Community Edition (Open Source - MIT License)

    Includes the core PDF engine and basic manipulation tools:

    • Native PDF parser and writer
    • Graphics and text generation
    • Image support (JPEG, PNG)
    • Basic semantic tagging
    • Text and image extraction
    • Page operations (merge, split, rotate)
    • OCR integration (Tesseract)
    • CLI and REST API

    PRO Edition (Commercial)

    Includes all Community features plus:

    • Export to Word (DOCX), OpenDocument (ODT), and Markdown
    • Advanced AI-Ready features
    • Invoice/Receipt and Form field detection
    • Digital signatures
    • Advanced analytics

    Enterprise Edition

    Includes all PRO features plus:

    • Cloud integrations (AWS, Azure, GCP)
    • WebSocket real-time processing
    • Multi-tenant support
    • Advanced monitoring
    • Custom ML models
    • Priority support
  4. Implement EncodingCMap for Non-Identity CID Encoding

    main

    To support Type0 text extraction for non-identity CID encoding, you must implement the EncodingCMap structure. This structure maps character codes (which can be variable-width) to CIDs. It is distinct from a standard CMap (ToUnicode), which maps to Unicode hex strings.

    Key components of EncodingCMap include:

    • codespace_ranges: Defines the byte widths for character codes.
    • single_cid: A map for exact cidchar matches.
    • cid_ranges: A list of ranges for cidrange matches.
    • usecmap_parent: The name of the parent CMap if specified via the usecmap keyword.
    #[derive(Debug, Clone, Default)]
    pub(crate) struct EncodingCMap {
        pub codespace_ranges: Vec<CodeRange>,
        pub single_cid: std::collections::HashMap<Vec<u8>, u16>,
        pub cid_ranges: Vec<CidRange>,
        pub notdef_ranges: Vec<CidRange>,
        pub ordering: Option<String>,
        pub usecmap_parent: Option<String>,
        pub wmode: u8,
    }
  5. Understand table structure: Merged cells and headers

    main

    Rich table structures (merged cells and multi-level headers) are only produced when a hard signal is present in the PDF:

    • Merged Cells: Detected only from the drawn grid. If an internal dividing line is absent, adjacent cells are merged. Note that 'borderless' tables are returned as flat grids without merged-cell info.
    • Multi-level Headers: Represented by header rows containing merged cells. If the PDF is tagged, structure tags are used; otherwise, the top row is assumed to be the header. A nested hierarchy is not read from the internal structure tree.
    • Borderless Detection: This is best-effort. If detection is ambiguous, content is preserved as prose rather than forced into a grid to prevent data loss.
    • GitHub-Flavored Markdown Export: Merged-cell values are repeated across all covered columns. Multi-level headers are flattened into a single row using the › separator.
  6. Understanding the impact of line-interleaving fixes on chunk counts

    main

    The implementation of the row_id Y-up-jump heuristic (Issue #265) for resolving line interleaving can cause significant changes in the number of chunks produced during PDF extraction. Developers should not rely on a strict ±5% per-document chunk-count guardrail, as legitimate structural improvements can cause larger deltas:

    • Decreased chunk counts: Occur when columns are correctly grouped into coherent paragraphs instead of being split into many short, interleaved chunks (e.g., the NCSC document saw a -24% change).
    • Increased chunk counts: Occur when improved heading detection or better structural boundary recognition creates more distinct segments (e.g., the Higgs document saw a +90% change).

    Instead of monitoring chunk count variance, evaluate extraction quality based on the absence of 'alphabet soup' (garbled substrings) and the presence of coherent prose.

  7. Understand Per-Document Font Metrics in oxidize-pdf

    main

    Starting with version 2.8.0, oxidize-pdf supports per-document custom font metrics. This replaces the previous process-wide CUSTOM_FONT_METRICS registry to prevent memory leaks, race conditions, and name leaks in long-running services or concurrent environments.

    Key Changes:

    • Scoped Storage: Each Document now owns a FontMetricsStore (Arc-backed, Clone + Send + Sync).
    • Hierarchical Lookup: When looking up font metrics, the system follows this priority:
      1. Document scope: The specific FontMetricsStore attached to the Document.
      2. Legacy global: The old process-wide registry (marked as #[deprecated(since = "2.8.0")]).
      3. Default: Fallback metrics with a rate-limited warning if the font is unknown.
    • Pure Read Path: The text::metrics::get_font_metrics function no longer auto-registers default metrics into the global registry on a read miss; it is now a pure read operation.
  8. Validate CFF subsetting implementation integrity

    main

    When implementing or refactoring CFF (Compact Font Format) subsetting, use byte-level desubroutinizer tests to verify correctness. While outline verification (comparing glyph paths) is a more advanced method, desubroutinizer tests are sufficient for catching byte-level errors in charstring processing.

    Additionally, use debug_assert! for structural validation after CFF subset generation. These assertions should perform lightweight checks, such as parsing the output header and verifying INDEX counts, to ensure the generated subset is structurally sound.

  9. Understand the Per-Document Font Metrics architecture

    main

    To prevent memory leaks in long-running services (resolving issue #230), oxidize-pdf is moving from a process-wide global registry (CUSTOM_FONT_METRICS) to a hierarchical, document-scoped model.

    Key Concepts:

    • FontMetricsStore: An Arc-backed, Clone + Send + Sync type that lives on a specific Document. It bounds custom-font metrics to the lifetime of that document.
    • Hierarchical Lookup: When looking up font metrics, the system first checks the FontMetricsStore attached to the Document (or Page). If the metrics are not found in the document scope, it falls back to the legacy global registry.
    • Lifecycle: Metrics are written to the per-document store via Document::add_font_from_bytes. Page objects carry an Option<FontMetricsStore>, which is injected during page creation via Document::new_page_*() or Document::add_page.
  10. Understand the Type0 text extraction decode pipeline

    main

    To extract text from Type0/CID fonts correctly, oxidize-pdf follows a multi-step decoding pipeline. If a PDF uses non-Identity encoding, the extractor must first map the content-stream bytes to a CID (Character Identifier) before mapping that CID to a Unicode character.

    The pipeline follows this flow:

    content-stream bytes $\rightarrow$ [Encoding CMap: code $\rightarrow$ CID] $\rightarrow$ [CID $\rightarrow$ Unicode] $\rightarrow$ text

    If the code $\rightarrow$ CID step is skipped (which happens if the extractor incorrectly assumes code == CID), the resulting text will appear as 'glyph-code garbage'.

  11. How test skipping works with missing corpora

    main

    Tests in oxidize-pdf are designed to detect if their required corpus is present. If a corpus is missing, the corresponding tests will gracefully skip with an informational message rather than failing.

    This behavior allows:

    • CI pipelines to pass even without the full corpus.
    • Local development without the need to download all 7K PDFs.
    • Targeted testing of only the tiers you have downloaded.
  12. Use oxidize-pdf for RAG-ready PDF chunking

    main

    The oxidize-pdf library (available in Rust, Python, and .NET) is designed to ingest complex, real-world PDFs and partition them into structured, RAG-ready (Retrieval-Augmented Generation) chunks.

    Instead of simple text extraction, the library uses a rag_chunks() method (utilizing a HybridChunkConfig by default) to produce chunks that include hierarchical context, element types (e.g., paragraph, heading), and token estimates. This makes the output immediately compatible with vector stores and LLM orchestration frameworks like LangChain or LlamaIndex.

    chunks = doc.rag_chunks() // default HybridChunkConfig