Skip to content
Set Custom XMP Metadata

Set Custom XMP Metadata

xmputil has typed setters for pdf:, xmpMM:, pdfaid: and pdfuaid:. For any other namespace, take the underlying document with GetGoXmpDocument, get the model from the matching go-xmp package, set its fields, and marshal as usual. The example does this for xmpRights:, the rights management namespace.

FindModel and MakeModel are the pair to know. FindModel returns nil if the packet has no node for that namespace; MakeModel returns the existing model or creates and attaches an empty one. Use MakeModel when writing.

Doing it

goXmpDoc := xmpDoc.GetGoXmpDocument()

rights, err := xmprights.MakeModel(goXmpDoc)
if err != nil {
    return err
}

rights.Certificate = "56c69b3eaf10cff5c3bd8932f0169b"
rights.UsageTerms = xmp.NewAltString("My Custom Usage Terms")
rights.Owner = xmp.NewStringArray("Custom Owner")
rights.WebStatement = "My Custom Web Statement"

if err := rights.SyncToXMP(goXmpDoc); err != nil {
    return err
}

data, err := xmpDoc.MarshalIndent("", "\t")
if err != nil {
    return err
}

metadataStream, err := core.MakeStream(data, nil)
if err != nil {
    return err
}
return pdfWriter.SetCatalogMetadata(metadataStream)

The xmputil.Document and the go-xmp document are the same object, so edits made through the unwrapped handle are picked up by xmpDoc.Marshal and MarshalIndent. There is no write-back step.

Field types come from the xmp package rather than being plain strings. Multi-value properties are built with constructors: xmp.NewAltString for a language alternative such as UsageTerms, xmp.NewStringArray for an unordered bag such as Owner, xmp.NewStringList for an ordered sequence. Assigning a bare Go string to one of those fields will not compile.

SyncToXMP is what mirrors a model’s properties into related namespaces. It is a no-op for xmpRights, but meaningful elsewhere: pdf.PDFInfo.SyncToXMP copies title, author, subject and copyright into dc: and the creation and modification dates into xmp:. Calling it after you set fields keeps the packet internally consistent, and costs nothing when the model does not need it.

Limitations

Cross-namespace sync only fills blanks. pdf.PDFInfo.SyncToXMP copies into dc: and xmp: only where the target property is empty, so an existing dc:title is never overwritten from pdf:Title. If those have to agree, set the dc: model yourself.

Marshaling a document loaded from a file rewrites the whole packet. Namespaces go-xmp does not model are still preserved as nodes, but formatting and property order will not match the original bytes, which matters if something downstream is comparing packets.

core.MakeStream with a nil encoder writes the XML uncompressed, which is what you want for a metadata stream. SetCatalogMetadata rejects anything that is not a stream, and clears /Metadata if you pass nil.

Setting XMP does not update the /Info dictionary, and the rights namespace has no /Info equivalent at all, so there is nothing to mirror in that direction here.

Run the example

The example copies the input into a writer, loads the existing XMP packet or starts a new one, fills in the four xmpRights properties, and writes the packet to the output file. Read it back with the get custom XMP metadata example.

git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/metadata
go run pdf_set_custom_xmp_metadata.go input.pdf output.pdf

If this is your first time using UniPDF, follow the getting started guide to create an API key and set up your development environment.

View the full source
Last updated on