Technical Article

Laden von Hybrid-Referenz-PDFs aus Word und Excel in Delphi

Öffnen Sie ein PDF, das mit Microsoft Word oder Excel erstellt wurde, blättern Sie hindurch, und nichts sieht ungewöhnlich aus. Laden Sie es in ein Delphi-Programm, lesen Sie die Seitenzahl aus, und die Zahl stimmt. Speichern Sie es dann mit aktivierter Verschlüsselung erneut ab, und der Vorgang schlägt mit einem EListError fehl oder die Ausgabe öffnet sich mit einer Warnung vor einer beschädigten Kreuzreferenz. Die Datei war nie beschädigt. Es handelt sich um eine Hybrid-Referenz-Datei, und genau die Struktur, die es einem fünfzehn Jahre alten Viewer ermöglicht, sie zu öffnen, bringt einen Lader zu Fall, der zu früh mit dem Lesen aufhört

Dies ist einer der häufigsten Wege, wie eine PDF-Pipeline, die jeden internen Test bestanden hat, auf eine Datei stößt, die sie nicht im Round-Trip verarbeiten kann. Die Testeingaben wurden alle intern generiert und waren daher nie hybrid. Die erste hybride Datei trifft an dem Tag ein, an dem ein Kunde eine aus einer Tabellenkalkulation exportierte Rechnung weiterleitet

Was Word und Excel tatsächlich schreiben

ISO 32000-1 beschreibt das Hybrid-Referenz-Layout in §7.5.8.4. Eine Anwendung, die PDF 1.5-Funktionen wie Objekt-Streams nutzen möchte, während sie gleichzeitig einem PDF 1.4-Reader das Öffnen der Datei ermöglicht, schreibt die Kreuzreferenzinformationen zweimal. Es gibt eine klassische Kreuzreferenztabelle, die ASCII-Zeilen mit fester Breite, die jedes PDF bis Version 1.4 beendeten, und es gibt einen Kreuzreferenz-Stream, der den Rest indiziert. Der Trailer des klassischen Abschnitts enthält einen /XRefStm-Eintrag, dessen Wert der Byte-Offset dieses Streams ist

Die Arbeitsteilung ist gewollt. Objekte, die ein alter Reader erreichen muss, darunter der Katalog und der Seitenbaum, sind über die klassische Tabelle adressierbar. Objekte, die in komprimierte Objekt-Streams ausgelagert wurden, sind in der klassischen Tabelle als frei markiert (mit einem Eintrag vom Typ f), sodass ein 1.4-Reader sie einfach überspringt und nie über eine Struktur stolpert, die er nicht parsen kann. Ihre tatsächlichen Speicherorte befinden sich ausschließlich im Kreuzreferenz-Stream. Die Signatur einer solchen Datei ist ihr Ende: ein kurzer klassischer Abschnitt, oft nicht mehr als ein xref gefolgt von einem 0 0-Unterabschnitts-Header, dessen Trailer auf den /XRefStm verweist, in dem sich die tatsächlichen Wiederherstellungsdaten befinden

Warum eine korrekte Seitenzahl nichts beweist

Da der Katalog und der Seitenbaum auf Seitenebene absichtlich aus der klassischen Tabelle erreichbar sind, findet ein Lader, der nur diese Tabelle liest, den /Root, durchläuft den Seitenbaum und meldet die korrekte Anzahl von Seiten. Alles, was ein alter Reader benötigt, ist vorhanden, sodass die Datei fehlerfrei erscheint. Die Objekte, die verloren gingen, sind diejenigen, die in Objekt-Streams verpackt wurden: AcroForm-Feldwörterbücher, getaggte PDF-Strukturelemente und der lange Schwanz kleiner Wörterbücher, die für einen alten Viewer nie sichtbar sein mussten

Sie bemerken die Lücke erst, wenn etwas diese Objekte berührt, und ein vollständiges erneutes Speichern berührt sie alle. Das Durchlaufen des Dokuments zur erneuten Verschlüsselung oder zum Umschreiben ist genau der Vorgang, der jede Objektnummer der Reihe nach abfragt, weshalb das Symptom beim Speichern und nicht beim Laden auftritt, weit entfernt von seiner Ursache

Die Falle ist ein Detektor, der xref sieht und anhält

Der einfache Weg zu entscheiden, wie eine Datei indiziert ist, besteht darin, startxref zu folgen und die ersten Bytes zu prüfen, auf die es verweist. Das Schlüsselwort xref bedeutet eine klassische Tabelle; ein Stream-Objekt bedeutet einen Kreuzreferenz-Stream. Dieser Test ist korrekt für jede Datei, die sich auf ein einziges Schema festlegt. Er ist falsch für eine Hybrid-Datei, deren startxref ausschließlich aus dem Grund auf einen klassischen Abschnitt zielt, um alte Reader zufriedenzustellen, während der /XRefStm im Trailer dieses Abschnitts der Ort ist, an dem der größte Teil des Dokuments tatsächlich indiziert ist. Ein Detektor, der beim ersten angetroffenen xref "klassisch" zurückgibt, liest /XRefStm nie, und jedes Objekt, das nur im Stream existiert, wird unsichtbar

var
  Pdf: THotPDF;
  PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf');  // count is correct
    // inspect or edit the loaded document here
    Pdf.SaveLoadedDocument('Invoice_secured.pdf');     // walks every object
  finally
    Pdf.Free;
  end;
end;

