lopdf

repository·main·Indexed 24 days ago

https://github.com/j-f-liu/lopdf

A Rust library for PDF document manipulation, providing tools for parsing, modifying, and working with the PDF file format. It supports creating PDFs from scratch, merging documents, text extraction, and text replacement. The library includes a companion CLI tool called `pdfutil` for operations such as compression, page extraction, and object pruning. It supports modern PDF features like object streams and XRef streams, and provides optional Cargo features for async loading, serialization, and various date backends.

Tokens
14.5K
Snippets
16
Records
86
Agent score
80%

What's inside lopdf

  1. Overview of lopdf

    main
    lopdf is a Rust library designed for PDF document manipulation. It is built to handle the complexities of the PDF file format, and users interested in the underlying specification can refer to the PDF 1.7 or PDF 2.0 standards.
  2. Requirements for using lopdf

    main

    To use lopdf, you must have Rust 1.85 or later installed. This version is required to support Rust 2024 edition features and object streams support.

    To check your current version:

    rustc --version

    To update your Rust toolchain:

    rustup update
    rustc --version
  3. Handle encrypted PDFs with automatic decryption

    main

    lopdf automatically attempts to decrypt PDFs that use empty passwords during the Document::load() process.

    Workflow for encrypted documents:

    1. Load the document using Document::load() (supports both sync and async).
    2. Check doc.is_encrypted() to see if the document has encryption.
    3. Check doc.encryption_state.is_some() to verify if decryption was successful.
    4. If successful, you can use all standard document methods (e.g., get_pages(), extract_text(), get_object()) transparently.

    Limitations:

    • Currently only supports PDFs encrypted with empty passwords.
    • For password-protected PDFs, you must use the authenticate_password method manually.
    • Some encryption algorithms may not be fully supported.
    use lopdf::Document;
    
    #[cfg(not(feature = "async"))]
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        // Load an encrypted PDF - automatically attempts decryption
        let doc = Document::load("assets/encrypted.pdf")?;
        
        // Check encryption status
        if doc.is_encrypted() {
            println!("Document is encrypted");
            
            // Check if decryption was successful
            if doc.encryption_state.is_some() {
                println!("Successfully decrypted");
                
                // Now you can work with the document normally
                let pages = doc.get_pages();
                println!("Pages: {}", pages.len());
                
                // Extract text
                let page_nums: Vec<u32> = pages.keys().cloned().collect();
                let text = doc.extract_text(&page_nums)?;
                println!("Text length: {} chars", text.len());
                
                // Access objects
                for i in 1..=10 {
                    if let Ok(_) = doc.get_object((i, 0)) {
                        println!("Object ({}, 0) accessible", i);
                    }
                }
            } else {
                println!("Decryption failed - password required");
            }
        }
        
        Ok(())
    }
  4. Use the pdfutil CLI for PDF operations

    main
    The pdfutil command-line tool provides a suite of utilities for common PDF tasks such as merging, extracting text, replacing text, compressing, and managing PDF objects. It is built on top of the lopdf library.
  5. Use IncrementalDocument to append changes to a PDF

    main

    The IncrementalDocument struct is used to perform incremental updates on an existing PDF. Instead of rewriting the entire file, you can create an incremental update that appends new data to the original bytes.

    To use it, you must initialize it using create_from, providing both the original raw bytes and the parsed Document object. This ensures that the new incremental updates are correctly layered over the existing structure.

    Warning: The prev_bytes and prev_documents must match exactly. If they do not, the resulting PDF may be broken.

  6. Configure Cargo features for lopdf

    main

    You can enable specific features in your Cargo.toml to add functionality such as date conversions, parallel parsing, serialization, or asynchronous loading.

    Note on Date Backends: The date features (chrono-clock, chrono, jiff, and time) are alternatives, not layers. Enabling more than one only adds extra dependencies. If you only need to read PDF dates faithfully, chrono (without clock) is sufficient as PDF dates use fixed offsets from UTC rather than named zones.

  7. Represent PDF data using the Object enum

    main

    The Object enum is the core representation of any PDF data type. It can represent Null, Boolean, Integer, Real, Name, String, Array, Dictionary, Stream, or a Reference to another object via an ObjectId (a tuple of (u32, u16) representing object and generation numbers).

    Most primitive types implement From<T> for easy conversion into an Object.

  8. Configure PDF compression with SaveOptions

    main

    The SaveOptions builder allows you to control how a PDF is serialized, specifically regarding compression and modern PDF features like object streams and XRef streams.

    Use use_object_streams(true) to enable object streams (PDF 1.5+) and use_xref_streams(true) to enable binary cross-reference streams instead of traditional ASCII tables.

    use lopdf::SaveOptions;
    
    let options = SaveOptions::builder()
        .use_object_streams(true)        // Enable object streams (default: false)
        .use_xref_streams(true)          // Enable xref streams (default: false)
        .max_objects_per_stream(200)     // Max objects per stream (default: 100)
        .compression_level(9)            // zlib level 0-9 (default: 6)
        .build();
  9. Understand EncryptionVersion variants

    main

    The EncryptionVersion enum defines the different PDF encryption standards supported by the library. This is used to specify how a document should be encrypted or to describe an existing one:

    • V1: PDF 1.4 (deprecated); RC4 or AES with 40-bit key length.
    • V2: PDF 1.4 (deprecated); RC4 or AES with key lengths > 40 bits.
    • V4: PDF 1.5 (deprecated); AES with 128-bit key length.
    • V5: PDF 2.0; AES with 256-bit key length.
    • R5: Proprietary Adobe extension (deprecated/for testing only).

    Note that V4 and V5 require specifying crypt_filters, stream_filter, and string_filter to define how data is processed.

  10. Create and build a new Object Stream

    main

    Use ObjectStreamBuilder to construct a new object stream. You can configure the maximum number of objects allowed in a single stream and the compression level used when converting the stream to a PDF Stream object.

    1. Initialize a builder with ObjectStream::builder().
    2. Configure settings using .max_objects(usize) and .compression_level(u32).
    3. Call .build() to get an ObjectStream instance.
    4. Use .add_object(id, obj) to populate it.
    5. Use .to_stream_object() to generate a Stream object ready for PDF insertion.
  11. Decrypt and extract text from PDF documents

    main

    To load an encrypted PDF, use Document::load(path). By default, lopdf attempts to decrypt using an empty password.

    • Use doc.is_encrypted() to check the encryption status.
    • Check doc.encryption_state.is_some() to verify if decryption was successful.
    • Once decrypted, use doc.get_pages() to retrieve page IDs and doc.extract_text(&page_numbers) to extract text content from specific pages.

    Async Support: If the async feature is enabled, use Document::load(path).await within a runtime like tokio.

    // Synchronous version
    let doc = Document::load("assets/encrypted.pdf").unwrap();
    
    if doc.is_encrypted() {
        if doc.encryption_state.is_some() {
            println!("Successfully decrypted with empty password");
        }
    }
    
    let pages = doc.get_pages();
    let page_numbers: Vec<u32> = pages.keys().cloned().collect();
    let text = doc.extract_text(&page_numbers).unwrap();
  12. Create a new PDF document

    main

    To create a PDF from scratch, use Document::with_version(version) to initialize a new document. You can then build the document structure by adding objects (like fonts, resources, and pages) using doc.add_object() or doc.new_object_id().

    Key concepts for manual construction:

    • Dictionaries: Use the dictionary! macro to create key-value relationships for PDF objects (e.g., Font, Page, Catalog).
    • Streams: Use Stream::new(dictionary, bytes) to wrap content like text operations.
    • Content: Use Content and Operation to define the actual drawing/text instructions (e.g., BT for Begin Text, Tj for text strings).
    • Hierarchy: A standard PDF requires a Catalog (the root) which points to a Pages tree, which in turn contains individual Page objects.
    use lopdf::dictionary;
    use lopdf::{Document, Object, Stream};
    use lopdf::content::{Content, Operation};
    
    // `with_version` specifes the PDF version this document compliess with.
    let mut doc = Document::with_version("1.5");
    
    // "Pages" is the root node of the page tree.
    let pages_id = doc.new_object_id();
    
    // Fonts are dictionaries.
    let font_id = doc.add_object(dictionary! {
        "Type" => "Font",
        "Subtype" => "Type1",
        "BaseFont" => "Courier",
    });
    
    // Resource dictionaries contain fonts used by pages.
    let resources_id = doc.add_object(dictionary! {
        "Font" => dictionary! {
            "F1" => font_id,
        },
    });
    
    // Content defines the operations (operators and operands).
    let content = Content {
        operations: vec![
            Operation::new("BT", vec![]),
            Operation::new("Tf", vec!["F1".into(), 48.into()]),
            Operation::new("Td", vec![100.into(), 600.into()]),
            Operation::new("Tj", vec![Object::string_literal("Hello World!")]),
            Operation::new("ET", vec![]),
        ],
    };
    
    let content_id = doc.add_object(Stream::new(dictionary! {}, content.encode().unwrap()));
    
    // Page dictionary
    let page_id = doc.add_object(dictionary! {
        "Type" => "Page",
        "Parent" => pages_id,
        "Contents" => content_id,
    });
    
    // Pages tree root
    let pages = dictionary! {
        "Type" => "Pages",
        "Kids" => vec![page_id.into()],
        "Count" => 1,
        "Resources" => resources_id,
        "MediaBox" => vec![0.into(), 0.into(), 595.into(), 842.into()],
    };
    
    doc.objects.insert(pages_id, Object::Dictionary(pages));
    
    // Catalog
    let catalog_id = doc.add_object(dictionary! {
        "Type" => "Catalog",
        "Pages" => pages_id,
    });
    
    doc.trailer.set("Root", catalog_id);
    doc.compress();
    
    // Save
    doc.save("example.pdf").unwrap();