Du har tiotusen kontrakts-PDF:er från ett dussin olika generatorer, och juristavdelningen vill att varenda en ska bära rätt Author, en korrigerad Producer sträng och ett läsläge som öppnar bokmärkespanelen vid start. Den naiva lösningen är att läsa in varje fil, lägga om sidorna och skriva ett nytt dokument. Gör du det kastar du bort varje befintligt objektnummer, historiken för inkrementella uppdateringar, alla digitala signaturer och den noggrant avstämda xref som det ursprungliga verktyget skapade. Sidorna ser identiska ut, men filen är strukturellt sett en främling. För en metadataändring är det helt fel avvägning
Det rätta draget är att behandla det inlästa dokumentet som en objektgraf du muterar på plats: gå in i Info-ordboken, den /Metadata streamen och Katalogen, ändra de få poster du bryr dig om och skriv tillbaka resultatet. HotPDF, den inbyggda VCL-PDF-komponenten för Delphi och C++Builder, exponerar exakt den ytan via sitt skriv-API för inlästa dokument. Den här artikeln handlar om att använda det rätt, och om det enda misstag nästan alla gör: att redigera Info-ordboken och glömma att en andra kopia av samma metadata finns i XMP
Två platser lagrar samma metadata, och de är inte överens
PDF lagrar dokumentinformation på två parallella ställen, och det är roten till de flesta "Jag ändrade titeln men Acrobat visar fortfarande den gamla"-ärenden. Den första är dokumentinformationsordboken, det klassiska /Info objektet med /Title, /Author, /Subject, /Keywords, /Creator, och /Producer nycklar, definierade i ISO 32000-1 §14.3.3. Den andra är ett XMP-paket, ett XML-dokument som lagras som en stream som hänger från Katalogen under /Metadata, definierat i §14.3.2 och byggt på Adobes XMP-datamodell
Båda kan bära en titel. Inget i specifikationen tvingar dem att stämma överens. Moderna visare och de flesta PDF/A-validerare föredrar XMP-paketet när det finns och faller tillbaka till Info-ordboken när det inte gör det. Så om du bara uppdaterar /Info - vilket är vad den stora majoriteten av kod som "sätter PDF-metadata" gör - en läsare som litar på XMP kommer att fortsätta visa det gamla värdet, och en PDF/A-kontroll kommer att flagga avvikelsen. Den korrekta åtgärden för alla filer som redan har ett XMP-paket är en dubbel skrivning: ändra Info-posten och skapa om XMP, så att de två förblir konsekventa. HotPDF ger dig båda halvorna; disciplinen att använda dem tillsammans är upp till dig
Redigera Info-ordboken
Info-hjälparna är tunna och förutsägbara. SetLoadedTitle, SetLoadedAuthor, SetLoadedSubject, SetLoadedKeywords, SetLoadedCreator, och SetLoadedProducer tar emot en enda AnsiString och skriver motsvarande nyckel i den inlästa Info-ordboken, och ersätter värdet om nyckeln finns och lägger till det om det inte gör det. För att ta bort en nyckel helt - säg en läckande /Creator som namnger ditt interna verktyg - anropa RemoveLoadedInfoKey med det rena nyckelnamnet. Ingen av dessa rör XMP; de arbetar enbart på /Info objektet som LoadFromFile hittade när den parsade filen
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;
En detalj att hålla korrekt: de här tar AnsiString. För ASCII-titlar är det inget problem, men PDF-textsträngar som behöver icke-latinska tecken måste kodas enligt specifikationen - UTF-16BE med byteordningsmärke eller PDFDocEncoding - innan du lämnar över dem. Biblioteket skriver de byte du ger det till ett string-objekt; det gissar ingen kodning åt dig. Om dina titlar är vanlig engelska kan du ignorera detta. Om de innehåller accenttecken eller CJK-tecken ska du koda dem medvetet och testa i en riktig visare
Skriva om XMP-paketet
SetLoadedXMPMetadata är den andra halvan av dubbel skrivning. Skicka det fullständiga XMP-paketet som ett AnsiString och den gör en av två saker: om Katalogen redan hänvisar till en /Metadata stream ersätter den innehållet i den streamen på plats och behåller samma objektnummer; om det inte finns någon metadata-stream skapar den en, markerar den /Type /Metadata och /Subtype /XML, allokerar ett objektnummer och länkar den från Katalogen. I båda fallen får du ett giltigt metadataobjekt som visarna läser
Du levererar XML, vilket betyder att du styr schemat - dc:title, dc:creator, xmp:CreatorTool, och så vidare. Det är makt och ansvar i ett: biblioteket parsar eller validerar inte ditt paket, och det skriver byten okomprimerade utan att någon streamfilter används. Ett felaktigt paket passerar utan problem genom anropet och dyker upp senare som ett klagomål om trasig metadata. Bygg XML noggrant och spegla exakt de värden du skrev i Info-ordboken så att de två vyerna aldrig motsäger varandra
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;
Den ordningen - Info först, XMP sedan, därefter sparande - är mönstret att ta till sig. De två anropen är oberoende; konsekvensen finns bara för att du matade dem med samma strängar. Hoppar du över XMP-anropet på en fil som har ett XMP-paket är du tillbaka i buggen med tyst föråldring som hela det här avsnittet finns till för att förhindra

