Skip to content
Set XMP Metadata

Set XMP Metadata

Writing XMP is a round trip: parse the existing packet (or start a new one), set the models you care about, marshal back to bytes, and store the result as the catalog’s metadata stream. Document.SetPdfInfo handles the common case, mapping a document information dictionary into the pdf: namespace so the two systems agree.

Doing it

xmpDoc := xmputil.NewDocument()

pdfInfo, err := reader.GetPdfInfo()
if err != nil {
    return err
}

err = xmpDoc.SetPdfInfo(&xmputil.PdfInfoOptions{
    InfoDict:   pdfInfo.ToPdfObject(),
    PdfVersion: reader.PdfVersion().String(),
    Copyright:  "Copyright Example",
    Overwrite:  true,
})
if err != nil {
    return err
}

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

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

To edit an existing packet instead of replacing it, load it first with GetCatalogMetadata, core.GetStream and xmputil.LoadDocument, and only fall back to NewDocument when the catalog has no metadata. Replacing a packet that was already there throws away every namespace it held, not just pdf:.

Overwrite decides how SetPdfInfo treats the pdf: model that is already in the packet. With true, the model is cleared first, so the output holds exactly what you passed. With false, your values are merged over it and anything you left unset survives - which is what you want when the packet carries fields the info dictionary does not.

Passing nil as the encoder to core.MakeStream writes the XML uncompressed, so tools that scan for the XMP packet can find it without decoding the stream. MarshalIndent is likewise for readability; Marshal produces the same content without the whitespace.

Limitations

SetPdfInfo reads a fixed set of keys out of InfoDict: Title, Author, Subject, Keywords, Creator, Producer, Trapped, CreationDate and ModDate. Custom info keys are ignored - see set custom XMP metadata for those.

Marked only ever sets. The code applies it when the option is true, so passing Marked: false leaves an existing pdf:Marked value alone rather than clearing it. The same goes for PdfVersion and Copyright, where an empty string is skipped. Use Overwrite: true if you need a field gone.

SetPdfInfo finishes by syncing the pdf: model into the neighbouring namespaces, which copies title, author, subject and copyright into dc: and the dates into xmp:. That sync only fills empty targets: an existing dc:title is left as it was, even with Overwrite: true, because the overwrite applies to the pdf: model and not to dc:. Set the dc: model directly if the two have to agree.

A malformed CreationDate or ModDate string makes SetPdfInfo return an error rather than skipping the field, and Trapped values other than True or False are recorded as trapped.

SetCatalogMetadata returns an error unless the object is a stream, and passing nil removes /Metadata from the catalog entirely.

Nothing here updates the document’s own /Info dictionary. If you changed the values rather than just copying them across, write them back with PdfWriter.SetDocInfo too.

Run the example

The example copies the input to a writer, loads any existing XMP packet, fills in a creation date if the input has none, sets the author and modification date, then writes the pdf: model with Overwrite: true and saves the packet to the output file.

git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/metadata
go run pdf_set_xmp_pdf_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