rpdfium 7676.6.0

A faithful Rust port of Google's PDFium PDF rendering engine
Documentation
// Derived from PDFium's fpdfsdk/fpdf_edit*.cpp
// Original: Copyright 2014 The PDFium Authors
// Licensed under BSD-3-Clause / Apache-2.0
// See pdfium-upstream/LICENSE for the original license.

//! PDF editing facade — create, modify, and save PDF documents.
//!
//! This module is available when the `edit` feature is enabled.

use std::path::Path;

use rpdfium_core::Rect;
use rpdfium_edit::EditDocument;

use crate::{Document, Library};

// Re-exports from rpdfium-edit
pub use rpdfium_edit::annotation_edit::{AnnotationBuilder, AnnotationSpec, AnnotationUpdates};
pub use rpdfium_edit::content_gen::{
    ResourceCollector, build_resource_dict, generate_content_stream,
};
pub use rpdfium_edit::error::{EditError, EditResult};
pub use rpdfium_edit::font_reg::{FontRegistration, FontType};
pub use rpdfium_edit::form_edit::{EditFormField, EditableForm, FieldValue};
pub use rpdfium_edit::npage_export::NupPageSettings;
pub use rpdfium_edit::page_object::{
    FillMode, FormObject, ImageObject, PageObject, PathObject, TextObject,
};
pub use rpdfium_edit::writer::WriteOptions;
pub use rpdfium_edit::{write_full, write_incremental};

/// An editable PDF document that supports creating, modifying, and saving PDFs.
pub struct EditableDocument<'lib> {
    #[allow(dead_code)]
    library: &'lib Library,
    inner: EditDocument,
}

impl<'lib> EditableDocument<'lib> {
    /// Create a new blank editable document.
    pub fn new_blank(library: &'lib Library) -> Self {
        Self {
            library,
            inner: EditDocument::new_blank(),
        }
    }

    /// Convert an existing document into an editable one.
    ///
    /// This consumes the `Document` and transfers ownership of the object store.
    pub fn from_document(doc: Document<'lib>) -> Self {
        let page_ids = doc.page_ids.clone();
        let catalog_id = doc.catalog_id;
        let library = doc.library;
        let inner = EditDocument::from_store(doc.store, page_ids, catalog_id);
        Self { library, inner }
    }

    /// Returns a reference to the inner `EditDocument`.
    pub fn inner(&self) -> &EditDocument {
        &self.inner
    }

    /// Returns a mutable reference to the inner `EditDocument`.
    pub fn inner_mut(&mut self) -> &mut EditDocument {
        &mut self.inner
    }

    /// Number of pages.
    pub fn page_count(&self) -> usize {
        self.inner.page_count()
    }

    /// Add a blank page with the given media box.
    pub fn add_page(&mut self, media_box: Rect) {
        self.inner.add_page(media_box);
    }

    /// Add a page with pre-built content objects.
    pub fn add_page_with_content(
        &mut self,
        media_box: Rect,
        objects: &[rpdfium_edit::page_object::PageObject],
    ) -> Result<(), EditError> {
        self.inner.add_page_with_content(media_box, objects)?;
        Ok(())
    }

    /// Register a Standard-14 font.  Corresponds to `FPDFText_LoadStandardFont`.
    pub fn load_standard_font(&mut self, name: &str) -> Result<FontRegistration, EditError> {
        self.inner.load_standard_font(name)
    }

    /// Embed arbitrary font data.  Corresponds to `FPDFText_LoadFont`.
    pub fn load_font_from_data(
        &mut self,
        data: &[u8],
        font_type: FontType,
        is_cid: bool,
    ) -> Result<FontRegistration, EditError> {
        self.inner.load_font_from_data(data, font_type, is_cid)
    }

    /// Delete a page by index.
    pub fn delete_page(&mut self, index: usize) -> Result<(), EditError> {
        self.inner.delete_page(index)
    }

    /// Add an annotation to a page.
    pub fn add_annotation(
        &mut self,
        page_index: usize,
        spec: AnnotationSpec,
    ) -> Result<rpdfium_core::error::ObjectId, EditError> {
        self.inner.add_annotation(page_index, spec)
    }

    /// Save the document as a full PDF rewrite.
    pub fn save(&self) -> Result<Vec<u8>, EditError> {
        let options = WriteOptions::default();
        let mut output = Vec::new();
        write_full(&self.inner, &mut output, &options)?;
        Ok(output)
    }

    /// Save the document with custom write options.
    pub fn save_with_options(&self, options: &WriteOptions) -> Result<Vec<u8>, EditError> {
        let mut output = Vec::new();
        write_full(&self.inner, &mut output, options)?;
        Ok(output)
    }

    /// Save the document to a file.
    pub fn save_to_file(&self, path: impl AsRef<Path>) -> Result<(), EditError> {
        let data = self.save()?;
        std::fs::write(path, data)?;
        Ok(())
    }

    /// Save as incremental update appended to the original data.
    ///
    /// Returns the full PDF (original + appended changes).
    pub fn save_incremental(&self, original_data: &[u8]) -> Result<Vec<u8>, EditError> {
        let options = WriteOptions::default();
        let mut output = Vec::new();
        write_incremental(&self.inner, original_data, &mut output, &options)?;
        Ok(output)
    }

    /// Import pages from `src` into this document, inserting at `insert_at`.
    ///
    /// Each page from `src_page_indices` is deep-cloned with all referenced
    /// objects (fonts, images, content streams, resources).
    ///
    /// Corresponds to `CPDF_PageExporter::ExportPages`.
    pub fn import_pages_from(
        &mut self,
        src: &EditableDocument<'_>,
        src_page_indices: &[usize],
        insert_at: usize,
    ) -> Result<(), EditError> {
        rpdfium_edit::page_export::export_pages(
            src.inner(),
            src_page_indices,
            &mut self.inner,
            insert_at,
        )
    }

