Technischer Artikel

Metadaten einer geladenen PDF-Datei in Delphi ohne Neuschreiben bearbeiten

Sie haben zehntausend Vertrags-PDFs von einem Dutzend verschiedenen Generatoren, und die Rechtsabteilung möchte, dass jede davon den richtigen Author, eine korrigierte Producer-Zeichenfolge und einen Lesemodus erhält, der beim Start das Lesezeichen-Panel öffnet. Die naive Lösung besteht darin, jede Datei zu laden, die Seiten neu anzuordnen und ein neues Dokument zu schreiben. Wenn Sie das tun, haben Sie gerade jede vorhandene Objektnummer, den Verlauf inkrementeller Aktualisierungen, jede digitale Signatur und die sorgfältig abgestimmte xref-Tabelle, die das ursprüngliche Werkzeug ausgegeben hat, verworfen. Die Seiten sehen zwar identisch aus, aber die Datei ist strukturell ein Fremdkörper. Für eine bloße Metadatenbearbeitung ist das ein völlig falscher Kompromiss

Der richtige Schritt besteht darin, das geladene Dokument als Objektgraphen zu behandeln, den Sie direkt ändern: Greifen Sie auf das Info-Dictionary, den /Metadata-Stream und den Katalog zu, ändern Sie die wenigen Einträge, die Sie interessieren, und schreiben Sie das Ergebnis zurück. HotPDF, die native VCL-PDF-Komponente für Delphi und C++Builder, stellt genau diese Oberfläche über ihre Schreib-API für geladene Dokumente zur Verfügung. In diesem Artikel geht es um die korrekte Verwendung und um den einen Fehler, den fast jeder macht: das Info-Dictionary zu bearbeiten und zu vergessen, dass eine zweite Kopie derselben Metadaten in XMP existiert

Zwei Orte speichern dieselben Metadaten, und sie weichen voneinander ab

PDF überträgt Dokumenteninformationen an zwei parallelen Orten, und das ist die Ursache für die meisten Support-Tickets des Typs „Ich habe den Titel geändert, aber Acrobat zeigt immer noch den alten an“. Der erste Ort ist das Dokumenteninformations-Dictionary, das klassische /Info-Objekt mit den Schlüsseln /Title, /Author, /Subject, /Keywords, /Creator und /Producer, definiert in ISO 32000-1 §14.3.3. Der zweite Ort ist ein XMP-Paket, ein XML-Dokument, das als Stream unter /Metadata an den Katalog angehängt ist, definiert in §14.3.2 und basierend auf dem Adobe-XMP-Datenmodell

Beide können einen Titel enthalten. Nichts in der Spezifikation zwingt sie zur Übereinstimmung. Moderne Viewer und die meisten PDF/A-Validatoren bevorzugen das XMP-Paket, wenn es vorhanden ist, und greifen andernfalls auf das Info-Dictionary zurück. Wenn Sie also nur /Info aktualisieren – was der Großteil des Codes zum Setzen von PDF-Metadaten tut –, zeigt ein Reader, der XMP vertraut, weiterhin den veralteten Wert an, und ein PDF/A-Prüfer meldet eine Diskrepanz. Der korrekte Vorgang bei jeder Datei, die bereits ein XMP-Paket enthält, ist ein doppelter Schreibzugriff: Ändern Sie den Info-Eintrag and generieren Sie das XMP neu, damit beide konsistent bleiben. HotPDF bietet Ihnen beide Hälften; die Disziplin, sie zusammen zu verwenden, liegt bei Ihnen

Bearbeiten des Info-Dictionarys

Die Hilfsfunktionen auf der Info-Seite sind schlank und berechenbar. SetLoadedTitle, SetLoadedAuthor, SetLoadedSubject, SetLoadedKeywords, SetLoadedCreator und SetLoadedProducer nehmen jeweils einen einzelnen AnsiString entgegen und schreiben den entsprechenden Schlüssel in das geladene Info-Dictionary. Dabei wird der Wert ersetzt, wenn der Schlüssel vorhanden ist, oder hinzugefügt, wenn er fehlt. Um einen Schlüssel vollständig zu entfernen – beispielsweise einen unerwünschten /Creator, der Ihre internen Werkzeuge benennt –, rufen Sie RemoveLoadedInfoKey mit dem reinen Schlüsselnamen auf. Keine dieser Funktionen greift auf XMP zu; sie arbeiten rein auf dem /Info-Objekt, das LoadFromFile beim Analysieren der Datei gefunden hat

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;

