Teknisk artikel

Redigér metadata i en indlæst PDF i Delphi uden at genopbygge den

Du har ti tusind kontrakt-PDF'er fra et dusin forskellige generatorer, og juristerne vil have, at hver eneste fil bærer den rigtige Author, en rettet Producer-streng og en læsetilstand, der åbner bogmærkepanelet ved start. Den naive løsning er at indlæse hver fil, lægge siderne om og skrive et helt nyt dokument. Gør du det, har du smidt alle eksisterende objektnumre, historikken for inkrementelle opdateringer, enhver digital signatur og den omhyggeligt afstemte xref væk, som det oprindelige værktøj skrev ud. Siderne ser identiske ud, men filen er strukturelt en fremmed. For en metadataredigering er det helt den forkerte byttehandel

Det rigtige greb er at behandle det indlæste dokument som en objektgraf, du muterer på stedet: gå ind i Info-ordbogen, /Metadata-strømmen og Catalog, ændr de få poster, du er interesseret i, og skriv resultatet tilbage. HotPDF, den native VCL PDF-komponent til Delphi og C++Builder, eksponerer netop den overflade gennem sin skrive-API til indlæste dokumenter. Denne artikel handler om at bruge den korrekt, og om den ene fejl næsten alle begår: at redigere Info-ordbogen og glemme, at en anden kopi af de samme metadata ligger i XMP

To steder gemmer de samme metadata, og de er ikke enige

PDF bærer dokumentoplysninger to parallelle steder, og det er roden til de fleste "jeg ændrede titlen, men Acrobat viser stadig den gamle" sager. Det første er dokumentinformations-ordbogen, det klassiske /Info-objekt med nøglerne /Title, /Author, /Subject, /Keywords, /Creator og /Producer, defineret i ISO 32000-1 §14.3.3. Det andet er en XMP-pakke, et XML-dokument lagret som en strøm knyttet til Catalog under /Metadata, defineret i §14.3.2 og bygget oven på Adobe XMP-datamodellen

Begge kan indeholde en titel. Specifikationen tvinger dem ikke til at være enige. Moderne fremvisere og de fleste PDF/A-validatorer foretrækker XMP-pakken, når den findes, og falder tilbage til Info-ordbogen, når den ikke gør. Så hvis du kun opdaterer /Info - hvilket langt det meste "sæt PDF metadata"-kode gør - vil en læser, der stoler på XMP, blive ved med at vise den gamle værdi, og en PDF/A-kontrol vil markere uoverensstemmelsen. Den korrekte handling på enhver fil, der allerede har en XMP-pakke, er derfor en dobbelt skrivning: ændr Info-posten og regenerér XMP, så de to forbliver konsistente. HotPDF giver dig begge halvdele; disciplinen med at bruge dem sammen er din

Redigering af Info-ordbogen

Hjælperne på Info-siden er tynde og forudsigelige. SetLoadedTitle, SetLoadedAuthor, SetLoadedSubject, SetLoadedKeywords, SetLoadedCreator og SetLoadedProducer tager hver en enkelt AnsiString og skriver den tilsvarende nøgle ind i den indlæste Info-ordbog, hvor værdien erstattes, hvis nøglen findes, og tilføjes, hvis den ikke gør. Hvis du vil fjerne en nøgle helt - for eksempel en utæt /Creator, der afslører dit interne værktøj - kalder du RemoveLoadedInfoKey med det rene nøgle-navn. Ingen af dem rører XMP; de arbejder udelukkende på /Info-objektet, som LoadFromFile fandt, da filen blev læst ind

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 detalje, der skal holdes ærlig: disse metoder tager AnsiString. For ASCII-titler er det ikke noget problem, men PDF-tekststrenge, der skal bære ikke-latinske tegn, skal kodes som specifikationen kræver - UTF-16BE med byte order mark eller PDFDocEncoding - før du sender dem videre. Biblioteket skriver de bytes, du giver det, ind i et strengobjekt; det gætter ikke en kodning for dig. Hvis dine titler er ren engelsk, kan du ignorere dette. Hvis de indeholder accenttegn eller CJK-tegn, skal du kode bevidst og teste i en rigtig fremviser

Omskrivning af XMP-pakken

SetLoadedXMPMetadata er den anden halvdel af dobbelt skrivning. Send den hele XMP-pakken som en AnsiString, og den gør én af to ting: hvis Catalog allerede refererer til en /Metadata-strøm, erstatter den indholdet af strømmen på stedet og bevarer samme objektnummer; hvis der ikke findes nogen metadata-strøm, opretter den en, markerer den med /Type /Metadata og /Subtype /XML, tildeler et objektnummer og linker den fra Catalog. Uanset hvad ender du med et gyldigt metadataobjekt, som fremvisere kan læse

Du leverer XML'en, og dermed styrer du skemaet - dc:title, dc:creator, xmp:CreatorTool og så videre. Det er både magt og ansvar: biblioteket parser eller validerer ikke din pakke, og det skriver bytes ukomprimeret uden noget stream-filter. En fejlformet pakke glider gennem kaldet og viser sig først senere som en klage over ødelagte metadata. Byg XML'en omhyggeligt, og spejl præcist de værdier, du skrev ind i Info-ordbogen, så de to visninger aldrig modsiger hinanden

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 rækkefølge - Info først, XMP derefter, og så gem - er mønsteret, du skal huske. De to kald er uafhængige; konsistensen findes kun, fordi du gav dem de samme strenge. Spring XMP-kaldet over på en fil, der har en XMP-pakke, og du er tilbage ved den tavse forældelsesfejl, som hele dette afsnit findes for at forhindre

