Technischer Artikel

Welchen Standard gibt dieses PDF an? Erkennen in Delphi

Ihr Erkennungscode meldet, dass eine PDF/UA-Datei eines anderen Producers nichts beansprucht, und ein PDF/R-Scan hört auf, PDF/R zu sein, sobald ein Editor eine Notiz anhängt. HotPDF liest diese Ansprüche aus einem geladenen Dokument über Accessoren wie IsLoadedPDFUA, GetLoadedPDFR1Version und GetLoadedPDFDeclarations, von denen jeder genau dort nachsieht, wo sein eigener Standard die Kennzeichnung ablegt

Wo jeder PDF-Standard seine Kennzeichnung ablegt

Die Nebenstandards haben sich nie auf einen einzigen Ort für den Anspruch geeinigt. Die meisten nutzen XMP-Eigenschaften im /Metadata-Stream des Catalogs, einige ältere verlassen sich weiter auf Schlüssel im Document-Info-Dictionary, PDF/R legt eine Kommentarzeile neben den Trailer, und die PDF-2.0-Generation kommt mit zwei Mechanismen mehr: Developer-Extension-Wörterbüchern im Catalog und PDF Declarations innerhalb von XMP. Eine Kennzeichnung ist die Erklärung der Datei über sich selbst; ob der Inhalt sie einlöst, entscheidet ein Validator, und konforme PDF/A-, PDF/X- und PDF/UA-Ausgabe zu erzeugen ist ein Thema für sich. Fürs Routing (Druckwarteschlange für PDF/X, Archiv für PDF/A, Barrierefreiheits-Pipeline für WTPDF) brauchen Sie genau den Anspruch, gelesen am richtigen Ort:

StandardWo der Anspruch liegtHotPDF-Accessor
PDF/A (ISO 19005)XMP pdfaid:part und pdfaid:conformance, plus pdfaid:rev in PDF/A-4IsLoadedPDFA, GetLoadedPDFAVersion, GetLoadedPDFARevision
PDF/X (ISO 15930)GTS_PDFXVersion im Info-Dictionary für PDF/X-1a und PDF/X-3, in XMP für die späteren TeileIsLoadedPDFX, GetLoadedPDFXVersion
PDF/UA (ISO 14289)XMP pdfuaid:partIsLoadedPDFUA, GetLoadedPDFUAVersion
PDF/VT (ISO 16612-2)XMP pdfvtid:GTS_PDFVTVersionIsLoadedPDFVT, GetLoadedPDFVTVersion
PDF/E (ISO 24517-1)ISO_PDFEVersion, gelesen aus Info-Dictionary, Catalog oder XMPIsLoadedPDFE, GetLoadedPDFEVersion
PDF/R-1 (ISO 23504-1)%PDF-raster-x.y-Kommentar direkt über dem letzten startxrefIsLoadedPDFR1, GetLoadedPDFR1Version
PDF/VCR-1 (ISO 16613-1)GTS_PDFVCRVersion im Catalog oder in XMPIsLoadedPDFVCR, GetLoadedPDFVCRVersion
WTPDF 1.0, ISO/TS 32005pdfd:declarations-Einträge in XMPIsLoadedWTPDF, GetLoadedWTPDFVersion, IsLoadedISO32005
PDF-2.0-Developer-Extensions/Extensions /ISO_ im CatalogHasLoadedISODeveloperExtension, GetLoadedISODeveloperExtensionLevels
uses
  System.SysUtils, HPDFDoc;

procedure ReportClaims(const FileName: string; const Password: AnsiString);
var
  Pdf: THotPDF;
  PDFX, PDFXLevel: AnsiString;
