You have ten thousand contract PDFs from a dozen different generators, and legal wants every one of them to carry the right Author, a corrected Producer string, and a reading mode that opens the bookmark panel on launch. The naive fix is to load each file, re-lay the pages, and write a fresh document. Do that and you have just thrown away every existing object number, the incremental-update history, any digital signature, and the carefully tuned xref the original tool emitted. The pages look identical and the file is, structurally, a stranger. For a metadata edit that is the wrong trade entirely.
The right move is to treat the loaded document as an object graph you mutate in place: reach into the Info dictionary, the /Metadata stream, and the Catalog, change the few entries you care about, and write the result back. HotPDF, the native VCL PDF component for Delphi and C++Builder, exposes exactly that surface through its loaded-document write API. This article is about using it correctly, and about the one mistake almost everyone makes: editing the Info dictionary and forgetting that a second copy of the same metadata lives in XMP.
Two places store the same metadata, and they disagree
PDF carries document information in two parallel locations, and this is the root of most "I changed the title but Acrobat still shows the old one" tickets. The first is the document information dictionary, the classic /Info object with /Title, /Author, /Subject, /Keywords, /Creator, and /Producer keys, defined in ISO 32000-1 §14.3.3. The second is an XMP packet, an XML document stored as a stream hanging off the Catalog under /Metadata, defined in §14.3.2 and built on the Adobe XMP data model.
Both can hold a title. Nothing in the spec forces them to agree. Modern viewers and most PDF/A validators prefer the XMP packet when it is present and fall back to the Info dictionary when it is not. So if you update only /Info — which is what the great majority of "set PDF metadata" code does — a reader that trusts XMP will keep showing the stale value, and a PDF/A checker will flag the mismatch. The correct operation on any file that already has an XMP packet is a double write: change the Info entry and regenerate the XMP, so the two stay consistent. HotPDF gives you both halves; the discipline of using them together is on you.
Editing the Info dictionary
The Info-side helpers are thin and predictable. SetLoadedTitle, SetLoadedAuthor, SetLoadedSubject, SetLoadedKeywords, SetLoadedCreator, and SetLoadedProducer each take a single AnsiString and write the corresponding key into the loaded Info dictionary, replacing the value if the key exists and adding it if it does not. To strip a key entirely — say a leaky /Creator that names your internal tooling — call RemoveLoadedInfoKey with the bare key name. None of these touch XMP; they operate purely on the /Info object that LoadFromFile located when it parsed the file.
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('contract-in.pdf', '') > 0 then
begin
Pdf.SetLoadedTitle('Master Services Agreement 2026');
Pdf.SetLoadedAuthor('Legal Department');
Pdf.SetLoadedSubject('Executed contract, retention 7 years');
Pdf.SetLoadedKeywords('contract; MSA; 2026; executed');
Pdf.SetLoadedProducer('Acme Document Pipeline');
Pdf.RemoveLoadedInfoKey('Creator'); // drop the originating tool name
Pdf.SaveLoadedDocument('contract-out.pdf');
end;
finally
Pdf.Free;
end;
end;
One detail to keep honest: these take AnsiString. For ASCII titles that is a non-issue, but PDF text strings that need non-Latin characters must be encoded as the spec requires — UTF-16BE with a byte-order mark, or PDFDocEncoding — before you hand them over. The library writes the bytes you give it into a string object; it does not guess an encoding for you. If your titles are plain English, ignore this. If they carry accented or CJK characters, encode deliberately and test in a real viewer.
Rewriting the XMP packet
SetLoadedXMPMetadata is the other half of the double write. Pass it the full XMP packet as an AnsiString and it does one of two things: if the Catalog already references a /Metadata stream, it replaces that stream's content in place, keeping the same object number; if there is no metadata stream, it creates one, marks it /Type /Metadata and /Subtype /XML, allocates an object number, and links it from the Catalog. Either way you end up with a valid metadata object that viewers will read.
You supply the XML, which means you control the schema — dc:title, dc:creator, xmp:CreatorTool, and so on. That is power and responsibility in one: the library does not parse or validate your packet, and it writes the bytes uncompressed, with no stream filter applied. A malformed packet will sail through the call and surface as a broken-metadata complaint later. Build the XML carefully, and mirror exactly the values you wrote into the Info dictionary so the two views never contradict each other.
const
XMP_TEMPLATE =
'<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>' +
'<x:xmpmeta xmlns:x="adobe:ns:meta/">' +
'<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">' +
'<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">' +
'<dc:title><rdf:Alt><rdf:li xml:lang="x-default">%s</rdf:li></rdf:Alt></dc:title>' +
'<dc:creator><rdf:Seq><rdf:li>%s</rdf:li></rdf:Seq></dc:creator>' +
'</rdf:Description></rdf:RDF></x:xmpmeta><?xpacket end="w"?>';
begin
// After setting the Info dictionary, mirror the same values into XMP:
Pdf.SetLoadedTitle('Master Services Agreement 2026');
Pdf.SetLoadedAuthor('Legal Department');
Pdf.SetLoadedXMPMetadata(
AnsiString(Format(XMP_TEMPLATE,
['Master Services Agreement 2026', 'Legal Department'])));
Pdf.SaveLoadedDocument('contract-out.pdf');
end;
That ordering — Info first, XMP second, then save — is the pattern to internalize. The two calls are independent; the consistency only exists because you fed them the same strings. Skip the XMP call on a file that has an XMP packet and you are back to the silent-staleness bug this whole section exists to prevent.

