Technischer Artikel

Umgang mit Hybrid-Reference PDFs aus Office-Anwendungen in Delphi

Wenn Sie ein Dokument aus Microsoft Word oder Excel über "Als PDF speichern" exportieren, ist die Datei auf der Festplatte meistens eine Hybrid-Reference-Datei. Sie enthält ihre Querverweisinformationen zweimal: einmal als klassische Tabelle mit fester Breite, die jedes PDF bis Version 1.4 abschloss, und einmal als komprimierten Querverweis-Stream, von dem der größte Teil des Dokuments tatsächlich abhängt. Ein einziger Trailer-Schlüssel, /XRefStm, fügt die beiden Ansichten zusammen, und ob ein Tool das gesamte Dokument sieht, hängt davon ab, ob es diesem Schlüssel folgt

Dieser Artikel betrachtet Hybrid-Dateien von der konsumierenden Seite: wie die Bytes am Ende der Datei aussehen, wie die beiden Ansichten bei der Bearbeitung auseinanderdriften und wie eine Delphi-Pipeline hybride Eingaben erkennen und weiterleiten kann. Wie ein Loader die Ansichten zusammenführt und warum die Reihenfolge nicht verhandelbar ist, ist das Thema unseres HotPDF-Artikels über das Laden von Hybrid-Reference-Dateien; hier geht es in erster Linie um das Erkennen des Layouts

Warum Office-Exporte den Index zweimal schreiben

PDF 1.5 führte zwei Funktionen ein, die die Struktur der Datei veränderten: Querverweis-Streams, die den Objektindex als komprimierte Binärdaten anstelle einer Klartext-Tabelle speichern, und Objekt-Streams, die viele kleine Objekte in einen Flate-komprimierten Container packen. Ein Writer, der diese verwendet, erzeugt kleinere Dateien, aber ein PDF 1.4-Reader kann das Ergebnis nicht öffnen, weil die Strukturen, auf die er angewiesen ist, das Schlüsselwort xref und das trailer-Wörterbuch, verschwunden sind

ISO 32000-1 §7.5.8.4 definiert den Kompromiss. Eine Hybrid-Reference-Datei schreibt beides: eine klassische Querverweistabelle, die die Objekte adressiert, die ein alter Reader erreichen muss, darunter den Katalog und den Seitenbaum, sowie einen Querverweis-Stream, der alles andere indiziert. In Objekt-Streams zusammengefasste Objekte werden in der klassischen Tabelle als frei markiert, sodass ein 1.4-Reader sie klaglos überspringt; ihre tatsächlichen Speicherorte existieren nur im Stream. Der klassische Trailer trägt dann einen /XRefStm-Schlüssel, der den Byte-Offset dieses Streams enthält. Ein alter Viewer liest den Schlüssel nie und rendert die Datei anhand der Tabellenansicht. Ein moderner Viewer folgt ihm und sieht das komplette Dokument. Word und Excel geben genau dieses Layout seit Jahren aus, weshalb Hybrid-Dateien kein exotischer Sonderfall sind, sondern einen großen Teil dessen ausmachen, was Business-Pipelines empfangen

Wie das Ende einer Hybrid-Datei aussieht

Das Layout ist am einfachsten anhand der Bytes zu verstehen. Hier ist das Ende einer kleinen Hybrid-Datei, Offsets gekürzt; in einem echten Office-Export ist der Wert /XRefStm typischerweise ein großer Offset nahe dem Ende der Datei. Die Lesereihenfolge ist der Bottom-Up-Durchlauf, der in unserem Überblick über die PDF-Dateistruktur beschrieben wird: finden Sie %%EOF, lesen Sie startxref, springen Sie zur Tabelle

% ... body objects, including object streams and, at byte 116,
% the cross-reference stream (a stream object with /Type /XRef) ...

xref                    % classic section: what startxref points at
0 4
0000000000 65535 f      % slot 0: head of the free list, always present
0000000017 00000 n      % object 1: the catalog, visible to any reader
0000000000 65535 f      % object 2: marked free -- lives in an object stream
0000000000 65535 f      % object 3: same; only the stream view locates it
trailer
<<
  /Size 4
  /Root 1 0 R
  /XRefStm 116          % byte offset of the cross-reference stream
>>
startxref
7164                    % byte offset of the 'xref' keyword above
%%EOF

Zwei Details in diesem Dump tragen den gesamten Mechanismus. Erstens zeigt startxref absichtlich auf den klassischen Abschnitt: das ist die Adresse, bei der ein alter Reader landen muss. Der Querverweis-Stream ist nur über den Schlüssel /XRefStm innerhalb des Trailer-Wörterbuchs erreichbar. Ein Parser, der nie nach diesem Schlüssel sucht, erfährt also nie, dass der Stream existiert. Zweitens sind die Objekte 2 und 3 Lügen der harmlosen Art. Die klassische Tabelle deklariert sie als frei, aber sie sind reale Objekte, die in einem komprimierten Container sitzen; die Markierung als frei bewahrt einen 1.4-Reader davor, über Einträge zu stolpern, die er nicht verwenden kann. Ein Konsument, der nur der klassischen Ansicht vertraut, kommt zu dem Schluss, dass der größte Teil dieses Dokuments nicht existiert