begin
  Pdf := THotPDF.Create(nil);   // eine eigene Instanz, nur für die Inspektion
  try
    // Die Passwort-Überladung entschlüsselt die Streams, das XMP-Paket mit;
    // '' übergeben für Dateien, die ohne Nachfrage öffnen
    if Pdf.LoadFromFile(FileName, Password) <= 0 then
      raise Exception.CreateFmt('Cannot open %s', [FileName]);

    if Pdf.IsLoadedPDFA then
      Writeln('PDF/A: ', Pdf.GetLoadedPDFAVersion);        // z. B. PDF/A-2b
    PDFX := Pdf.GetLoadedPDFXVersion;                       // XMP, dann Info
    if PDFX <> '' then
    begin
      PDFXLevel := Pdf.GetLoadedInfoKeyValue('GTS_PDFXConformance');
      if PDFXLevel <> '' then
        Writeln('PDF/X: ', PDFX, ', conformance ', PDFXLevel)
      else
        Writeln('PDF/X: ', PDFX);
    end;
    if Pdf.IsLoadedPDFUA then
      Writeln('PDF/UA: ', Pdf.GetLoadedPDFUAVersion);      // z. B. PDF/UA-1
    if Pdf.IsLoadedPDFVT then
      Writeln('PDF/VT: ', Pdf.GetLoadedPDFVTVersion);
    if Pdf.IsLoadedPDFE then
      Writeln('PDF/E: ', Pdf.GetLoadedPDFEVersion);
    if Pdf.IsLoadedPDFR1 then
      Writeln('PDF/R: ', Pdf.GetLoadedPDFR1Version);       // z. B. PDF/R-1.0
    if Pdf.IsLoadedWTPDF then
      Writeln('WTPDF: ', Pdf.GetLoadedWTPDFVersion);
  finally
    Pdf.Free;
  end;
end;

Nutzen Sie für die Inspektion eine eigene THotPDF-Instanz, wie das Beispiel es tut. Die IsLoaded…-Funktionen antworten für jedes Dokument, das die Komponente gerade hält, auch für eines, das sie gerade aufbaut: IsLoadedPDFUA ist ebenfalls True, wenn PDFUACompliance für die Ausgabe eingeschaltet ist, und IsLoadedPDFX deckt sich mit einem nichtleeren PDFXCompliance. Mitten im Aufbau praktisch, aber auf einer Komponente, die beide Jobs erledigt, mischt das Ihre Ausgabeeinstellungen in das Urteil über die Datei eines anderen

PDF/X ist die eine Familie, deren Kennzeichnung zwischen den Teilen umgezogen ist. PDF/X-1a und PDF/X-3 sind PDF-1.3-Profile, die GTS_PDFXVersion ins Document-Info-Dictionary legen und gar kein XMP brauchen, während PDF/X-4 und die späteren Teile die XMP-Eigenschaft pdfx:GTS_PDFXVersion verlangen. GetLoadedPDFXVersion sucht zuerst im XMP, unter dem Präfix pdfx oder pdfxid, dann im Info-Dictionary, dann in den Catalog-Schlüsseln, die einige ältere Producer schreiben, und liefert den deklarierten Wert unverändert zurück. PDF/X-1a hat eine Marotte, die man im Code bedenken sollte: Ihr Versionswert ist PDF/X-1:2001, und die Konformitätsstufe PDF/X-1a:2001 wohnt in einem eigenen Schlüssel /GTS_PDFXConformance, den das Beispiel mit GetLoadedInfoKeyValue liest, wenn es X-1a von schlichtem X-1 unterscheiden muss

Wo jeder PDF-Standard seine Konformitätskennzeichnung ablegt und wie HotPDF für Delphi in einem geladenen PDF XMP, Info-Dictionary und Catalog-Schlüssel durchsucht
Jeder Standard legt seinen Anspruch dort ab, wo seine Spezifikation es entschied, und der Reader durchsucht die Orte, zwischen denen ein Anspruch umziehen kann, in fester Reihenfolge

Wie sieht ein gültiger XMP-Anspruch aus?

XMP ist als XML serialisiertes RDF, und RDF/XML erlaubt, eine einfache Eigenschaft in einem rdf:Description auf zwei Arten zu schreiben: als Kindelement, <pdfuaid:part>1</pdfuaid:part>, oder als Attribut an der Beschreibung selbst, pdfuaid:part="1", in einfachen oder doppelten Anführungszeichen. ISO 16684-1 akzeptiert beide Formen, und ein Paket darf seine Eigenschaften über mehrere rdf:Description-Elemente verteilen. Die Aufteilung ist nicht hypothetisch. HotPDF schreibt die Kindelement-Form für PDF/UA, PDF/VT, PDF/E und PDF/X, während der veraPDF-Testkorpus und viele externe Producer die Kurzschreibweise verwenden, mit Beschreibungen wie pdfuaid:part="1" oder pdfvtid:GTS_PDFVTVersion="PDF/VT-1", die den ganzen Anspruch in Attributen tragen. Ein Detektor, der um Pos('<pdfuaid:part>', Xmp) gebaut ist und nur mit eigener Ausgabe gefüttert wurde, besteht jede lokale Prüfung und stuft dann das erste veraPDF-Sample als unbezeichnet ein. HotPDFs XMP-Reader beherrscht beide Formen für jeden Accessor in der Tabelle. GetLoadedPDFAVersion kombiniert pdfaid:part und pdfaid:conformance zu Kennungen wie PDF/A-1b, PDF/A-2u, PDF/A-4 oder PDF/A-4f, und GetLoadedPDFARevision liefert pdfaid:rev, das Ausgabejahr, das PDF/A-4 verlangt (2020) und das die Teile 1 bis 3 nicht definieren:

