Full working example available on GitHub: manage-xmp-in-psd-and-ai-files-python
Introduction
A marketing team drops 400 PSD files into your asset platform. The upload works. Search does not, because none of the files carry keywords, half are missing a copyright notice, and the designer names live only in a spreadsheet somewhere. The fix is not a bigger spreadsheet. XMP management is a GroupDocs.Metadata capability for Python via .NET that reads and writes the metadata packet embedded in Photoshop PSD and Illustrator AI files, which means ownership and search data can live in the files themselves.
XMP is an XML packet inside a binary container, organized into schemes: Dublin Core for the fields every system understands, the Photoshop scheme for editorial context, XmpBasic for tool identity. Parsing a PSD by hand to reach that packet is genuinely hard. With the Metadata class it is three attribute lookups, and the identical code serves AI files.
This tutorial walks the full round trip in four steps: snapshot the whole packet, read the schemes that matter, write copyright and creator, and tag keywords for search. Every snippet comes from a runnable repository that asserts the written values persist.
Prerequisites
Before starting, ensure you have:
- Python 3 with pip
- GroupDocs.Metadata for Python via .NET (the repository pins version 26.5)
- A PSD or AI file to experiment with
Installation
pip install groupdocs-metadata-net==26.5
Step 1 - Snapshot the Entire XMP Packet
Start by seeing everything the file carries. The snapshot walks the root packet, each registered scheme, and finally sweeps the property tree for anything nonstandard, collecting it all into one flat dict.
result = {}
def put(props, prop):
value = (str(prop.interpreted_value) if prop.interpreted_value is not None
else (str(prop.value) if prop.value is not None else ""))
props[prop.name] = value
with Metadata("campaign-hero.psd") as metadata:
root = metadata.get_root_package()
xmp = getattr(root, "xmp_package", None)
if xmp is not None:
for p in xmp: # root packet properties
put(result, p)
schemes = xmp.schemes
for scheme in (schemes.dublin_core, schemes.xmp_basic, schemes.photoshop,
schemes.camera_raw, schemes.paged_text,
schemes.xmp_dynamic_media, schemes.xmp_media_management):
if scheme is None:
continue
for p in scheme:
put(result, p)
for p in metadata.find_properties(lambda p: p.name is not None):
if p.name not in result: # catch custom packets
put(result, p)
Key points:
interpreted_valuefirst: dates and enumerations arrive human-readable instead of raw.- Seven schemes plus a sweep: the trailing
find_propertiespass catches vendor packets the named schemes miss. - One file open: the whole snapshot costs a single
Metadatacontext, which matters in bulk ingestion.
рџ’Ў Tip: index this dict at ingestion time and most later metadata questions become dictionary lookups instead of file reads.
Which XMP scheme should my integration read first?
Start with Dublin Core. Its nine dc: fields carry the title, creator, rights, and subject values that most DAM systems, search indexes, and licensing checks agree on, and both PSD and AI files expose them identically. Read the Photoshop scheme second for editorial context such as City, Credit, and DateCreated. Save the full-packet sweep for ingestion jobs that must capture everything.
Step 2 - Read the Schemes That Answer Real Questions
For request-time code, scope the read to one scheme. Dublin Core answers ownership and search questions:
dc_fields = {}
with Metadata("campaign-hero.psd") as metadata:
xmp = getattr(metadata.get_root_package(), "xmp_package", None)
dc = xmp.schemes.dublin_core if xmp is not None else None
if dc is not None:
for p in dc:
dc_fields[p.name] = (str(p.interpreted_value)
if p.interpreted_value is not None else
str(p.value) if p.value is not None else "")
print(dc_fields.get("dc:rights", "<no rights recorded>"))
The Photoshop scheme works the same way through typed properties: ps.color_mode, ps.icc_profile, ps.city, ps.country, ps.date_created, ps.caption_writer, ps.credit, and ps.source, each read with a None guard. Those are the fields Bridge, Lightroom, and DAM search filters key on for Adobe files.
Note what happens on files without XMP: the guards produce an empty dict, not an exception. Freshly exported assets make this case routine, so keep that behavior in your integration.
The same three lookups work on Illustrator files. Swap campaign-hero.psd for brand-mark.ai and nothing else changes, which is what makes a single code path realistic for mixed Adobe archives. In practice a fresh AI export tends to arrive with fewer populated schemes than a Photoshop save, so the empty-dict path gets exercised more often there.
Step 3 - Write Copyright and Creator
Now the write path. Ownership stamping touches three fields so every reader sees the same identity: dc:rights for the legal notice, dc:creator as an ordered list, and xmp:CreatorTool for tools that read the XmpBasic scheme instead of Dublin Core. I once lost an afternoon to a licensing banner showing “Unknown author” on assets the designers swore were tagged; the values sat in dc:creator while the tool read only xmp:CreatorTool. Writing both ended that class of bug.
with Metadata("campaign-hero.psd") as metadata:
root = metadata.get_root_package()
xmp = getattr(root, "xmp_package", None)
if xmp is None: # file has no XMP at all
root.xmp_package = XmpPacketWrapper()
xmp = root.xmp_package
if xmp.schemes.dublin_core is None:
xmp.schemes.dublin_core = XmpDublinCorePackage()
dc = xmp.schemes.dublin_core
dc.set_rights("(C) 2026 GroupDocs Sample")
dc.set("dc:creator", XmpArray.from_(["Digital Asset Team"],
XmpArrayType.ORDERED))
if xmp.schemes.xmp_basic is None:
xmp.schemes.xmp_basic = XmpBasicPackage()
xmp.schemes.xmp_basic.creator_tool = "Digital Asset Team"
metadata.save("campaign-hero-stamped.psd")
Key points:
- Guards create missing layers:
XmpPacketWrapperandXmpDublinCorePackageare created on demand, so the write works on XMP-less files. - ORDERED array for creators: author order carries meaning, so the creator list uses an ordered
XmpArray. - Save to a new path: the source file stays untouched, which is the right default for export steps.
Step 4 - Tag Keywords for Search
dc:subject is the keyword bag DAM search indexes. The write replaces the whole bag in one call:
with Metadata("campaign-hero.psd") as metadata:
root = metadata.get_root_package()
xmp = getattr(root, "xmp_package", None)
if xmp is None:
root.xmp_package = XmpPacketWrapper()
xmp = root.xmp_package
if xmp.schemes.dublin_core is None:
xmp.schemes.dublin_core = XmpDublinCorePackage()
xmp.schemes.dublin_core.set(
"dc:subject",
XmpArray.from_(["landscape", "sunset", "commercial"],
XmpArrayType.UNORDERED))
metadata.save("campaign-hero-tagged.psd")
Keywords use an UNORDERED array because order means nothing to an indexer. And since set replaces the existing bag, read the current keywords first and merge in Python when you need additive tagging rather than replacement.
To verify any write, re-run the Step 2 reader against the output file. The repository automates exactly that: it re-reads its outputs and asserts the copyright string and the first keyword survive in the saved bytes.
Real-World Applications
DAM ingestion
Run the Step 1 snapshot on every incoming file and store the dict alongside the asset record. Search, dedup, and rights checks then run against your database instead of re-opening binary files. The snapshot of the repository’s small sample PSD already returns a healthy stack of properties in one pass, and the same call keeps its shape when the input becomes a folder of thousands.
Licensing enforcement
Before an asset ships to a client portal, require a non-empty dc:rights. Files that fail get the Step 3 stamping treatment automatically, so nothing leaves without a notice.
Batch retagging
When taxonomy changes, read each file’s dc:subject, map old terms to new ones in Python, and write the merged bag back with Step 4. Both PSD and AI archives take the same loop. No Photoshop seat required.
Best Practices and Tips
-
Treat empty as normal: files without XMP are routine, not errors; the early-return pattern keeps pipelines flowing.
-
Merge before writing keywords:
setreplacesdc:subject, so additive tagging means read, extend, write. -
Write identity to both schemes: pairing
dc:creatorwithxmp:CreatorToolkeeps Dublin Core readers and XmpBasic readers in agreement. -
Verify writes with a read-back: a re-read after save is cheap and catches container surprises immediately.
-
License for production: evaluation mode runs everything shown here; use a license before stamping real client assets.
Conclusion
Reading and writing XMP in Adobe files reduces to three moves: resolve the packet through get_root_package(), guard the scheme you need, and read or write typed values. With those moves you built a full round trip in this tutorial, from packet snapshot to scheme reads to copyright stamping and keyword tagging, with the same code serving PSD and AI files.
Ready to implement this in your project? Here are some next steps:
- Read the Working with XMP metadata documentation for updating and removing packets
- Follow the integration-focused use case guide built on the same repository
- Clone the sample project and run it against your own assets
Additional Resources
- GroupDocs.Metadata Documentation
- API Reference
- Sample Projects on GitHub
- GroupDocs.Metadata Blog Category
Questions about your XMP workflow? Ask on the support forum.