Document Management

Document Management

The Document class is the entry point for almost every operation in Aspose.PDF FOSS for Python: creating a new PDF, loading an existing one, editing its pages, and writing the result back out. This guide walks through the document lifecycle, page-collection operations, optimization, multi-file workflows, encryption, and the exceptions you should expect to handle.


Document Lifecycle: Create, Open, and Save

Document() with no arguments creates an empty, in-memory document. Pass a file path, raw bytes, or any readable binary stream as the first argument (or call load_from() explicitly) to load an existing PDF instead. save() accepts a path or a writable binary stream such as io.BytesIO.

from aspose_pdf import Document

# Create a new, empty document and add a blank page
doc = Document()
doc.pages.add()
doc.save("hello.pdf")

# Re-open it — a path, bytes, or a binary stream all work
reopened = Document("hello.pdf")
print(reopened.page_count)   # 1

# ...or load explicitly onto an existing instance
another = Document()
another.load_from("hello.pdf")

reopened.close()
another.dispose()
doc.dispose()

save() raises FileExistsError if the destination path already exists, unless you pass overwrite=True. close() is an alias for dispose(); both are idempotent, so calling them more than once is safe.

Only PDF is implemented as a save target — this is the library’s core, fully working function. Passing an export value such as SaveFormat.PPTX or DocFormat.HTML to save() raises UnsupportedFeatureException instead of writing a mislabeled file, so a failed export is always loud rather than silent.


Managing the Page Collection

doc.pages is a PageCollection. It supports len(), iteration, and 0-based indexing (doc.pages[0]), plus add(), insert(index, page), and delete(index) for structural edits. Each Page exposes index, rect (the MediaBox), and rotation.

from aspose_pdf import Document

doc = Document()
doc.pages.add()            # page 0
doc.pages.add()            # page 1
doc.pages.insert(1, None)  # insert a blank page at index 1

print(doc.page_count)      # 3

first_page = doc.pages[0]
print(first_page.rect)     # (0, 0, 612, 792)

for page in doc.pages:
    print(page.index, page.rotation)

doc.pages.delete(1)        # remove the page we inserted
doc.save("pages_demo.pdf", overwrite=True)

pages.add(page=None) appends a blank page when called with no argument. pages.insert() clamps an out-of-range index to the nearest valid position rather than raising.


Optimizing and Compressing Documents

Document.optimize() runs image/stream deduplication, unused-object garbage collection, and stream compression in one call. Pass an OptimizationOptions instance to control which techniques run; omit it to use the standard cleanup profile.

from aspose_pdf import Document, OptimizationOptions

doc = Document("large_report.pdf")

options = OptimizationOptions()
options.remove_unused_objects = True
options.link_duplicate_streams = True
options.image_compression_quality = 60
options.subset_fonts = True

doc.optimize(options)
doc.save("large_report_optimized.pdf", overwrite=True)

optimize_resources() is an alias for optimize(). If you only want stream compression without the structural cleanup pass, call doc.compress_streams() directly.


Merging, Splitting, and Editing Files

For documents you already have open, Document.merge() appends other Document instances onto the current one:

from aspose_pdf import Document

base = Document("part1.pdf")
extra = Document("part2.pdf")

base.merge(extra)
base.save("combined.pdf", overwrite=True)

For file-to-file workflows without opening a Document yourself, the low-code Merger and Splitter plugins take a MergeOptions/SplitOptions object built from FileDataSource inputs and outputs:

from aspose_pdf import Merger, MergeOptions, Splitter, SplitOptions, FileDataSource

# Merge two files into one
merge_options = MergeOptions()
merge_options.add_input(FileDataSource("part1.pdf"))
merge_options.add_input(FileDataSource("part2.pdf"))
merge_options.add_output(FileDataSource("combined.pdf"))
Merger().process(merge_options)

# Split the result into one file per page
split_options = SplitOptions()
split_options.add_input(FileDataSource("combined.pdf"))
split_options.add_output(FileDataSource("combined_page1.pdf"))
split_options.add_output(FileDataSource("combined_page2.pdf"))
Splitter().process(split_options)

PdfFileEditor offers the same family of operations as a facade that returns True/False instead of raising, which is convenient for batch scripts:

from aspose_pdf import PdfFileEditor

with PdfFileEditor() as editor:
    ok = editor.concatenate(["part1.pdf", "part2.pdf"], "combined.pdf")
    if not ok:
        print("concatenate failed:", editor.last_exception)

    editor.extract("combined.pdf", "first_page_only.pdf", page_from=1, page_to=1)

PdfFileEditor.extract() and .insert() take 1-based page numbers, unlike PageCollection’s 0-based indexing — see Common Issues below.


Encryption and Document Security

Document.encrypt(user_password, owner_password=None, permissions=-4) encrypts the in-memory document; decrypt(password) and change_passwords(old, new_user, new_owner=None) reverse or rotate passwords. is_encrypted and permissions report the current state.

from aspose_pdf import Document
from aspose_pdf.exceptions import PdfSecurityException

doc = Document()
doc.pages.add()
doc.encrypt("user-pass", "owner-pass", permissions=-4)
doc.save("secured.pdf", overwrite=True)

try:
    Document("secured.pdf", password="wrong-pass")
except PdfSecurityException as exc:
    print("could not open:", exc)

reopened = Document("secured.pdf", password="user-pass")
print(reopened.is_encrypted)   # True
reopened.decrypt("user-pass")
reopened.save("unsecured.pdf", overwrite=True)

Opening an encrypted document with no password, or the wrong one, raises PdfSecurityException — catch this class (from aspose_pdf.exceptions) rather than assuming a load always succeeds.