Diagram showing a PDF Info dictionary and an XMP metadata stream both holding title and author, edited in place alongside the bookmark outline tree
Metadata findes to steder - i Info-ordbogen og i XMP-strømmen - sammen med Catalogs læsehints og dispositionstræet. En redigering på stedet berører dem alle uden at genopbygge dokumentet.

Styr hvordan fremviseren åbner filen

Tre Catalog-poster afgør, hvad læseren ser i det øjeblik dokumentet åbnes, og alle tre er one-line-redigeringer på den indlæste graf. SetLoadedPageMode skriver /PageMode som et name-objekt: send 'UseOutlines' for at åbne bogmærkepanelet, 'UseThumbs' for miniaturelisten, 'FullScreen' for præsentationstilstand eller 'UseAttachments' for at vise vedhæftningspanelet (ISO 32000-1 §7.7.3.1, tabel 28). SetLoadedPageLayout skriver /PageLayout på samme måde - 'SinglePage', 'OneColumn', 'TwoColumnLeft' og resten. Begge tager navnet uden en indledende skråstreg; biblioteket tilføjer den ved output

SetLoadedLanguage skriver Catalogs /Lang-post, den naturlige sprogmarkør for hele dokumentet - 'en-US', 'de-DE', en BCP 47-tag. Bemærk typeforskellen, som ofte snubler folk: /PageMode og /PageLayout er PDF name-objekter, mens /Lang er en string. HotPDF håndterer det korrekt internt, men hvis du inspicerer outputtet, vil du se /PageMode /UseOutlines ved siden af /Lang (en-US), og nu ved du hvorfor. /Lang-posten betyder mere, end den ser ud til: det er den, hjælpeteknologi læser for at vælge udtale, og det er et hårdt krav i PDF/UA-tilgængelighedsoverensstemmelse

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;

Omdøb bogmærker uden at forstyrre træet

Bogmærketitler er rutineoprydning - en tastefejl i en overskrift, eller et kapitel, der er blevet omnummereret efter at dispositionen blev bygget. SetLoadedOutlineTitle tager et nulbaseret indeks ind i de øverste dispositionselementer og en ny titel, følger Catalog → /Outlines/First/Next-kæden til den position og erstatter postens /Title-streng. Den ændrer kun titlen; destinationen, den åbne/lukkede tilstand og understrukturen forbliver urørt

if Pdf.LoadFromFile('report.pdf', '') > 0 then
begin
  Pdf.SetLoadedOutlineTitle(0, 'Executive Summary');
  Pdf.SetLoadedOutlineTitle(1, 'Financial Results');
  Pdf.SaveLoadedDocument('report-renamed.pdf');
end;

Omdøbning er sikker netop fordi den aldrig rører de strukturelle tællere. Sletning af et dispositionselement er det tilfælde, der bider, og det er værd at forstå, også når du kun omdøber, fordi det fortæller dig, hvad du ikke skal redigere i hånden. Hver dispositionsnode bærer en /Count, og - ifølge ISO 32000-1 §12.3.3 - er den tæller ikke antallet af umiddelbare børn. Det er det samlede antal synlige efterkommere: en positiv /Count på N betyder, at N efterkommere aktuelt er udfoldet, mens en negativ værdi betyder, at noden har efterkommere, men er klappet sammen. Når en topniveaupost fjernes, kan root-tælleren for /Outlines ikke bare mindskes med én; den skal genberegnes ved at summere for hver overlevende topniveau-node, "én for selve noden plus dens positive /Count", og samtidig springe efterkommerne for enhver sammenklappet (negativ) node over. Går du galt i byen her, driver det bogmærketal, fremviseren viser, af sted - det hopper mere end én per sletning. Omdøbning går uden om alt dette, og det er endnu en grund til at foretrække den målrettede hjælper frem for selv at rode i ordbogen

Sådan bliver gemningen på stedet

Hver redigering ovenfor muterer objekter i hukommelsen; intet rammer disken, før SaveLoadedDocument kører. Grunden til, at denne tilgang er billig, er, at gemningen ikke regenererer dokumentet - den bevarer de eksisterende objektnumre og den struktur, HotPDF parse'ede ved indlæsning, og skriver den samme graf tilbage med dine få ændrede og nyoprettede objekter. Det er det, der forhindrer en metadataopdatering i at omskrive hele filen, og det er den samme mekanik for opdatering på stedet, som får object streams og inkrementelle opdateringer til at virke. Hvis dine kildefiler kommer fra Word eller en anden office-suite, har deres objektlayout sine egne særheder, som er værd at kende, før du redigerer dem; artiklen om hybrid-reference cross-reference streams i Office-PDF'er gennemgår, hvordan de filer er struktureret, og hvad der overlever en rundtur

Der er to grænser, du skal respektere. For det første er dette en rediger-på-stedet-model, ikke et redaktions- eller sanitiseringsværktøj: at fjerne en Info-nøgle fjerner den nøgle, men det skrubber ikke ældre værdier, der kan ligge i en tidligere inkrementel opdateringsgeneration af den samme fil. Hvis kravet er reel fjernelse af følsomme metadata, er det en anden og tungere operation. For det andet er XMP-skrivningen bogstavelig - biblioteket stoler på din XML og validerer den ikke - så for alt, der er tiltænkt PDF/A eller en streng validator, skal du generere pakken ud fra en kendt god skabelon og verificere outputtet. Brugt inden for de rammer er redigering af metadata på stedet det rigtige værktøj i den rigtige størrelse: det retter de få bytes, der er forkerte, og lader de nioghalvfems procent af filen, som allerede var korrekt, stå præcis som den oprindelige producent skrev den

Den viste skrive-API til indlæste dokumenter følger med den standard HotPDF Component til Delphi og C++Builder, sammen med hele sættet af metoder til metadata-, disposition- og Catalog-redigering