Steering how the viewer opens the file
Three Catalog entries decide what a reader sees the instant the document opens, and all three are one-line edits on the loaded graph. SetLoadedPageMode writes /PageMode as a name object: pass 'UseOutlines' to pop the bookmark panel, 'UseThumbs' for the thumbnail rail, 'FullScreen' for presentation mode, or 'UseAttachments' to show the attachments pane (ISO 32000-1 §7.7.3.1, Table 28). SetLoadedPageLayout writes /PageLayout the same way — 'SinglePage', 'OneColumn', 'TwoColumnLeft', and the rest. Both take the name without a leading slash; the library adds it on output.
SetLoadedLanguage writes the Catalog /Lang entry, the natural-language tag for the document as a whole — 'en-US', 'de-DE', a BCP 47 tag. Note the type difference that trips people up: /PageMode and /PageLayout are PDF name objects, while /Lang is a string. HotPDF gets this right internally, but if you ever inspect the output you will see /PageMode /UseOutlines against /Lang (en-US), and now you know why. The /Lang entry matters more than it looks: it is what assistive technology reads to pick a pronunciation, and it is a hard requirement for PDF/UA accessibility conformance.
if Pdf.LoadFromFile('handbook.pdf', '') > 0 then
begin
Pdf.SetLoadedPageMode('UseOutlines'); // /PageMode, a name
Pdf.SetLoadedPageLayout('TwoColumnLeft'); // /PageLayout, a name
Pdf.SetLoadedLanguage('en-US'); // /Lang, a string
Pdf.SaveLoadedDocument('handbook-tagged.pdf');
end;
Renaming bookmarks without disturbing the tree
Bookmark titles are routine cleanup — a typo in a heading, a chapter renumbered after the outline was built. SetLoadedOutlineTitle takes a zero-based index into the top-level outline entries and a new title, walks the Catalog → /Outlines → /First → /Next chain to that position, and replaces the entry's /Title string. It changes only the title; the destination, the open/closed state, and the child structure are untouched.
if Pdf.LoadFromFile('report.pdf', '') > 0 then
begin
Pdf.SetLoadedOutlineTitle(0, 'Executive Summary');
Pdf.SetLoadedOutlineTitle(1, 'Financial Results');
Pdf.SaveLoadedDocument('report-renamed.pdf');
end;
Renaming is safe precisely because it never touches the structural counters. Deleting an outline entry is the case that bites, and it is worth understanding even when you are only renaming, because it tells you what not to hand-edit. Each outline node carries a /Count, and — per ISO 32000-1 §12.3.3 — that count is not the number of immediate children. It is the total number of visible descendants: a positive /Count of N means N descendants are currently exposed, while a negative value means the node has descendants but is collapsed. When a top-level entry is removed, the /Outlines root count cannot simply be decremented by one; it has to be recomputed by summing, over each surviving top-level node, "one for the node itself plus its positive /Count," skipping the descendants of any collapsed (negative-count) node. Get that wrong and the bookmark total a reader displays drifts — it jumps by more than one per deletion. Renaming sidesteps all of this, which is one more reason to prefer the targeted helper over poking at the dictionary yourself.
How the save stays in place
Every edit above mutates objects in memory; nothing reaches disk until SaveLoadedDocument runs. The reason this approach is cheap is that the save does not regenerate the document — it preserves the existing object numbers and the structure HotPDF parsed on load, writing back the same graph with your handful of changed and newly allocated objects. That is what keeps a metadata pass from rewriting the entire file, and it is the same in-place-update machinery that makes object streams and incremental updates work. If your source files come out of Word or another office suite, their object layout has its own quirks worth knowing before you edit them; the article on hybrid-reference cross-reference streams in Office PDFs covers how those files are structured and what survives a round trip.
Two boundaries to respect. First, this is an edit-in-place model, not a redaction or sanitization tool: removing an Info key removes that key, but it does not scrub older values that might persist in a prior incremental-update generation of the same file. If your requirement is true removal of sensitive metadata, that is a different, heavier operation. Second, the XMP write is literal — the library trusts your XML and does not validate it — so for anything destined for PDF/A or a strict validator, generate the packet from a known-good template and verify the output. Used within those lines, in-place metadata editing is the right-sized tool: it fixes the few bytes that are wrong and leaves the ninety-nine percent of the file that was already correct exactly as the original producer wrote it.
The loaded-document write API shown here ships with the standard HotPDF Component for Delphi and C++Builder, alongside the full set of metadata, outline, and Catalog editing methods.