procedure ReportPDFALevel(Pdf: THotPDF);
var
  Level, Revision: AnsiString;
begin
  Level := Pdf.GetLoadedPDFAVersion;      // 'PDF/A-1b', 'PDF/A-2u', 'PDF/A-4f', ...
  if Level = '' then
    Exit;
  Revision := Pdf.GetLoadedPDFARevision;  // '2020' für PDF/A-4, leer für die Teile 1-3
  if Revision <> '' then
    Writeln(Level, ', edition ', Revision)
  else
    Writeln(Level);
  // Erwartet: "PDF/A-2b", egal ob der Producer
  // <pdfaid:part>2</pdfaid:part> oder pdfaid:part="2" geschrieben hat
end;

Die Präfixe sind der andere bewegliche Teil. In XML ist das Präfix nur ein lokaler Alias; was das PDF/A-Schema identifiziert, ist der Namespace-URI http://www.aiim.org/pdfa/ns/id/, den genehmigte Errata zu ISO 19005-4 auch in seiner https://-Schreibweise zulassen, und ein Producer darf ihn an jedes Präfix binden, das ihm gefällt. GetLoadedPDFAVersion schlägt das Präfix nach, das das Paket für diesen URI deklariert, bevor es Teil und Stufe liest, also identifiziert sich ein Paket, das pdfa:part statt pdfaid:part schreibt, trotzdem. Jede andere XMP-Eigenschaft, die Sie selbst aus dem dekodierten Paket lesen, das GetLoadedXMPMetadata zurückgibt, verdient dieselbe Sorgfalt: beide Serialisierungen, das Präfix über seine xmlns-Deklaration aufgelöst und Whitespace um das Gleichheitszeichen erlaubt

Zwei gültige XMP-Serialisierungen eines PDF/UA-Anspruchs neben der Namespace-Regel, die einem umbenannten Präfix erlaubt, für HotPDF in Delphi trotzdem PDF/A zu identifizieren
Kindelement- und Attribut-Schreibweisen sind gleichermaßen gültig, und der Namespace-URI identifiziert das Schema, nicht das Präfix

Verschlüsselte Dateien verbergen den Anspruch vor Bytesuchen

Ein Scan über die Rohdatei nach pdfaid oder ISO_PDFEVersion überspringt das Parsen und scheitert an zwei ganz gewöhnlichen Fällen. Die meisten XMP-Streams sind Flate-komprimiert, die Eigenschaftsnamen tauchen also nie als Text auf der Platte auf. Und in einem verschlüsselten Dokument ist der Metadata-Stream Chiffretext: ISO 32000-2 §7.6.4.2 definiert EncryptMetadata mit dem Standardwert true, also ist das Paket, sofern der Producer nicht ausdrücklich abgewählt hat, zusammen mit jedem anderen Stream verschlüsselt. Eine Bytesuche gegen eine verschlüsselte PDF/E-1-Datei meldet den ISO_PDFEVersion-Anspruch daher als fehlend, obwohl er da ist. Die verlässliche Reihenfolge ist laden, entschlüsseln, dekodieren, dann suchen. Öffnen Sie die Datei über die Passwort-Überladung LoadFromFile(FileName, Password), und übergeben Sie einen leeren String für Dateien, die nur ein Owner-Passwort tragen und in jedem Viewer ohne Nachfrage öffnen. Das einargumentige LoadFromFile(FileName) lädt die Dokumentstruktur, ohne ihre Streams zu entschlüsseln, was das XMP-Paket unlesbar lässt; nach der Passwort-Überladung arbeiten GetLoadedXMPMetadata und jeder Accessor von oben auf dem entschlüsselten, dekomprimierten Paket

Warum verliert eine PDF/R-Datei nach einer Bearbeitung ihre Identität?