Wie die beiden Ansichten auseinanderdriften

Eine Hybrid-Datei frisch aus Word ist intern konsistent: Beide Ansichten beschreiben dasselbe Dokument, jede innerhalb ihres deklarierten Bereichs. Das Problem beginnt, wenn die Datei von einem Tool bearbeitet wird, das nur eine der Ansichten versteht. Stellen Sie sich ein Stempel-Dienstprogramm vor, das ein inkrementelles Update im klassischen Stil anhängt: neue Objekte, ein neuer xref-Abschnitt, eine /Prev-Kette zum vorherigen Abschnitt und ein neuer Trailer. Wenn dieser Trailer den Schlüssel /XRefStm weglässt, wird die Stream-Ansicht verwaist; wenn er den alten Wert vorwärts kopiert, beschreibt die Stream-Ansicht immer noch das Dokument, wie es vor der Bearbeitung war. In beiden Fällen stimmen die beiden Indizes nun nicht mehr darüber überein, was die Datei enthält

Die resultierende Datei weist eine charakteristische Fehlersignatur auf: Objekte, die in einer Ansicht sichtbar sind, fehlen in der anderen oder sind veraltet. Ein Reader, der über die Stream-Ansicht auflöst, findet die Version vor der Bearbeitung eines aktualisierten Objekts, oder gar keinen Eintrag für ein neu angehängtes. Ein Reader, der die Tabellenansicht nutzt, sieht die Bearbeitung, verliert jedoch den Überblick über die komprimierten Objekte, die nur der Stream lokalisiert. In der Praxis äußert sich dies als Formularfelder, die in einem Viewer überleben und in einem anderen verschwinden, Anmerkungen, die ein Stempel-Durchgang gelöscht zu haben scheint, oder Lookups, die beim völlig falschen Objekt landen

Was die Fehlersuche bei diesen Dateien so aufwendig macht, ist, dass Adobe Acrobat sie normalerweise ohne Murren öffnet: Wenn der Index nicht mit den Bytes übereinstimmt, rekonstruiert er stillschweigend die Querverweisdaten durch Scannen nach Objekt-Headern. Wer also die kaputte Datei erzeugt hat, sieht nichts Falsches. Der Fehler tritt erst später zutage, wenn die Datei einen strengen Konsumenten, einen Preflight-Validator, einen Signaturdienst oder einen Archivierungs-Ingest-Job erreicht, der der deklarierten Struktur vertraut und fehlende Objekte oder eine Nichtübereinstimmung der Querverweise meldet. "Es lässt sich in Acrobat einwandfrei öffnen" ist der Beginn fast jedes Tickets über hybride Desynchronisation

Erkennung einer Hybrid-Datei in purem Delphi

Für die Klassifizierung von Eingaben ist keine PDF-Bibliothek erforderlich. Der Schlüssel /XRefStm kann nur innerhalb eines klassischen Trailer-Wörterbuchs vorkommen, und der aktive Trailer befindet sich in den letzten paar Kilobytes der Datei, da die Spezifikation vorschreibt, dass %%EOF nahe dem physischen Ende stehen muss. Das Einlesen und Durchsuchen eines begrenzten End-Fensters reicht für die Triage aus:

uses
  System.SysUtils, System.Classes, System.StrUtils, System.Math;

function IsHybridReferencePdf(const FileName: string): Boolean;
const
  TailWindow = 2048;
var
  Stream: TFileStream;
  Buf: TBytes;
  Tail: string;
  Len, TrailerPos, NextPos, KeyPos, StartXrefPos: Integer;
begin
  Result := False;
  Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    if Stream.Size < 48 then
      Exit;
    Len := Min(TailWindow, Integer(Stream.Size));
    SetLength(Buf, Len);
    Stream.Position := Stream.Size - Len;
    Stream.ReadBuffer(Buf[0], Len);
  finally
    Stream.Free;
  end;

  // Every keyword involved is 7-bit ASCII, so a byte-wise decode is safe
  Tail := TEncoding.ANSI.GetString(Buf);

  // Find the LAST 'trailer' keyword: with incremental updates,
  // the newest trailer is the one that governs the file
  TrailerPos := 0;
  NextPos := Pos('trailer', Tail);
  while NextPos > 0 do
  begin
    TrailerPos := NextPos;
    NextPos := PosEx('trailer', Tail, NextPos + 1);
  end;
  if TrailerPos = 0 then
    Exit;  // no classic trailer: a pure xref-stream file, not hybrid

  // A hybrid trailer carries /XRefStm between 'trailer' and 'startxref'
  KeyPos := PosEx('/XRefStm', Tail, TrailerPos);
  StartXrefPos := PosEx('startxref', Tail, TrailerPos);
  Result := (KeyPos > 0) and
    ((StartXrefPos = 0) or (KeyPos < StartXrefPos));
end;