    /// Arrange pages from `src` in an N-up grid on new destination pages.
    ///
    /// Each batch of `pages_on_x × pages_on_y` source pages is placed on one
    /// destination page of size `dest_page_size` (width, height in PDF units).
    ///
    /// Corresponds to `CPDF_NPageToOneExporter::ExportNPagesToOne`.
    pub fn import_npage_layout(
        &mut self,
        src: &EditableDocument<'_>,
        src_page_indices: &[usize],
        dest_page_size: (f64, f64),
        pages_on_x: usize,
        pages_on_y: usize,
    ) -> Result<(), EditError> {
        rpdfium_edit::npage_export::export_npage_to_one(
            src.inner(),
            src_page_indices,
            &mut self.inner,
            dest_page_size,
            pages_on_x,
            pages_on_y,
        )
    }

    /// Remove a page object by index from a page's content stream.
    ///
    /// Objects inserted via content generation are wrapped in `q … Q` groups;
    /// `object_index` identifies which group to remove.
    ///
    /// Corresponds to `FPDFPageObj_Destroy` / `FPDF_RemovePageObject`.
    pub fn remove_page_object(
        &mut self,
        page_index: usize,
        object_index: usize,
    ) -> Result<(), EditError> {
        self.inner.remove_page_object(page_index, object_index)
    }
}

impl<'lib> Document<'lib> {
    /// Convert this document into an editable document.
    ///
    /// This consumes the `Document`. The returned `EditableDocument` can be
    /// used to add/remove pages, annotations, form field values, and then
    /// saved back to PDF bytes.
    pub fn into_editable(self) -> EditableDocument<'lib> {
        EditableDocument::from_document(self)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use rpdfium_core::{OpenOptions, ParsingMode, Rect};
    use rpdfium_edit::annotation_edit::AnnotationBuilder;
    use rpdfium_parser::store::ObjectStore;

    #[test]
    fn new_blank_and_save() {
        let lib = Library::new();
        let mut doc = EditableDocument::new_blank(&lib);
        doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));

        let bytes = doc.save().unwrap();
        assert!(bytes.starts_with(b"%PDF-"));

        // Re-open and verify
        let store = ObjectStore::open(bytes, ParsingMode::Lenient).unwrap();
        let page_ids = rpdfium_page::collect_page_ids(&store).unwrap();
        assert_eq!(page_ids.len(), 1);
    }

    #[test]
    fn add_annotation_and_save() {
        let lib = Library::new();
        let mut doc = EditableDocument::new_blank(&lib);
        doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));

        let spec =
            AnnotationBuilder::new(rpdfium_doc::AnnotationType::Text, [10.0, 10.0, 50.0, 50.0])
                .contents("Test note")
                .build();
        doc.add_annotation(0, spec).unwrap();

        let bytes = doc.save().unwrap();
        let store = ObjectStore::open(bytes, ParsingMode::Lenient).unwrap();
        let page_ids = rpdfium_page::collect_page_ids(&store).unwrap();
        assert_eq!(page_ids.len(), 1);
    }

    #[test]
    fn open_and_convert_to_editable() {
        let lib = Library::new();

        // First create a PDF
        let mut doc = EditableDocument::new_blank(&lib);
        doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
        let bytes = doc.save().unwrap();

        // Open it and convert to editable
        let opened = Document::open(&lib, bytes, &OpenOptions::default()).unwrap();
        let initial_count = opened.page_count();
        let mut editable = opened.into_editable();

        // Add a page
        editable.add_page(Rect::new(0.0, 0.0, 595.0, 842.0));
        assert_eq!(editable.page_count(), initial_count as usize + 1);

        // Save again
        let bytes2 = editable.save().unwrap();
        let store = ObjectStore::open(bytes2, ParsingMode::Lenient).unwrap();
        let page_ids = rpdfium_page::collect_page_ids(&store).unwrap();
        assert_eq!(page_ids.len(), 2);
    }

    #[test]
    fn page_count_matches() {
        let lib = Library::new();
        let mut doc = EditableDocument::new_blank(&lib);
        assert_eq!(doc.page_count(), 0);

        doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
        assert_eq!(doc.page_count(), 1);

        doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
        assert_eq!(doc.page_count(), 2);

        doc.delete_page(0).unwrap();
        assert_eq!(doc.page_count(), 1);
    }

    #[test]
    fn save_with_custom_options() {
        let lib = Library::new();
        let mut doc = EditableDocument::new_blank(&lib);
        doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));

        let options = WriteOptions {
            version: rpdfium_parser::header::PdfVersion::new(2, 0),
            ..Default::default()
        };
        let bytes = doc.save_with_options(&options).unwrap();
        assert!(bytes.starts_with(b"%PDF-2.0"));
    }

    #[test]
    fn incremental_save_round_trip() {
        let lib = Library::new();
        let mut doc = EditableDocument::new_blank(&lib);
        doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
        let original = doc.save().unwrap();

        // Open and edit
        let opened = Document::open(&lib, original.clone(), &OpenOptions::default()).unwrap();
        let mut editable = opened.into_editable();
        editable.add_page(Rect::new(0.0, 0.0, 595.0, 842.0));

        let saved = editable.save_incremental(&original).unwrap();
        // Incremental save should start with original data
        assert!(saved.starts_with(&original));

        // Re-open incremental result
        let store = ObjectStore::open(saved, ParsingMode::Lenient).unwrap();
        let page_ids = rpdfium_page::collect_page_ids(&store).unwrap();
        assert_eq!(page_ids.len(), 2);
    }
}