Styra hur visaren öppnar filen
Tre katalogposter avgör vad en läsare ser i samma ögonblick dokumentet öppnas, och alla tre är enkla engångsredigeringar på den inlästa grafen. SetLoadedPageMode skriver /PageMode som ett namnobjekt: skicka 'UseOutlines' för att öppna bokmärkespanelen, 'UseThumbs' för miniatyrraden, 'FullScreen' för presentationsläge, eller 'UseAttachments' för att visa bilagepanelen (ISO 32000-1 §7.7.3.1, tabell 28). SetLoadedPageLayout skriver /PageLayout på samma sätt - 'SinglePage', 'OneColumn', 'TwoColumnLeft', och resten. Båda tar namnet utan inledande snedstreck; biblioteket lägger till det vid utmatningen
SetLoadedLanguage skriver Katalogens /Lang post, den naturliga språktaggen för dokumentet som helhet - 'en-US', 'de-DE', en BCP 47-tagg. Lägg märke till typ-skillnaden som brukar ställa till det: /PageMode och /PageLayout är PDF name objekt, medan /Lang är en string. HotPDF gör detta rätt internt, men om du någonsin inspekterar utdata ser du /PageMode /UseOutlines mot /Lang (en-US), och nu vet du varför. /Lang posten betyder mer än den ser ut att göra: det är det assistiv teknik läser för att välja uttal, och det är ett hårt krav för PDF/UA-tillgänglighetsöverensstämmelse
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;
Byta namn på bokmärken utan att störa trädet
Bokmärkesrubriker är vanligt städarbete - ett stavfel i en rubrik, ett kapitel som har numrerats om efter att dispositionsstrukturen byggdes. SetLoadedOutlineTitle tar ett nollbaserat index in i poster på översta nivån i dispositionsstrukturen och en ny titel, går längs kedjan Katalog → /Outlines → /First → /Next till den positionen och ersätter postens /Title sträng. Den ändrar bara titeln; målet, det öppna/stängda läget och barnstrukturen lämnas orörda
if Pdf.LoadFromFile('report.pdf', '') > 0 then
begin
Pdf.SetLoadedOutlineTitle(0, 'Executive Summary');
Pdf.SetLoadedOutlineTitle(1, 'Financial Results');
Pdf.SaveLoadedDocument('report-renamed.pdf');
end;
Att byta namn är säkert just för att det aldrig rör de strukturella räknarna. Att ta bort en dispositionspost är det som brukar bita tillbaka, och det är värt att förstå även när du bara byter namn, eftersom det visar vad du inte ska redigera för hand. Varje dispositionsnod har en /Count, och - enligt ISO 32000-1 §12.3.3 - är det talet inte antalet omedelbara barn. Det är det totala antalet synliga efterföljare: ett positivt /Count på N betyder att N efterföljare just nu visas, medan ett negativt värde betyder att noden har efterföljare men är hopfälld. När en post på översta nivån tas bort kan /Outlines rotantalet inte bara minskas med ett; det måste räknas om genom att man summerar, för varje överlevande nod på översta nivån, "ett för noden själv plus dess positiva /Count," och hoppar över efterföljarna till varje hopfälld (negativ) nod. Gör du fel där driver den bokmärkestotal som läsaren visar iväg - den hoppar med mer än ett per borttagning. Att byta namn kringgår allt detta, vilket är ännu en anledning att föredra den riktade hjälpfunktionen framför att peta i ordboken själv
Hur sparandet stannar på plats
Varje redigering ovan muterar objekt i minnet; inget når disken förrän SaveLoadedDocument körs. Anledningen till att detta tillvägagångssätt är billigt är att sparandet inte återskapar dokumentet - det bevarar de befintliga objektnumren och den struktur som HotPDF tolkade vid inläsning och skriver tillbaka samma graf med dina få ändrade och nyligen allokerade objekt. Det är det som gör att en metadataomgång inte skriver om hela filen, och det är samma mekanism för in-place-uppdatering som gör att objektströmmar och inkrementella uppdateringar fungerar. Om dina källfiler kommer från Word eller någon annan kontorssvit har deras objektlayout egna egenheter som är värda att känna till innan du redigerar dem; artikeln om hybridreferens-korsreferensströmmar i Office-PDF:er går igenom hur de filerna är uppbyggda och vad som överlever en tur fram och tillbaka
Två gränser att respektera. För det första är detta en modell för redigering på plats, inte ett redigerings- eller saneringsverktyg: att ta bort en Info-nyckel tar bort just den nyckeln, men det rensar inte äldre värden som kan leva kvar i en tidigare inkrementell uppdateringsgeneration av samma fil. Om du verkligen behöver ta bort känslig metadata är det en annan och tyngre åtgärd. För det andra är XMP-skrivningen bokstavlig - biblioteket litar på din XML och validerar den inte - så för allt som är avsett för PDF/A eller en strikt validerare ska du generera paketet från en beprövad mall och verifiera utdata. Använt inom de ramarna är redigering av metadata på plats rätt verktyg i rätt storlek: den fixar de få byte som är fel och lämnar de nittionio procent av filen som redan var korrekt precis som den ursprungliga producenten skrev den
Skriv-API:t för inlästa dokument som visas här följer med standarden HotPDF Component för Delphi och C++Builder, tillsammans med hela uppsättningen metoder för metadata, disposition och Katalogredigering