Ein Detail, das beachtet werden sollte: Diese Methoden erwarten einen AnsiString. Für ASCII-Titel ist das kein Problem, aber PDF-Textzeichenfolgen, die Nicht-Latein-Zeichen erfordern, müssen wie in der Spezifikation verlangt codiert werden – als UTF-16BE mit einer Byte-Order-Markierung (BOM) oder als PDFDocEncoding –, bevor Sie sie übergeben. Die Bibliothek schreibt die übergebenen Bytes direkt in ein String-Objekt; sie rät die Codierung nicht für Sie. Wenn Ihre Titel in einfachem Englisch verfasst sind, können Sie dies ignorieren. Wenn sie jedoch Akzente oder CJK-Zeichen enthalten, codieren Sie sie bewusst und testen Sie das Ergebnis in einem echten Viewer

Neuschreiben des XMP-Pakets

SetLoadedXMPMetadata ist die andere Hälfte des doppelten Schreibzugriffs. Übergeben Sie das vollständige XMP-Paket als AnsiString, und die Funktion tut eines von zwei Dingen: Wenn der Katalog bereits auf einen /Metadata-Stream verweist, ersetzt sie dessen Inhalt direkt unter Beibehaltung derselben Objektnummer; wenn kein Metadaten-Stream vorhanden ist, erstellt sie einen, markiert ihn als /Type /Metadata und /Subtype /XML, weist ihm eine Objektnummer zu und verlinkt ihn im Katalog. In jedem Fall erhalten Sie ein gültiges Metadatenobjekt, das von Viewern gelesen werden kann

Sie stellen das XML bereit, was bedeutet, dass Sie das Schema kontrollieren – dc:title, dc:creator, xmp:CreatorTool und so weiter. Das bedeutet Macht und Verantwortung zugleich: Die Bibliothek parst oder validiert Ihr Paket nicht, und sie schreibt die Bytes unkomprimiert, ohne dass ein Stream-Filter angewendet wird. Ein fehlerhaftes Paket durchläuft den Aufruf ohne Fehler und äußert sich später als Metadatenproblem. Erstellen Sie das XML sorgfältig und spiegeln Sie genau die Werte wider, die Sie in das Info-Dictionary geschrieben haben, damit sich die beiden Ansichten niemals widersprechen

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;

Diese Reihenfolge – erst Info, dann XMP, dann speichern – ist das Muster, das Sie verinnerlichen sollten. Die beiden Aufrufe sind unabhängig voneinander; die Konsistenz entsteht nur, weil Sie ihnen dieselben Zeichenfolgen übergeben haben. Wenn Sie den XMP-Aufruf bei einer Datei weglassen, die bereits ein XMP-Paket besitzt, kehren Sie zu dem Fehler der unbemerkten Veraltung zurück, den dieser Abschnitt verhindern soll

Diagramm, das ein PDF-Info-Dictionary und einen XMP-Metadaten-Stream zeigt, die beide Titel und Autor enthalten, bearbeitet direkt neben dem Lesezeichen-Strukturbaum
Metadaten befinden sich an zwei Orten – dem Info-Dictionary und dem XMP-Stream – sowie in den Lesehinweisen auf Katalogebene und dem Strukturbaum. Eine Bearbeitung vor Ort ändert beide, ohne das Dokument neu aufzubauen.

Steuern, wie der Viewer die Datei öffnet

Drei Katalogeinträge entscheiden darüber, was ein Reader in dem Moment sieht, in dem das Dokument geöffnet wird, und alle drei sind einzeilige Änderungen im geladenen Graphen. SetLoadedPageMode schreibt /PageMode als Name-Objekt: Übergeben Sie 'UseOutlines', um das Lesezeichen-Panel anzuzeigen, 'UseThumbs' für die Miniaturansichtenleiste, 'FullScreen' für den Vollbildmodus oder 'UseAttachments', um das Anlagen-Panel anzuzeigen (ISO 32000-1 §7.7.3.1, Tabelle 28). SetLoadedPageLayout schreibt /PageLayout auf dieselbe Weise – 'SinglePage', 'OneColumn', 'TwoColumnLeft' und so weiter. Beide erwarten den Namen ohne einen führenden Schrägstrich; die Bibliothek fügt ihn bei der Ausgabe hinzu

SetLoadedLanguage schreibt den Katalogeintrag /Lang, das Sprach-Tag für das Dokument als Ganzes – beispielsweise 'en-US', 'de-DE', ein BCP 47-Tag. Beachten Sie den Typunterschied, der oft zu Verwirrung führt: /PageMode und /PageLayout sind PDF-Name-Objekte, während /Lang ein String (Zeichenfolge) ist. HotPDF handhabt dies intern korrekt, aber wenn Sie jemals die Ausgabe untersuchen, werden Sie /PageMode /UseOutlines im Vergleich zu /Lang (en-US) sehn, und jetzt wissen Sie, warum. Der /Lang-Eintrag ist wichtiger als es scheint: Er wird von assistiven Technologien gelesen, um die richtige Aussprache zu wählen, und ist eine zwingende Anforderung für die PDF/UA-Barrierefreiheitskonformität

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;

Lesezeichen umbenennen, ohne den Strukturbaum zu stören

