Skip to content

Add lossless page reordering that preserves the original object graph #531

Description

@bzsanti

Problem / use case

operations::reorder currently creates a new Document and converts each parsed page before saving. This can preserve page content and resources, but it does not preserve the complete original object graph. Reordering pages must not silently discard or rewrite unrelated document data such as annotations, outlines, named destinations, AcroForm references, structure trees, attachments, or metadata.

A page-order change should be written as an incremental revision, retaining the original indirect objects and bytes wherever possible. Objects that define the page tree may be replaced in the new revision when required to keep /Kids, /Count, /Parent, and inherited page attributes coherent. Unsupported inputs must return an explicit error instead of producing a degraded document.

This issue is phase 1 of the work and deliberately supports only unencrypted, unsigned documents. Certification-signature permission enforcement is tracked separately in #532.

Proposed API

pub fn reorder_pdf_pages_lossless<P: AsRef<Path>, Q: AsRef<Path>>(
    input_path: P,
    output_path: Q,
    page_order: &[usize],
) -> OperationResult<()>;

The order must be an exact permutation of every source-page index: identical length, no duplicates, no omissions, and no out-of-range values. Separate extraction or duplication APIs can cover subsets and repeated pages.

Phase 1 expected behaviour

  • Preserve the original file bytes and append the page-order change as an incremental revision.
  • Preserve the original indirect page-object identifiers.
  • Preserve all unrelated indirect objects and catalog references.
  • Replace only the catalog/page-tree/page dictionaries required to maintain a valid tree.
  • Keep /Kids, /Count, and every page's /Parent mutually consistent.
  • Preserve the effective values of inherited page attributes when pages move between /Pages nodes, materializing inherited values when necessary.
  • Support page trees with multiple nested /Pages nodes.
  • Accept source files using classic xref tables, xref streams, or object streams; emit an incremental xref section compatible with the source.
  • Reject encrypted documents explicitly.
  • Reject documents containing signatures explicitly until Enforce DocMDP certification permissions for incremental structural edits #532 is implemented.
  • Write to a temporary file in the destination directory and replace the destination atomically only after parsing and validation succeed.
  • Reopen and validate the completed output before replacing the destination.

“Lossless” in this API means that page contents, object identities, unrelated objects, and prior bytes are retained. It does not mean that page-tree dictionaries remain byte-for-byte unchanged: the new incremental revision must replace the minimum set of structural dictionaries needed for a valid reordered page tree.

Validation and failure behaviour

  • Reject an empty order unless the source document itself has zero pages.
  • Reject duplicate, omitted, and out-of-range page indices.
  • Reject malformed, cyclic, or unsupported page trees.
  • Reject inputs whose effective inherited page attributes cannot be preserved safely.
  • Never leave a partial destination file or corrupt an existing destination.

Tests

Add integration fixtures covering:

  • flat and nested page trees;
  • pages that inherit /Resources, /MediaBox, /CropBox, and /Rotate from different ancestors;
  • annotations and AcroForm widgets;
  • outlines and named destinations;
  • tagged PDFs and structure-tree references;
  • attachments and metadata;
  • classic xref tables and xref streams;
  • objects originally stored in object streams;
  • documents with existing incremental revisions;
  • malformed permutations and page trees;
  • explicit rejection of encrypted and signed documents;
  • atomic destination replacement on success and preservation of the previous destination on failure.

Verify that the requested page order is the only semantic change, every source page retains its indirect object identifier, unrelated objects remain reachable, inherited page behaviour is unchanged, and all pre-existing bytes remain as the prefix of the output.

Relevant bindings

Core only.

Follow-up

Alternatives considered

Rebuilding every page through the high-level Document writer. This cannot guarantee preservation of the complete source object graph and can silently alter unrelated content.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions