Öffnen Sie ein PDF, das Microsoft Word oder Excel erstellt hat, blättern Sie hindurch, und nichts sieht ungewöhnlich aus. Laden Sie es in ein Delphi-Programm, lesen Sie die Seitenanzahl aus, und die Zahl stimmt. Speichern Sie es dann mit aktivierter Verschlüsselung erneut, und der Vorgang schlägt mit einem EListError fehl, oder die Ausgabe öffnet sich mit einer Warnung über eine beschädigte Querverweis-Tabelle (Cross-Reference). 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, ist die Struktur, an der ein Loader scheitert, der zu früh aufhört zu lesen
Dies ist eine der häufigsten Arten, wie eine PDF-Pipeline, die jeden internen Test bestanden hat, auf eine Datei trifft, die sie nicht im Roundtrip-Verfahren verarbeiten kann. Die Eingaben wurden alle intern generiert, waren also nie hybrid. Die erste Hybrid-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, aber dennoch zulassen will, dass ein PDF 1.4-Reader die Datei öffnet, schreibt die Querverweis-Informationen (Cross-Reference) zweimal. Es gibt eine klassische Querverweis-Tabelle, die ASCII-Zeilen mit fester Breite, die jedes PDF bis Version 1.4 abschlossen, und es gibt einen Querverweis-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 beabsichtigt. Objekte, die ein alter Reader erreichen muss, darunter der Katalog und der Seitenbaum (Page Tree), sind über die klassische Tabelle adressierbar. Objekte, die in komprimierte Objekt-Streams gepackt wurden, werden in der klassischen Tabelle mit einem Typ-f-Eintrag als frei markiert, sodass ein 1.4-Reader direkt darüber hinwegspringt und niemals über eine Struktur stolpert, die er nicht parsen kann. Ihre wahren Speicherorte befinden sich nur im Querverweis-Stream. Die Signatur einer solchen Datei ist ihr Ende: ein kurzer klassischer Abschnitt, oft nicht mehr als xref gefolgt von einem 0 0-Unterabschnitt-Header, dessen Trailer auf das /XRefStm verweist, wo sich die tatsächlichen Wiederherstellungsdaten befinden
Warum eine korrekte Seitenanzahl nichts beweist
Da der Katalog und der Seitenbaum absichtlich von der klassischen Tabelle aus erreichbar sind, findet ein Loader, der nur diese Tabelle liest, /Root, durchläuft den Seitenbaum und meldet die richtige Seitenanzahl. Alles, was ein alter Reader benötigt, ist vorhanden, sodass die Datei gesund erscheint. Die verloren gegangenen Objekte sind diejenigen, die in Objekt-Streams verpackt wurden: AcroForm-Feldwörterbücher, Tagged-PDF-Strukturelemente und die lange Liste kleiner Wörterbücher, die für einen veralteten 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, um es neu zu verschlüsseln oder neu zu schreiben, ist genau der Vorgang, der nacheinander nach jeder Objektnummer fragt, 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, um zu entscheiden, wie eine Datei indiziert ist, besteht darin, startxref zu folgen und die ersten Bytes zu prüfen, auf die es zeigt. Das Schlüsselwort xref bedeutet eine klassische Tabelle; ein Stream-Objekt bedeutet einen Querverweis-Stream. Dieser Test ist korrekt für jede Datei, die sich auf ein Schema festlegt. Er ist falsch für eine Hybrid-Datei, deren startxref ausschließlich auf einen klassischen Abschnitt abzielt, um alte Reader zufriedenzustellen, während die meisten Dokumentinhalte tatsächlich im /XRefStm im Trailer dieses Abschnitts indiziert sind. 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'); // Anzahl ist korrekt
// inspizieren oder bearbeiten Sie das geladene Dokument hier
Pdf.SaveLoadedDocument('Invoice_secured.pdf'); // durchläuft jedes Objekt
finally
Pdf.Free;
end;
end;
Ist ein Early-Exit-Detektor im Einsatz, sieht der Ladevorgang gut aus, und beim erneuten Speichern machen sich die fehlenden Objekte bemerkbar. Die Lösung besteht nicht darin, am Anfang mehr Bytes zu lesen; sie besteht darin, den Hybrid-Trailer zu erkennen und /XRefStm zu folgen, bevor man entscheidet, dass die Datei vollständig gelesen ist
Die Reihenfolge der Zusammenführung ist nicht verhandelbar
Sobald beide Indizes gelesen wurden, können sie nur in eine Richtung kombiniert werden. Der Querverweis-Stream muss zuerst zusammengeführt werden, wobei die klassischen Einträge darum herum eingefügt werden. Der Grund ist die kleine Täuschung im Herzen des Formats. Eine Hybrid-Datei markiert ihre komprimierten Objekte in der klassischen Tabelle als frei, damit alte Reader sie ignorieren. Ein Loader, der eine First-Seen-Wins-Richtlinie respektiert und zuerst die klassische Tabelle liest, verzeichnet diese Objektnummern als frei und verwirft dann die Stream-Einträge, die sie tatsächlich lokalisieren, da die Slots bereits belegt sind. Drehen Sie die Reihenfolge um, gewinnen die Typ-2-Einträge aus dem Stream (jeder eine Objekt-Stream-Nummer plus Index) die Slots, die ihnen zustehen, und die klassischen Einträge ordnen sich um sie herum an
Die gleiche Disziplin schützt davor, dass eine ältere Revision ein gelöschtes Objekt wiederauferstehen lässt. Inkrementelle Updates verketten sich rückwärts durch /Prev, und ein Typ-0-Freieintrag ist ein Wächter dafür, dass ein neuerer Abschnitt eine Objektnummer ausgemustert hat. Einem späteren, älteren Abschnitt in der Kette darf nicht erlaubt werden, diesen Wächter mit einem veralteten Ort zu überschreiben. Behandeln Sie First-Seen als maßgeblich für Freimarkierungen, und das gelöschte Objekt bleibt gelöscht; gehen Sie unachtsam damit um, reanimiert die eigene Historie der Datei Inhalte, die die neueste Revision entfernt hat
Was das für HotPDF bedeutet
Die Engine löst Hybrid-Referenz-Dateien für Sie auf, und zwar auf jedem Pfad, der die Querverweis-Daten 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 Objekte, die in Streams leben, auf, bevor das Schreiben sie enumeriert. 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 voraussetzt, dass jedes Objekt bereits lokalisiert wurde
// One-Shot: Lesen der hybriden Eingabe, Schreiben einer AES-256-verschlüsselten Kopie
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
'owner-secret', '', aes256, [prPrint, prFillAnnotations]);
Das Detail, das es sich mitzunehmen lohnt, befindet sich im Vorfeld der API. Dateien, die aus Word, Excel, PowerPoint und einer langen Liste von "Als PDF speichern"-Pipelines stammen, sind routinemäßig hybrid, sodass ein Loader, den Sie nur gegen die Ausgabe Ihres eigenen Generators testen, in der Testphase möglicherweise nie auf einen solchen trifft. Bestücken Sie Ihre Fixtures mit Dokumenten, die aus echten Office-Anwendungen exportiert wurden, nicht nur mit Dateien, die Ihr eigener Code erzeugt hat
Eine Datei überprüfen, bei der Sie einen Verdacht haben
Zwei Überprü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 Parsevorgang meldet, mit der höchsten Objektnummer, die /Size im Trailer deklariert. Eine große Lücke bedeutet, dass sich Objekte in Streams verbergen, die der Loader nicht geöffnet hat, was genau jenes Defizit ist, das sich später in einen Fehler beim Speichern verwandelt
Die Schreiber-Seite dieser Geschichte, wie Objekt-Streams und komprimierte Querverweise überhaupt erst produziert werden, wird in unserem Artikel über Objekt-Streams und inkrementelle Updates behandelt. Wenn die betreffende Hybrid-Datei zudem sehr groß ist, ermöglichen Ihnen die Ladetechniken im Direct File API-Walkthrough für große PDF-Workflows, diese zu untersuchen, ohne alles in den Speicher zu laden. Beides passt natürlich zu der hier beschriebenen Wiederherstellung, die als Teil der HotPDF Component für Delphi und C++Builder geliefert wird, zusammen mit den APIs für Laden, Bearbeiten, Verschlüsselung und Signierung, die an anderer Stelle in diesem Blog behandelt werden