How to Work with PDF Data in Python
Aspose.PDF FOSS for Python represents a PDF document’s XMP metadata as an in-memory data model built from XmpPacket, XmpField, XmpArray, XmpStruct, and XmpProperty objects, with NamespaceProvider resolving namespace prefixes to URIs. This lets you read, build, and rewrite structured metadata — titles, dates, keyword lists, custom fields — without hand-writing RDF/XML. The library is pure Python and is installed from PyPI with pip.
Step-by-Step Guide
Step 1: Install the Package
Install the Aspose.PDF FOSS package from PyPI:
pip install aspose-pdf-foss-for-pythonVerify the installation by importing XmpPacket and printing a confirmation message:
from aspose_pdf import XmpPacket
print("aspose-pdf-foss-for-python is ready.")Step 2: Import Required Classes
Import the XMP data model classes and the namespace resolver:
from aspose_pdf import (
NamespaceProvider,
XmpArray,
XmpField,
XmpPacket,
XmpProperty,
XmpStruct,
)Step 3: Create a Packet and Set Simple Properties
XmpPacket() starts with an empty field list. set_value(prefix, name, value, uri=...) sets or replaces a simple property, and get(prefix_or_uri, name) reads it back as an XmpField; get accepts either the registered prefix or the full namespace URI:
from aspose_pdf import XmpPacket
packet = XmpPacket()
packet.set_value("dc", "title", "Quarterly Report")
packet.set_value(
"dc", "creator", "Automation Pipeline",
uri="http://purl.org/dc/elements/1.1/",
)
title_field = packet.get("dc", "title")
print(title_field.value) # "Quarterly Report"Step 4: Store Typed Values
XmpPacket provides typed convenience accessors that convert to and from the plain-text form XMP stores internally: set_date/get_date, set_int/get_int, set_real/get_real, and set_bool/get_bool. Each getter returns None when the property is absent instead of raising:
from datetime import datetime
packet.set_date("xmp", "CreateDate", datetime(2026, 7, 29, 9, 30))
packet.set_int("pdf", "PageCount", 42)
packet.set_real("custom", "ConfidenceScore", 0.97, uri="https://example.com/ns/custom/1.0/")
packet.set_bool("custom", "IsFinal", True, uri="https://example.com/ns/custom/1.0/")
print(packet.get_date("xmp", "CreateDate"))
print(packet.get_int("pdf", "PageCount"))
print(packet.get_real("custom", "ConfidenceScore"))
print(packet.get_bool("custom", "IsFinal"))Step 5: Store Localized Text and Ordered Arrays
set_localized_text writes a language-alternative (rdf:Alt) property such as dc:title in a specific language, defaulting to "x-default". set_array writes an ordered (Seq), unordered (Bag), or alternative (Alt) array from a plain list of values, and get_array reads it back as a list of strings:
packet.set_localized_text("dc", "description", "Quarterly summary", lang="en")
packet.set_localized_text(
"dc", "description", "Resumen trimestral",
uri="http://purl.org/dc/elements/1.1/", lang="es",
)
print(packet.get_localized_text("dc", "description", lang="es"))
packet.set_array("dc", "subject", ["finance", "quarterly", "internal"], kind="Bag")
print(packet.get_array("dc", "subject"))Step 6: Build Structured Values with XmpStruct
Some XMP properties (dimension records, history entries) are structured values rather than simple text. Build one with XmpStruct, add member XmpField objects with add, and read a member back by name with get, then attach the struct to the packet with add:
dimensions = XmpStruct()
dimensions.add(XmpField(prefix="stDim", name="w", value="612"))
dimensions.add(XmpField(prefix="stDim", name="h", value="792"))
dimensions.add(XmpField(prefix="stDim", name="unit", value="pt"))
print(dimensions.get("w").value) # "612"
packet.add(XmpField(prefix="xmpTPg", name="MaxPageSize", value=dimensions))Step 7: Register Custom XMP Namespaces
NamespaceProvider is preloaded with the standard XMP namespaces (Dublin Core, Adobe XMP, PDF, and others). Call register(prefix, uri) to add a custom mapping — it returns the provider itself so calls can be chained — then attach the provider to a packet so serialization can resolve the custom prefix to its URI:
from aspose_pdf import NamespaceProvider, XmpPacket
provider = NamespaceProvider()
provider.register("custom", "https://example.com/ns/custom/1.0/")
packet = XmpPacket(namespace_provider=provider)
packet.set_value("custom", "batch_id", "run-2026-07-29")Step 8: Parse and Serialize a Packet
XmpPacket.parse(data, provider=...) reads raw XMP packet bytes or text into an XmpPacket. serialize() and to_bytes() both render the packet back to XMP packet bytes:
xmp_bytes = packet.to_bytes()
restored = XmpPacket.parse(xmp_bytes)
print(restored.get("dc", "title").value)Step 9: Attach XMP Metadata to a PDF Document
A loaded Document’s xmp_metadata property returns the catalog’s XMP packet (an empty XmpPacket when the PDF has none). Edit it with the same XmpPacket methods, then assign it back so the change is written on save:
from aspose_pdf import Document
with Document("report.pdf") as document:
xmp = document.xmp_metadata
xmp.set_value("dc", "title", "Quarterly Report")
xmp.set_date("xmp", "ModifyDate", datetime.now())
document.xmp_metadata = xmp
document.save("report-updated.pdf", overwrite=True)Common Issues and Fixes
get() or a typed getter returns None even though I just set the property
Confirm the prefix (or URI) argument to get/get_int/get_date/etc. matches exactly what was passed to the matching set_* call — get matches on both the field’s prefix and its namespace URI, but a mismatched custom prefix on one call and a URI on the other will miss.
Custom namespace prefix produces incomplete output when serialized
A prefix that is not one of the standard XMP namespaces (dc, xmp, pdf, and similar) needs either an explicit uri= argument on every set_* call, or a NamespaceProvider with that prefix registered and attached to the packet via XmpPacket(namespace_provider=provider) before calling serialize()/to_bytes().
get_bool, get_int, or get_real returns None for a value I know is set
These typed getters return None when the stored text does not parse as the expected type — for example, get_int on a property whose value was set with set_value as free-form text rather than set_int. Use the matching typed setter (set_int, set_real, set_bool) so the getter’s parsing logic matches the write path.
XmpPacket.parse raises ValueError on a packet from an untrusted source
parse rejects any <!DOCTYPE or <!ENTITY declaration as a hostile-input guard against XXE and billion-laughs attacks. A legitimate XMP packet never needs a DTD; treat the exception as a signal that the source stream is malformed or unsafe rather than working around the guard.
get_array returns None instead of a list
get_array returns None when the named property either does not exist or was not stored as an XmpArray (for example, if it was set with set_value instead of set_array). Use set_array when writing the property so the stored value matches the shape get_array expects.
Frequently Asked Questions
What is the difference between set_value and the typed setters like set_int?
set_value stores whatever value you pass as-is. The typed setters (set_date, set_int, set_real, set_bool) convert their input to the plain-text form XMP uses internally and pair with a matching typed getter that parses it back, so use them when you need round-trip type safety instead of raw strings.
How do XmpArray kinds (Bag, Seq, Alt) differ?
Bag is an unordered set of values, Seq is an ordered list, and Alt holds alternative values (most often language alternatives, as used by set_localized_text). Pass the desired kind to set_array(..., kind="Bag") or build an XmpArray(kind=...) directly.
Can I read the raw list of properties on a packet without knowing their names in advance?
Yes — iterate packet.fields, which holds every XmpField, XmpArray, and XmpProperty added to the packet in insertion order.
Do I need a NamespaceProvider for the standard XMP namespaces?
No. dc, xmp, pdf, and the other standard prefixes resolve automatically. A NamespaceProvider is only needed for custom prefixes you invent yourself.
How do XmpProperty qualifiers differ from a plain XmpField?
XmpProperty wraps a base XmpField together with a list of qualifier fields (added with add_qualifier), used for the rarer case where a property itself needs additional RDF metadata attached to its value, beyond the xml:lang qualifier that XmpField.language already covers.