PDF/R-1 identifiziert sich auf eine Art, wie es kein anderer Nebenstandard tut. ISO 23504-1, Abschnitt 5, verlangt eine Kommentarzeile mit %PDF-raster-1.0 (oder der jeweils geltenden Version) unmittelbar vor der letzten Zeile, die startxref enthält. Ein PDF-Parser wirft Kommentare weg, während er die Querverweisdaten liest, also hat ein Objektmodell nichts zu melden; der Beweis existiert nur in den Rohbytes am Dateiende. GetLoadedPDFR1Version geht zurück zur Quelle, aus der das Dokument geladen wurde, liest höchstens die letzten 64 KiB, findet das letzte startxref und prüft die Zeile darüber. Nun vollziehe man ein inkrementelles Update nach. Ein Editor, der eine Annotation hinzufügt, hängt neue Objekte, einen neuen Querverweisabschnitt, einen neuen Trailer und ein neues startxref hinter das ursprüngliche %%EOF. Die %PDF-raster-1.0-Zeile ist weiter in der Datei, aber sie sitzt nun über dem vorletzten startxref, und der Standard lässt keinen Zweifel, welche zählt. Erwartung: IsLoadedPDFR1 liefert nach der Bearbeitung False, sofern das Bearbeitungswerkzeug den Kommentar nicht in seinem eigenen Update neu ausgibt. Ein Detektor, der die ganze Datei nach %PDF-raster- durchsucht, würde Ja sagen, und nach der Regel aus Abschnitt 5 ist diese Antwort falsch. Ein Capture-Workflow, der seine Dateien PDF/R halten muss, legt Annotationen in eine Sidecar-Datei oder in ein Werkzeug, das die ganze Datei als PDF/R neu schreibt

Dateienden vor und nach einem inkrementellen Update: Der PDF/R-Kommentar bleibt, verliert aber den letzten startxref-Anker, den HotPDF in Delphi prüft
Ein Editor, der ein Update anhängt, lässt den PDF/R-Kommentar über dem vorletzten startxref zurück, wo der Standard ihn nicht mehr zählt

Developer-Extensions: Ganzzahl-Level, Namen und Arrays

PDF-2.0-Dokumente künden die ISO-technischen Spezifikationen, auf die sie bauen (SHA-3-Hashes aus ISO/TS 32001, ECC-Signaturen aus ISO/TS 32002, glTF-Assets aus ISO/TS 32007), über das /Extensions-Wörterbuch des Catalogs, definiert in ISO 32000-2 §7.12. Unter dem registrierten Präfix /ISO_ erlaubt Tabelle 48 entweder ein einzelnes Developer-Extensions-Wörterbuch oder, neu in PDF 2.0, ein Array davon, und Tabelle 49 macht zusammen mit §7.12.5 /ExtensionLevel zu einer Ganzzahl. Ein Reader, der nur eine der beiden Containerformen beherrscht, verpasst jede Datei, die auf die andere Weise geschrieben wurde. HasLoadedISODeveloperExtension und GetLoadedISODeveloperExtensionLevels akzeptieren beide und lösen unterwegs indirekte Referenzen auf, obwohl §7.12.1 sagt, diese Wörterbücher sollen direkte Objekte sein:

procedure ReportISOExtensions(Pdf: THotPDF);
var
  Levels: THPDFIntegerArray;
  Revision, URL: AnsiString;
  I: Integer;
begin
  Levels := Pdf.GetLoadedISODeveloperExtensionLevels;
  for I := 0 to High(Levels) do
    Writeln('ISO_ extension level ', Levels[I]);

  if Pdf.HasLoadedISODeveloperExtension(32001, Revision, URL) then
    Writeln('ISO/TS 32001 revision ', Revision, ', documented at ', URL);
  // Erwartet für eine Datei, die mit EnsureISO32001Extensions geschrieben wurde:
  //   ISO/TS 32001 revision :2022, documented at https://www.iso.org/standard/45874.html
  if Pdf.HasLoadedISODeveloperExtension(32007) then
    Writeln('glTF 3D assets per ISO/TS 32007');
end;

Die erste Ausgabe von ISO/TS 32007:2024 enthält in §4.8 ein Beispiel, das /ExtensionLevel /32007 als Namen schreibt; PDF Errata Issue #835 hat es zur Ganzzahl 32007 korrigiert, aber ein Producer, der das Originalbeispiel abgeschrieben hat, schreibt weiter den Namen. HotPDFs Reader fällt deshalb auf das Parsen eines Namens oder String-Werts zurück, mit oder ohne führenden Schrägstrich, wenn die Ganzzahl-Suche leer ausgeht, also findet HasLoadedISODeveloperExtension(32007) beide Schreibweisen. Das andere Erratum betrifft /URL: Tabelle 49 in ISO 32000-2:2020 führt /URL als optional, und Issue #732 stuft es auf erforderlich. Auf der Schreibseite gibt HotPDF in jedem EnsureISO…Extensions-Aufruf die Ganzzahl-Form und ein /URL aus; auf der Leseseite ist ein leerer URL-Out-Parameter für Dateien normal, die nach dem Text von 2020 geschrieben wurden, also behandeln Sie ihn als Information und niemals als Grund, die Extension abzulehnen