Die drei Ergebnisse stimmen mit den drei Layouts überein. Eine rein klassische Datei hat einen Trailer, aber kein /XRefStm: False. Eine Datei, die vollständig auf Querverweis-Streams setzt, hat überhaupt kein Schlüsselwort trailer, ihre Trailer-Schlüssel leben im Stream-Wörterbuch: ebenfalls False, korrekterweise, weil eine solche Datei komprimiert und nicht hybrid ist. Nur das doppelt indizierte Layout gibt True zurück

Für den produktiven Einsatz sind zwei Härtungen die zusätzlichen Zeilen wert. Parsen Sie die Ganzzahl (Integer) nach /XRefStm, springen Sie zu diesem Offset und vergewissern Sie sich, dass dort tatsächlich ein Stream-Objekt mit /Type /XRef sitzt. Eine abgeschnittene Datei kann den Schlüssel enthalten, während der Stream fehlt, was in eine andere Kategorie gehört als ein gesundes Hybrid. Behandeln Sie außerdem die Fenstergröße als Parameter: 2 KB decken gewöhnliche Office-Ausgaben ab, aber ein ungewöhnlich großes Trailer-Wörterbuch kann das Schlüsselwort aus dem Bereich schieben, und eine Vergrößerung des Fensters ist besser, als die Datei versehentlich als klassisch zu deklarieren

Routing von Hybrid-Dateien durch eine Delphi-Pipeline

Die Erkennung ermöglicht Ihnen eine Routing-Entscheidung. Für Dateien, die nur gelesen, gerendert oder validiert werden, verwenden Sie einen Loader, der beide Ansichten auflöst, und überprüfen Sie dann das Verhalten anstatt der Bytes. Die PDFium-Komponente parst die /XRefStm-Kette während des Ladens, sodass die Objekttabelle, die Ihr Code sieht, die zusammengeführte ist, und die in unserem Artikel zur Validierung von Objekt- und Querverweis-Streams beschriebenen Prüfungen gelten unverändert. Wenn ein desynchronisiertes Hybrid so stark beschädigt ist, dass das Laden verweigert wird, meldet die Engine dies durch ihr Fehler-Set (FPDF_ERR_SUCCESS, FPDF_ERR_UNKNOWN, FPDF_ERR_FILE, FPDF_ERR_FORMAT, FPDF_ERR_PASSWORD, FPDF_ERR_SECURITY und FPDF_ERR_PAGE), wobei FPDF_ERR_FORMAT dasjenige ist, das auf strukturelle Schäden hinweist. Verlassen Sie sich jedoch nicht auf dieses Signal: PDFium ist absichtlich tolerant und rekonstruiert die meisten inkonsistenten Dateien im Stillen. Ein erfolgreiches Laden beweist also nur, dass die Datei wiederherstellbar war, nicht, dass ihre beiden Ansichten übereinstimmen. Die aussagekräftige Konsistenzprüfung besteht darin, das zu vergleichen, was ein vollständiger Objekt-Durchlauf findet, mit dem, was /Size des Trailers deklariert

Für Dateien, die Ihre Pipeline ändert, ist es am sichersten, sie gar nicht erst hybrid sein zu lassen. Ein Laden gefolgt von einem vollständigen Speichern über HotPDF schreibt das Dokument mit einem einzigen, selbstkonsistenten Querverweis in einer einzigen Form neu: kein /XRefStm, keine zweite Ansicht, die asynchron werden könnte, jedes Objekt gehört zu genau einem Indexeintrag. Diese Normalisierung ist das, was Sie vor einem Archiv-Ingest, vor einem strengen nachgelagerten RIP oder Signaturdienst und nach jeder Bearbeitung einer hybriden Eingabe wünschen. Dies funktioniert, weil der Loader die Ansichten beim Einlesen korrekt zusammengeführt hat — der Mechanismus, den der HotPDF-Artikel über Hybrid-Referenzen im Detail durchgeht

Die einzige Klasse von Dateien, die Sie in Ruhe lassen sollten, sind digital signierte Dokumente. Ein vollständiges Neuschreiben verschiebt jedes Byte, was jede Signatur ungültig macht, die über die ursprünglichen Bereiche berechnet wurde. Eine Änderung an einem signierten Hybrid muss als echtes inkrementelles Update erfolgen, das beide Ansichten beibehält; eine Datei, die nur gelesen werden muss, sollte unangetastet durchgeleitet werden. Normalisierung ist für Dateien gedacht, die Ihnen gehören; bei signierten Dateien hängen Sie immer nur an

Hybrid-Reference PDFs sind nicht fehlerhaft; sie sind die formatspezifische Kompatibilitätsbrücke, und Office-Anwendungen werden sie so lange produzieren, wie PDF 1.4-Reader in der Installationsbasis überleben. Eine Pipeline, die den Schlüssel /XRefStm erkennen, das zusammengeführte Dokument mit der PDFium-Komponente validieren und mit der HotPDF-Komponente eine saubere Ausgabe mit nur einem Index regenerieren kann, behandelt sie als das, was sie sind: gewöhnliche Eingaben mit einem zusätzlichen Wegweiser im Trailer