Mit dem vorzeitigen Abbruch-Detektor sieht das Laden gut aus, und das erneute Speichern ist der Moment, in dem sich die fehlenden Objekte bemerkbar machen. Die Lösung besteht nicht darin, am Anfang mehr Bytes zu lesen; sie besteht darin, den hybriden Trailer zu erkennen und /XRefStm zu folgen, bevor entschieden wird, dass die Datei fertig geladen ist

Die Reihenfolge des Zusammenführens ist nicht verhandelbar

Sobald beide Indizes gelesen wurden, können sie nur in einer Richtung kombiniert werden. Der Kreuzreferenz-Stream muss zuerst zusammengeführt werden, wobei die klassischen Einträge darum herum eingefügt werden. Der Grund dafür ist die kleine Täuschung im Kern des Formats. Eine Hybrid-Datei markiert ihre komprimierten Objekte in der klassischen Tabelle als frei, damit alte Reader sie ignorieren. Ein Lader, der eine "Zuerst-Gesehen-Gewinnt"-Richtlinie befolgt und die klassische Tabelle zuerst liest, registriert diese Objektnummern als frei und verwirft dann die Stream-Einträge, die sie tatsächlich lokalisieren, da die Plätze bereits belegt sind. Kehrt man die Reihenfolge um, gewinnen die Typ-2-Einträge aus dem Stream (jeweils eine Objekt-Stream-Nummer plus ein Index) die Plätze, die sie besitzen sollen, und die klassischen Einträge ordnen sich darum herum an

Dieselbe Disziplin schützt davor, dass eine ältere Revision ein gelöschtes Objekt wiederbelebt. Inkrementelle Aktualisierungen verketten sich rückwärts über /Prev, und ein freier Eintrag vom Typ 0 ist ein Wächter dafür, dass ein neuerer Abschnitt eine Objektnummer außer Dienst gestellt hat. Einem späteren, älteren Abschnitt in der Kette darf es nicht gestattet werden, diesen Wächter mit einem veralteten Speicherort zu überschreiben. Behandeln Sie die erste Sichtung als maßgebend für freie Markierungen, und das gelöschte Objekt bleibt gelöscht; behandeln Sie dies unachtsam, erweckt die eigene Geschichte der Datei Inhalte wieder zum Leben, die die neueste Revision entfernt hat

Was dies in HotPDF bedeutet

Die Engine löst Hybrid-Referenz-Dateien für Sie auf, und zwar auf jedem Pfad, der die Kreuzreferenzdaten parsen muss. Laden Sie ein Dokument mit LoadFromFile oder LoadFromStream, nehmen Sie Ihre Änderungen vor und rufen Sie SaveLoadedDocument auf; oder führen Sie eine One-Shot-Operation wie EncryptFile aus, die eine Eingabe liest und eine Ausgabe schreibt. In jedem Fall liest die Wiederherstellung /XRefStm, führt den Stream-Abschnitt vor den klassischen Einträgen zusammen und löst die in Streams befindlichen Objekte auf, bevor der Schreibvorgang sie auflistet. Der AES-256-Verschlüsselungspfad ist der Ort, an dem sich das Problem zuerst zeigte, da das Verschlüsseln eines Dokuments jedes Objekt neu schreibt und somit verlangt, dass jedes Objekt bereits lokalisiert wurde

// One-shot: read the hybrid input, write an AES-256 encrypted copy
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
  'owner-secret', '', aes256, [prPrint, prFillAnnotations]);

Das Detail, das man sich merken sollte, liegt vor der API. Dateien, die von Word, Excel, PowerPoint und einer langen Liste von „Als PDF speichern“-Pipelines stammen, sind routinemäßig hybrid. Ein Lader, den Sie nur mit der Ausgabe Ihres eigenen Generators testen, wird in Tests möglicherweise nie auf eine solche stoßen. Bestücken Sie Ihre Testumgebungen mit Dokumenten, die aus echten Office-Anwendungen exportiert wurden, und nicht nur mit Dateien, die Ihr eigener Code erzeugt hat

Überprüfen einer verdächtigen Datei

Zwei Prüfungen klären die Frage schnell. Öffnen Sie die Datei in einer Hex-Ansicht und lesen Sie die Bytes nach dem letzten startxref; eine Hybrid-Datei zeigt einen kurzen klassischen Abschnitt, dessen Trailer-Wörterbuch /XRefStm enthält. Oder vergleichen Sie die Objektanzahl, die ein vollständiger Parse-Vorgang meldet, mit der höchsten Objektnummer, die /Size im Trailer deklariert. Eine große Lücke bedeutet, dass sich Objekte in Streams verstecken, die der Lader nicht geöffnet hat - was genau das Defizit ist, das sich später beim Speichern in einen Fehler verwandelt

Die Schreiberseite dieser Geschichte, also wie Objekt-Streams und komprimierte Kreuzreferenzen überhaupt erst erzeugt werden, wird in unserem Artikel über Objekt-Streams und inkrementelle Aktualisierungen behandelt. Wenn die fragliche Hybrid-Datei zudem sehr groß ist, ermöglichen es die Ladetechniken in der Direct File API-Anleitung für große PDF-Workflows, diese zu untersuchen, ohne das gesamte Dokument in den Speicher zu laden. Beide passen hervorragend zu der hier beschriebenen Wiederherstellung, die als Teil der HotPDF-Komponente für Delphi und C++Builder zusammen mit den APIs zum Laden, Bearbeiten, Verschlüsseln und Signieren ausgeliefert wird, die an anderer Stelle auf diesem Blog behandelt werden