Die Bereinigung von Lesezeichentiteln ist Routine – ein Tippfehler in einer Überschrift oder ein Kapitel, das nach der Erstellung der Struktur umnummeriert wurde. SetLoadedOutlineTitle übernimmt einen nullbasierten Index für die obersten Struktureinträge und einen neuen Titel, durchläuft die Kette Katalog → /Outlines/First/Next bis zu dieser Position und ersetzt die /Title-Zeichenfolge des Eintrags. Es ändert nur den Titel; das Ziel, der geöffnete/geschlossene Zustand und die Kindstruktur bleiben unberührt

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

Das Umbenennen ist genau deshalb sicher, weil es niemals die strukturellen Zähler berührt. Das Löschen eines Struktureintrags ist der problematische Fall, den man selbst dann verstehen sollte, wenn man nur umbenennt, da er zeigt, was man nicht manuell bearbeiten sollte. Jeder Strukturknoten enthält einen /Count, und – gemäß ISO 32000-1 §12.3.3 – ist dieser Zähler nicht die Anzahl der direkten Kinder. Es ist die Gesamtzahl der sichtbaren Nachkommen: Ein positiver /Count von N bedeutet, dass N Nachkommen derzeit eingeblendet sind, während ein negativer Wert bedeutet, dass der Knoten Nachkommen hat, aber zugeklappt ist. Wenn ein Eintrag auf oberster Ebene entfernt wird, kann der Root-Zähler von /Outlines nicht einfach um eins dekrementiert werden; er muss neu berechnet werden, indem für jeden verbleibenden Knoten auf oberster Ebene „eins für den Knoten selbst plus sein positiver /Count“ summiert wird, wobei die Nachkommen aller zugeklappten Knoten (negativer Zähler) übersprungen werden. Wenn Sie das falsch machen, gerät die vom Reader angezeigte Gesamtzahl der Lesezeichen aus dem Ruder – sie springt bei jedem Löschen um mehr als eins. Das Umbenennen umgeht all dies, was ein weiterer Grund ist, die gezielte Hilfsfunktion dem manuellen Ändern des Dictionarys vorzuziehen

Wie das Speichern an Ort und Stelle bleibt

Jede der obigen Bearbeitungen ändert Objekte im Speicher; nichts gelangt auf die Festplatte, bis SaveLoadedDocument ausgeführt wird. Der Grund, warum dieser Ansatz ressourcenschonend ist, liegt darin, dass beim Speichern das Dokument nicht neu generiert wird – es behält die vorhandenen Objektnummern und die Struktur bei, die HotPDF beim Laden analysiert hat, und schreibt denselben Graphen mit Ihren wenigen geänderten und neu zugewiesenen Objekten zurück. Das verhindert, dass ein Metadaten-Durchlauf die gesamte Datei neu schreibt, und es ist dieselbe In-Place-Aktualisierungsmaschinerie, die auch für Objekt-Streams und inkrementelle Aktualisierungen sorgt. Wenn Ihre Quelldateien aus Word oder einer anderen Office-Suite stammen, weist ihr Objekt-Layout eigene Besonderheiten auf, die man vor der Bearbeitung kennen sollte. Der Artikel über Hybrid-Reference-Kreuzverweis-Streams in Office-PDFs behandelt, wie diese Dateien strukturiert sind und was einen Roundtrip übersteht

Zwei Grenzen, die es zu beachten gilt: Erstens handelt es sich hierbei um ein In-Place-Bearbeitungsmodell, nicht um ein Schwärzungs- oder Bereinigungswerkzeug. Das Entfernen eines Info-Schlüssels entfernt diesen Schlüssel, löscht jedoch keine älteren Werte, die möglicherweise in einer früheren inkrementellen Aktualisierungsgeneration derselben Datei verbleiben. Wenn Ihre Anforderung darin besteht, sensible Metadaten wirklich und endgültig zu entfernen, ist dies eine andere, komplexere Operation. Zweitens erfolgt das Schreiben von XMP wörtlich – die Bibliothek vertraut Ihrem XML und validiert es nicht. Generieren Sie daher für alles, was für PDF/A oder einen strengen Validator bestimmt ist, das Paket aus einer bekannten Vorlage und überprüfen Sie die Ausgabe. In diesem Rahmen ist die In-Place-Metadatenbearbeitung das passende Werkzeug: Sie korrigiert die wenigen Bytes, die falsch sind, und belässt die neunundneunzig Prozent der Datei, die bereits korrekt waren, genau so, wie der ursprüngliche Ersteller sie geschrieben hat

Die hier gezeigte Schreib-API für geladene Dokumente wird mit der standardmäßigen HotPDF Component für Delphi und C++Builder ausgeliefert, zusammen mit dem vollständigen Satz an Bearbeitungsmethoden für Metadaten, Strukturen (Outlines) und den Katalog