Metadata, Validation, and Exception Handling

Document metadata lives on doc.info (a plain dict[str, str]), and doc.version / doc.id expose the PDF header version and trailer file-identifier. validate() (aliased as check()) reports structural integrity; repair() attempts to fix common problems such as a missing page list or an out-of-range MediaBox.

from aspose_pdf import Document, PdfLoadLimits
from aspose_pdf.exceptions import AsposePdfException, PdfIOException

doc = Document()
doc.pages.add()
doc.info["Title"] = "Quarterly Report"
doc.info["Author"] = "Reporting Bot"
doc.save("report.pdf", overwrite=True)

# Load untrusted input under an explicit resource-limit policy
safe_limits = PdfLoadLimits(max_input_bytes=50 * 1024 * 1024, max_pages=1000)

try:
    untrusted = Document("incoming.pdf", limits=safe_limits)
    if not untrusted.validate():
        untrusted.repair()
except (AsposePdfException, PdfIOException) as exc:
    print("failed to process incoming.pdf:", exc)

PdfLoadLimits bounds memory and object counts for untrusted files; call PdfLoadLimits.unlimited() to disable every limit when you fully trust the source. AsposePdfException is the base class for the whole exception hierarchy (including PdfIOException and PdfSecurityException), so a single except AsposePdfException catches any library-raised error.


Tips and Best Practices

  • Always call dispose() (or close()) on a Document when you are done with it, or use it as a short-lived local variable — the engine holds decoded page content and images in memory until disposal.
  • Pass overwrite=True to save() when re-writing a path you already created in the same run; the default is False and raises FileExistsError.
  • Prefer doc.optimize() before shipping a generated PDF — it is a single call that removes unused objects and compresses streams, and typically shrinks output size noticeably.
  • Set a PdfLoadLimits policy explicitly whenever you load PDFs from an untrusted source (uploads, email attachments, web scrapes); the defaults are generous but finite, not a security boundary you should rely on blindly.
  • Catch AsposePdfException (or a specific subclass such as PdfSecurityException) around load/save calls rather than bare Exception — it is the common base for every error the library raises.

Common Issues

IssueCauseFix
FileExistsError on save()Destination path already exists and overwrite was left at its default FalsePass save(path, overwrite=True)
UnsupportedFeatureException on save()A non-PDF save_format (e.g. SaveFormat.PPTX, DocFormat.HTML) was requestedSave as PDF — any other SaveFormat/DocFormat value raises UnsupportedFeatureException instead of writing output
PdfSecurityException: Password required for encrypted documentOpened an encrypted PDF without a password argumentPass Document(path, password="...") or call load_from(path, password="...")
Off-by-one page numbers between PageCollection and PdfFileEditordoc.pages[i] is 0-based; PdfFileEditor.extract()/.insert() page arguments are 1-basedAdd or subtract 1 when converting between the two APIs
IndexError: Page index out of range. from pages.delete()Index passed to delete() does not exist in the collectionCheck doc.page_count (or len(doc.pages)) before deleting

FAQ

Do I need a license to use Aspose.PDF FOSS for Python?

No. This is the open-source (MIT-licensed) edition; there is no license file or activation step to configure.

Can I export a Document to formats other than PDF?

Not in this release. save() only implements PDF output — passing another SaveFormat/DocFormat value raises UnsupportedFeatureException rather than producing a mislabeled file.

What is the difference between Document.merge() and the Merger plugin?

Document.merge() combines Document instances you already have open in memory. Merger (with MergeOptions and FileDataSource) is a file-to-file convenience wrapper that opens, merges, and saves for you in one call — useful for simple batch scripts that never need the intermediate Document object.

Why does pages.insert() never raise for an out-of-range index?

PageCollection.insert() clamps the index into the valid range (negative values become 0, values past the end become len(doc.pages)) instead of raising, so an insert never fails purely because of the index value.

How do I safely load a PDF from an untrusted source?

Construct a PdfLoadLimits with explicit caps (max_input_bytes, max_pages, max_objects, and so on) and pass it as the limits= argument to Document(...) or load_from(). Every field defaults to a finite value already, but tightening them to your expected input size reduces the resources a malformed file can consume.


API Reference Summary

Class / MethodDescription
Document() / Document.load_from()Create an empty document or load one from a path, bytes, or a binary stream
Document.save()Write the document to a path or writable stream (PDF only)
Document.dispose() / Document.close()Release engine resources; idempotent
Document.pagesThe document’s PageCollection
Document.infoDocument metadata as a dict[str, str]
Document.optimize() / Document.optimize_resources() / Document.compress_streams()Remove unused resources and compress streams
Document.merge()Append other Document instances onto this one
Document.encrypt() / Document.decrypt() / Document.change_passwords()Apply, remove, or rotate document passwords
Document.validate() / Document.check() / Document.repair()Check and attempt to fix structural integrity
PageCollection.add() / .insert() / .delete() / .item()Structural page-collection edits (0-based)
Page.rect / Page.rotation / Page.indexPer-page geometry and position
OptimizationOptionsFine-grained flags consumed by Document.optimize()
MergeOptions / MergerFile-to-file merge plugin
SplitOptions / SplitterFile-to-file one-page-per-output split plugin
FileDataSourceFile-backed input/output for the plugin APIs
PdfFileEditorFacade for concatenate(), extract(), insert(), delete(), append() (1-based pages)
PdfLoadLimitsImmutable resource-limit policy for untrusted input
AsposePdfExceptionBase class for every exception the library raises
PdfSecurityExceptionRaised for missing/incorrect passwords and permission errors
PdfIOExceptionRaised for I/O errors during PDF processing

See Also