PDFlibPas implementiert PDF/VCR-1 als getaggtes Strukturprofil: Die Ersetzungsdefinition lebt unter StructTreeRoot als direkter Kindzweig, wobei eine Vorlagenwurzel /O /GTS_Template trägt, um das Feldset zu deklarieren, und jeder Platzhalter /O /GTS_Replacement trägt, um ein Datenfeld zu binden. Nichts davon ist ein Seitenlevel-Dictionary
Genau dieser eine Satz ist der Teil, den die meisten Implementierungen falsch machen – und falsch machen kostet mehr als eine fehlgeschlagene Validierung. Ein Variable-Data-Job, dessen Ersetzungsplan falsch gelesen wird, crasht nicht. Er druckt in Serie mit dem falschen Feld in der falschen Box, und der Erste, der es merkt, ist der Kunde mit fünfzigtausend Mailern in der Hand
Wo die Ersetzungsdefinition tatsächlich lebt
PDF/VCR-1 legt sein Ersetzungsmodell in die in ISO 32000-1 §14.7 beschriebene logische Strukturhierarchie, nicht in ein privates Seiten-Dictionary. Die Vorlagenwurzel ist ein Strukturelement, das direkt an StructTreeRoot hängt, erkennbar an einem Attributbesitzer GTS_Template, und deklariert den gesamten Feldwortschatz an einer Stelle über GTS_Fields plus ein optionales GTS_Pages, das das Feld benennt, das die Seitenauswahl steuert
Platzhalter sind Strukturelemente, die in dieser Wurzel verschachtelt liegen, mit dem Attributbesitzer GTS_Replacement. Jeder trägt GTS_Generator (das PDFlibPas als PassThrough verlangt), einen GTS_Data-Wert, der genau ein deklariertes Feld benennt, und ein optionales GTS_BBox. Die Bindung an die Seite ist auch keine Seitenannotation: Das Element zeigt über /Pg auf seine Seite und über /K auf die Marked-Content-Sequenz. Die Wahl von Struktur statt Seiten-Dictionary hat dann eine Konsequenz, die leicht zu benennen und leicht zu unterschätzen ist: Der Strukturbaum ist ein allgemeiner Graph, den man vom Katalog aus erreicht, daher muss alles, was das Profil konsumiert, ein Baumläufer sein – mit allen Gefahren, die das mit sich bringt – statt ein Dictionaryleser. Wenn Sie bisher nicht mit dem Strukturbaum gearbeitet haben, sind die Mechanismen in getaggter PDF-Struktur und Barrierefreiheit ausgebreitet
Warum lässt das Prüfen nur des ersten Top-Level-Zweigs schlechte Dateien durch?
Weil StructTreeRoot viele direkte Kinder haben kann, und ein Validator, der nach dem Auffinden der Vorlagenwurzel stoppt, die übrigen nie sieht. Genau dieses Versagen ist PDFlibPas gebaut, um es zu vermeiden, und es erzeugt genau die Art von falschem Bestehen, die schlimmer ist als gar keine Prüfung: eine Datei, die konform meldet und eine zweite Vorlagenwurzel enthält, oder einen Platzhalter außerhalb des Vorlagenzweigs, den kein Konsument beachtet
Deshalb läuft ValidatePDFVCRStructure in PDFlibPas zweimal über denselben Baum. Der erste Durchlauf zählt alle direkten Kinder von StructTreeRoot, die GTS_Template tragen; alles andere als genau eines wird abgelehnt, bevor auch nur ein Platzhalter gelesen wird. Der zweite Durchlauf steigt in alle Top-Level-Zweige ab, nicht nur in den Vorlagenzweig, und lehnt zwei Dinge zugleich ab: ein verschachteltes Element mit GTS_Template, das nicht die Wurzel ist, und jedes GTS_Replacement-Element außerhalb des Vorlagenzweigs
Der Durchlauf ist bei 64 Ebenen Tiefe und 1.000.000 Knoten begrenzt, und er notiert Objekt- und Generationsnummer jedes besuchten Elements, sodass ein Zyklus oder eine wiederholte Referenz gemeldet statt verfolgt wird. Diese Zahlen sind kein Performance-Tuning. Ein Strukturbaum ist ein Objektgraph, den ein Angreifer kontrolliert, und ein Validator, der ohne Decke rekursiert, ist ein Stack Overflow mit einer Dateiendung
Eine Vorlage erstellen – und die Beschränkungen, an denen die API nicht rütteln lässt
PDFlibPas stellt das Erstellen über eine Klammer-API bereit, die die Strukturregeln beim Schreiben durchsetzt statt sie beim Speichern zu entdecken. SetPDFVCRMode schaltet das Dokument in das Profil, was intern den PDF/X-4-Basismodus von ISO 15930-7 voraussetzt und das Mark-Info-Flag einschaltet, denn eine PDF/VCR-1-Datei ohne Marked Content hat nichts, worauf ein Platzhalter zeigen könnte. Von dort nimmt BeginPDFVCRTemplate die Feldliste und das Seitenauswahlfeld entgegen, und es ist streng auf Weisen, die Sie kennen sollten, bevor Sie Ihr Datenschema entwerfen. Feldnamen laufen durch PDFVCRValidFieldName, das nur ASCII-Buchstaben, Ziffern, Unterstrich, Bindestrich und Punkt akzeptiert, denn das sind die Zeichen, die ohne Escape-Mehrdeutigkeit in einen PDF-Namen serialisieren. PDFVCRNormalizeFieldList hält die Liste case-sensitiv und lehnt Duplikate rundweg ab, und das Seitenauswahlfeld muss selbst in der deklarierten Liste auftauchen
var
Lib: TPDFlib;
Diag: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.NewDocument;
if Lib.SetPDFVCRMode(1) <> 1 then
raise Exception.Create('PDF/VCR mode was refused');
Lib.NewPage;
if Lib.BeginPDFVCRTemplate('Salutation,FullName,AccountNo',
'AccountNo') <> 1 then
raise Exception.Create('Field list rejected');
// Ein Platzhalter bindet genau ein deklariertes Feld.
Lib.BeginPDFVCRPlaceholder('FullName', 72, 690, 320, 712);
Lib.DrawText(72, 694, 'SAMPLE NAME');
Lib.EndPDFVCRPlaceholder;
Lib.EndPDFVCRTemplate;
Diag := Lib.GetPDFVCRDiagnostics;
if Diag <> '' then
begin
Writeln('PDF/VCR blockers: ', Diag);
Exit;
end;
Lib.SaveToFile('campaign-template.pdf');
finally
Lib.Free;
end;
end;
Beachten Sie, was Ihnen die Klammerung bringt. BeginPDFVCRPlaceholder weigert sich zu öffnen, solange keine Vorlage aktiv ist und der Tag-Stack nicht genau eine Ebene tief ist – so garantiert PDFlibPas, dass Platzhalter direkte Kinder der Vorlagenwurzel sind und nie ineinander verschachteln. Auch Koordinaten werden validiert: NaN, Unendlich und invertierte Rechtecke werden abgelehnt, sodass eine schlechte Bounding Box beim Aufruf scheitert statt in einem Preflight-Bericht eine Woche später
Warum braucht ein Platzhalter einen eigenen MCID-Serialisierungspfad?
Weil PDF/VCR-1 /K als nackte Ganzzahl will – und so serialisiert ein allgemeiner Tagged-PDF-Writer die Verbindung zwischen Strukturelement und Inhalt nicht. Nach ISO 32000-1 §14.6 referenziert ein Strukturelement Marked Content normalerweise entweder über eine direkte MCID-Ganzzahl, wenn das Element seine Seite bereits über /Pg identifiziert, oder über ein Marked-Content-Referenzdictionary, das die Seite explizit benennt. Eine Bibliothek, die für viele Zwecke getaggte Ausgabe erzeugt, neigt dazu, sich auf die Dictionary-Form zu vereinheitlichen, weil sie sich über Seiten hinweg besser komponiert
PDFlibPas hat hier die schmale Option gewählt statt der allgemeinen. SetCurrentStructElemDirectMCIDKid markiert das aktuelle Strukturelement, und nur Elemente mit diesem Flag serialisieren /K als direkte MCID mit der Seite auf /Pg; alles andere lässt den bestehenden MCR-Dictionary-Pfad unangetastet. Das ist ein bewusster Trade-off, und er ist einer, den man laut benennen sollte: Ein Profil hinzuzufügen darf die Bytes der Ausgabe nicht ändern, die jedes andere getaggte Dokument bereits erzeugt, daher bekommt das Profil einen eigenen Pfad statt eines globalen Schalters. Jede bestehende getaggte Datei behält ihre bisherige Serialisierung, und die Regressionsfläche bleibt so groß wie das neue Feature
Ein Durchlauf, zwei Konsumenten
PDFlibPas leitet das Ersetzungsmanifest und das Konformitätsurteil aus demselben Zweipass-Lauf ab – deshalb darf man dem Manifest als Jobplan vertrauen. Der Durchlauf füllt während der Validierung einen TPDFVCRManifestData-Record: Fields von der Vorlagenwurzel, PageSelectionField, wenn eines deklariert ist, und ein Placeholders-Array aus TPDFVCRPlaceholderInfo-Einträgen mit FieldName, PageNumber, MCID, HasBBox und BBox. Der Konformitätseinstieg ist CheckCompliancePDFVCR(InputStream, Password, Options, ResultList), und er führt seine Prüfungen in Abhängigkeitsreihenfolge aus statt auf einmal: zuerst die volle PDF/X-Prüfung, dann GTS_PDFXVersion gegen PDF/X-4, dann GTS_PDFVCRVersion aus dem Namespace http://www.npes.org/pdfvcr/ns/id/, und erst dann der Strukturlauf. Options als 1 übergeben lässt ihn beim ersten Problem stoppen – das will man in einem Batch-Gate; 0 übergeben sammelt alles – das will man in einem Bericht
// Eine Vorlage prüfen, die Sie erhalten statt erzeugt haben
var
Issues: TStringList;
Stream: TFileStream;
I: Integer;
begin
Issues := TStringList.Create;
Stream := TFileStream.Create('supplier-template.pdf',
fmOpenRead or fmShareDenyWrite);
try
if CheckCompliancePDFVCR(Stream, '', 0, Issues) <> 100 then
for I := 0 to Issues.Count - 1 do
Writeln('PDF/VCR: ', Issues[I]);
finally
Stream.Free;
Issues.Free;
end;
end;
Das Manifest ist ein Vertrag, daher existiert es nur für konforme Dateien
BuildPDFVCRManifest gibt JSON nur zurück, wenn die Problemliste leer bleibt, sonst einen leeren String. Das ist keine Defensivprogrammierung um ihrer selbst willen. Ein Manifest wird von einer Kompositionsengine konsumiert, die echte Ausgabe daraus steuert, und eine partielle Feldliste aus einer Datei mit zwei Vorlagenwurzeln einzuhändigen, lädt die Engine ein, eine nicht konforme Struktur als vertrauenswürdigen Ersetzungsplan zu behandeln. Über die Dokument-API verpackt GetPDFVCRManifest(InputFileName, Password) denselben Aufruf und setzt den letzten Fehlercode auf PDFLIB_ERROR_PDFVCR_COMPLIANCE (608), wenn die Datei nicht qualifiziert ist, sodass eine fehlgeschlagene Extraktion von einer leeren Vorlage unterscheidbar ist. Das JSON meldet fields, pageSelectionField als String oder null und ein placeholders-Array, dessen Einträge field, page, mcid und entweder eine vierteilige bbox oder null angeben
var
Lib: TPDFlib;
Manifest: WideString;
begin
Lib := TPDFlib.Create;
try
Manifest := Lib.GetPDFVCRManifest('campaign-template.pdf', '');
if Manifest = '' then
// 608 heißt: Die Datei ist nicht PDF/VCR-1-konform,
// 401 heißt: Die Datei konnte nicht geöffnet werden
Writeln('No manifest, error ', Lib.LastErrorCode)
else
Writeln(Manifest);
finally
Lib.Free;
end;
end;
Ein Detail, das Sie in Ihren Konsumenten mitnehmen sollten: Der page-Wert folgt der bibliotheksweiten nullbasierten Seitenkonvention, nicht der einbasierten Nummerierung, die ein Mensch im Viewer sieht. Das zu verwechseln ist ein stiller Off-by-One, der Inhalt auf das falsche Blatt bringt – und kein Validator fängt es, denn beide Zahlen sind strukturell gültig
Wo die Grenzen liegen
Die PDF/VCR-1-Unterstützung in PDFlibPas deckt Vorlagenerstellung, Save-Gate, eigenständiges Preflight und Manifestextraktion ab. Sie führt die Ersetzung nicht aus. Daten in Platzhalter einzusetzen und die personalisierten Exemplare zu erzeugen, ist der Kompositionsschritt, und er lebt mit Absicht außerhalb der Bibliothek, denn das Manifest ist genau die Schnittstelle, die es erlaubt, eine Kompositionsengine unabhängig zu schreiben. Auch das Save-Gate verweigert statt zu reparieren. Anders als bei Profilen, in denen ein falsches Annotationsflag stillschweigend normalisiert werden kann, ist jede von PDFVCRReadyForSave durchgesetzte PDF/VCR-1-Beschränkung eine, bei der Raten die Bedeutung des Dokuments ändern würde: Eine Vorlagenwurzel zu erfinden oder zu raten, zu welchem Feld ein ungebundener Platzhalter gehört, erzeugt eine Datei, die validiert und lügt. Dieselbe Reparieren-gegen-Verweigern-Linie ist für ein anderes Profil im Artikel PDF/E-1-Autorenmodus und begrenztes Preflight besprochen, und der Workflow für mehrere Konformitätsprüfungen über ein Dokument ist in der Compliance- und Signing-Workbench beschrieben
Erstellung, der begrenzte Strukturlauf, das Save-Gate und der Manifestextraktor gehören alle zur PDFlibPas-Delphi-PDF-Bibliothek, was bedeutet: Eine Vorlage kann unter dem Profil erzeugt und dann über einen getrennten Codepfad verifiziert werden, der den Writer-Zustand nie gesehen hat – und genau diese Trennung ist der einzige Grund, warum eine Konformitätsaussage etwas wert ist