Welchen WTPDF-URI hat der Producer geschrieben?

WTPDF-1.0- und ISO/TS-32005-Ansprüche sind PDF Declarations: Einträge in einem pdfd:declarations-Bag in XMP, unter dem Namespace http://pdfa.org/declarations/, den die PDF Declarations Technical Note der PDF Association definiert. Jeder Eintrag trägt einen conformsTo-URI und optionale Felder claimBy, claimDate, claimCredentials und claimReport, die GetLoadedPDFDeclarations als Array von THPDFDeclarationEntry-Records zurückgibt; auf der Schreibseite legt AddPDFDeclaration dieselben fünf Felder ins XMP-Paket, neben den PDF-2.0-, PDF/A-4- und PDF/UA-2-Identifikations-Namespaces. Das Bag per simpler Textsuche in Einträge zu schneiden hat eine eigene Falle: Trennt man an jedem Vorkommen von conformsTo, zählt das schließende Tag </pdfd:conformsTo> der Kindelement-Form als Anfang eines neuen Eintrags, was die erste Deklaration vor ihrem eigenen Ende abschneidet. Der HotPDF-Reader steigt über schließende Tags hinweg und akzeptiert beide Serialisierungen

Die URIs selbst sind der härtere Teil. Derselbe Anspruch erreicht Sie als http:// oder https://, und WTPDF 1.0 Issue #395 hat daran geändert, ob ein / vor dem # sitzt, also unterscheiden sich Dateien vor und nach dem Erratum in genau diesem einen Zeichen; ein schlichter Stringvergleich behandelt sie als verschiedene Standards. HasLoadedPDFDeclaration vergleicht URIs ohne Groß-/Kleinschreibung, faltet https:// auf http:// zusammen und streicht ein / direkt vor # auf beiden Seiten, also passt die Abfrage unten auf jede Schreibweise, die der Producer gewählt hat. Das Fragment bleibt intakt: #accessibility1.0 und #accessibility1.0-validated sind verschiedene Ansprüche, der zweite behauptet, ein Validator habe die Datei geprüft, und Sie sollten den anfordern, um den Ihr Workflow sich schert. GetLoadedWTPDFVersion faltet beide zusammen zu 'WTPDF 1.0 Accessibility' oder 'WTPDF 1.0 Reuse' fürs Routing

procedure ReportDeclarations(Pdf: THotPDF);
var
  Decls: THPDFDeclarationEntryArray;
  I: Integer;
begin
  Decls := Pdf.GetLoadedPDFDeclarations;
  for I := 0 to High(Decls) do
    Writeln(Decls[I].ConformsTo, ' claimed by ', Decls[I].ClaimBy,
      ' on ', Decls[I].ClaimDate);

  // Trifft auf http oder https zu, mit oder ohne den Schrägstrich vor '#'
  if Pdf.HasLoadedPDFDeclaration(
    'https://pdfa.org/declarations/wtpdf#accessibility1.0') then
    Writeln('WTPDF 1.0 Accessibility (unvalidated claim)');

  // Deckt sowohl die schlichten als auch die -validierten Varianten von Reuse und Accessibility ab
  Writeln('WTPDF profile: ', Pdf.GetLoadedWTPDFVersion);
end;

Für einen Capture- oder Print-Router zählt die Reihenfolge der Operationen mehr als jeder einzelne Accessor: Prüfen Sie IsLoadedPDFR1 an der Datei, wie sie ankam, bevor ein Annotations- oder Stamping-Schritt sein Update anhängt, und routen Sie PDF/X über GetLoadedPDFXVersion, das das Info-Dictionary von PDF/X-1a- und PDF/X-3-Jobs wie das XMP von PDF/X-4-Jobs abdeckt und /GTS_PDFXConformance nur dort liest, wo X-1a von schlichtem X-1 zu unterscheiden ist. Beide Prüfungen laufen in jedem Dokument, das in die HotPDF-Delphi-Komponente geladen ist, auch in verschlüsselten Dateien, die mit ihrem Passwort geöffnet wurden