Technischer Artikel

PDFs für Fast Web View mit PDFium in Delphi linearisieren

Die PDFium Component erzeugt Fast-Web-View-PDFs (linearisierte PDFs) aus Delphi über TPdf.SaveAsLinearized. Die Methode speichert das aktuelle Dokument mit PDFium und schreibt das Ergebnis dann nach ISO 32000-1 Annex F um: Die Objekte der ersten Seite wandern nach vorn, zwei Cross-Reference-Sections werden geschrieben, und Page-Offset- und Shared-Object-Hint-Tables sagen einem Viewer, welche Byte-Ranges er für jede Seite holen soll

Der Grund, warum sich jemand dafür interessiert, ist das Netzwerk. Ein 40-MB-Katalog aus Object Storage oder einem Dokument-Portal sollte Seite eins nach den ersten paar Kilobytes zeigen, und der Sprung zu Seite 380 sollte ein paar HTTP-Range-Requests kosten, keinen kompletten Download. Viewer können das nur, wenn die Datei linearisiert ist. Die meisten PDFs, die eine Editing-Pipeline verlassen, sind es nicht, denn die natürliche Art, ein PDF zu schreiben, setzt die Cross-Reference-Table ans Ende und verstreut Seiten-Objects dorthin, wo der Writer sie gerade ausgegeben hat

Warum kann PDFium linearisierte Dateien lesen, aber nicht schreiben?

PDFium hat einen kompletten Reader für Linearisierung und keinen Writer dafür. Die FPDFAvail_*-Availability-API erkennt das Linearization-Dictionary, dekodiert Hint-Tables und fragt den Host nach exakt den Byte-Ranges, die es braucht – darauf baut der Progressive-Download- und Cancellation-Workflow in PDFium Component. Die öffentliche Save-ABI bietet dagegen nur einen schlichten Kopier-Save und einen Save mit Ziel-PDF-Version. Nichts darin ordnet Objects um oder emittiert Hint-Streams

Die PDFium Component schließt diese Lücke, ohne nachzubauen, was PDFium schon gut kann. TPdf.SaveAsLinearized ruft zuerst das vorhandene SaveAs auf, der semantische Save bleibt also bei PDFium: Bearbeitete Seiten, Formularfeld-Zustände und Versions-Handling laufen alle durch den seit Jahren gefahrenen Code-Pfad. Die Komponente fährt dann einen Annex-F-Umbau über dieses stabile Zwischen-PDF und schreibt das linearisierte Ergebnis an Ihr Ziel. Die Arbeit so zu teilen heißt: Linearisierung muss nie eine Annotation oder ein Formular verstehen, nur Objects, Referenzen und Byte-Offsets

TPdf.SaveAsLinearized aus Delphi aufrufen

TPdf.SaveAsLinearized hat zwei Overloads, einen für einen TStream und einen für einen Dateinamen; beide nehmen dieselben optionalen Parameter Option: TSaveOption und PdfVersion: TPdfVersion wie SaveAs. Beide geben bei Erfolg True zurück. Der Datei-Overload schreibt in eine temporäre Datei im Zielverzeichnis und ersetzt das Ziel atomar, ein fehlgeschlagener Save hinterlässt also nie ein halb geschriebenes PDF dort, wo ein gutes lag

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'catalog-2026.pdf';
    Pdf.Active := True;
    // ... Seiten bearbeiten, Formulare füllen, stempeln, was die Pipeline eben tut ...
    if not Pdf.SaveAsLinearized('catalog-2026-web.pdf', saNoIncremental, pv17) then
      raise Exception.Create('Linearized save failed');
  finally
    Pdf.Free;
  end;
end;

Die Parameterregeln sind absichtlich streng. saIncremental wirft einen Fehler, denn ein inkrementelles Update hängt eine neue Revision ans Dateiende und würde das vor geladene Layout sofort zerstören. Eine Zielversion unter PDF 1.2 wird ebenfalls abgelehnt; pvUnknown behält die Quellversion. Verschlüsselte Eingabe braucht saRemoveSecurity, denn der Writer nummeriert Objects um, und PDFs Object-Level-Verschlüsselungs-Keys leiten sich aus der Objektnummer ab. Und weil Linearisierung ein kompletter Rewrite ist, wird jede vorhandene digitale Signatur ungültig. Müssen Sie signiert und linearisiert ausliefern, linearisieren Sie zuerst und signieren danach

Wie der Writer Objects für Fast Web View gruppiert

Linearisierung ist nicht nur, Seite eins an den Dateianfang zu verschieben. Der Writer parst die aktuelle Revision – ob ihre Cross-Reference-Daten eine klassische Table, eine Hybrid-Datei oder ein Cross-Reference-Stream sind – und expandiert jedes Mitglied eines aktiven Object Streams in ein eigenständiges Object. Dann baut er einen Referenzgraphen und sortiert die Objects in fünf Gruppen: dokumentweite Objects, die zum Öffnen der Datei nötig sind, Objects der ersten Seite, Objects, die einer späteren Seite privat sind, Objects, die mehrere spätere Seiten teilen, und alles andere

Die Falle in diesem Graphdurchlauf ist /Parent. Jedes Page-Dictionary zeigt im Seitenbaum nach oben, und eine naive Traversierung folgt diesem Zeiger zum Elternknoten und dann über /Kids in jede Geschwisterseite und ihre Content-Streams. Einen Schritt später enthält die Erstseiten-Gruppe das ganze Dokument, und die Datei ist technisch linearisiert, aber für Range-First-Zugriff nutzlos. Die PDFium Component ignoriert den Seiten-/Parent-Link und die Seitenbaum-Knoten außerhalb der eingesammelten Seite, genau das hält die Erstseiten-Section klein. Bei einer 51-Seiten-Regressionprobe mit tiefem Seitenbaum ist die Erstseiten-Section 5289 Bytes groß, 0,48% der 1105354-Byte-Ausgabe

Object Streams und Cross-Reference-Streams in der Eingabe werden zu gewöhnlichen indirekten Objects der Generation 0 flachgelegt, jede Referenz wird auf die neuen Nummern umgeschrieben, und die Ausgabe nutzt klassische Cross-Reference-Sections. Das kostet etwas Dateigröße gegenüber komprimierten Object Streams, vereinfacht aber die Hint-Table-Abdeckung und ist das Layout, das jeder linearisierungsbewusste Viewer handhabt. Zählt Kompaktheit bei einer bestimmten Ausgabe mehr als Fast Web View, hat die PDFium Component dafür einen separaten Object-Stream-Writer

Was kommt ins Linearization-Dictionary und die zwei xref-Sections?

Eine von der PDFium Component geschriebene linearisierte Datei trägt zwei klassische Cross-Reference-Sections und ein Linearization-Dictionary im allerersten Object. Die erste xref-Section folgt dem Linearization-Dictionary und deckt die dokumentweiten und Erstseiten-Objects ab, ein Viewer kann Seite eins also allein aus dem Dateianfang rendern. Die Haupt-xref-Section sitzt am Ende und deckt alles andere ab

Das Linearization-Dictionary selbst hält die Zahlen, die ein Viewer braucht, bevor er irgendetwas anderes liest: /L die Dateilänge, /H Offset und Länge des Hint-Streams, /O die Objektnummer der ersten Seite, /E das Ende der Erstseiten-Section, /N die Seitenzahl und /T den Offset des ersten Eintrags in der Haupt-xref-Table. All das hängt vom finalen Layout ab, und das Dictionary sitzt vor dem Layout. Es mit natürlicher Zahlenbreite zu schreiben erzeugt eine Schleife: Eine Länge, die von 999999 auf 1000000 wächst, addiert ein Byte, verschiebt jeden folgenden Offset, was die Breite einer anderen Zahl ändern kann, und so weiter

Die PDFium Component bricht die Schleife, indem sie für das Linearization-Dictionary feste 320 Bytes reserviert. Jedes Object, der Hint-Stream, beide xref-Sections und die Trailer werden zuerst geschrieben; sind alle Offsets final, wird das Dictionary an Ort und Stelle gefüllt und auf die reservierte Breite aufgepolstert. Ein subtiles Detail ist /T: Es muss auf den Object-0-Eintrag der Haupt-xref-Table zeigen, nicht auf das xref-Schlüsselwort davor. Strenge Validatoren bemängeln den Schlüsselwort-Offset, obwohl großzügige Viewer die Datei trotzdem öffnen

Warum ignorieren Hint-Table-Offsets den Hint-Stream?

Hint-Table-Offsets in einer linearisierten PDF werden interpretiert, als existierte der Hint-Stream in der Datei nicht. Annex F definiert es so, damit der Hint-Stream zwischen der Erstseiten-Section und dem Rest sitzen kann, ohne dass seine eigene Größe in die Zahlen zurückfedert, die er enthält. Die praktische Konsequenz für einen Writer ist leicht falsch zu bekommen: Jeder echte Offset, der hinter dem Hint-Stream liegt, muss reduziert werden, bevor er in die Page-Offset- oder Shared-Object-Hint-Table geht

Die Frage ist, um wie viel reduziert wird. Die verführerische Antwort ist die Länge der Stream-Daten, denn das ist der Teil, der sich nach Hint-Inhalt anfühlt. Die korrekte Antwort ist die Länge des gesamten Hint-Indirect-Objects, inklusive N 0 obj, dem Stream-Dictionary, den Schlüsselwörtern stream und endstream und endobj. Zieht man nur die Datenlänge ab, ist jeder spätere Offset um einige Dutzend Bytes daneben. Seite eins rendert trotzdem, sie lebt vor dem Hint-Stream; der Bug überlebt also eine schnelle Sichtprüfung und taucht erst auf, wenn ein Viewer zu einer späteren Seite springt und eine Range anfordert, die mitten im falschen Object beginnt

Beide Hint-Tables sind bit-gepackt mit dem höchstwertigen Bit zuerst, ebenfalls nach Annex F. Die Komponente verifiziert das Ergebnis so, wie ein echter Client es täte: Ein Sparse-Range-Store liefert nur die ersten 1 KiB der 51-Seiten-Probe, und PDFiums native Hint-Behandlung fordert den Rest an. Die letzte Seite öffnet, ohne dass die vollständige Datei je vorhanden war

Eine linearisierte PDF in Delphi verifizieren

Die billigste Prüfung ist, PDFium selbst zu fragen. TPdf.InspectAvailability fährt die Availability-API gegen einen Stream, ohne das Dokument zu laden, und sein Feld Linearization meldet plnLinearized, plnNotLinearized oder plnUnknown

function IsFastWebView(const FileName: string): Boolean;
var
  Stream: TFileStream;
  Info: TPdfAvailability;
begin
  Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Info := TPdf.InspectAvailability(Stream);
    Result := Info.Linearization = plnLinearized;
  finally
    Stream.Free;
  end;
end;

Das bestätigt, dass PDFium das Layout erkennt; es beweist nicht, dass die Hints korrekt sind. Für einen echten End-to-End-Test serve Sie die Datei über einen range-begrenzten Transport und öffnen eine späte Seite mit TPdfProgressiveDocument, genauso wie der Guide zum Streaming großer PDFs on demand mit PDFium VCL. Rendert die Seite, nachdem nur ein kleiner Bruchteil der Datei geholt wurde, machen die Hint-Tables ihren Job. Einen zweiten unabhängigen Linearisierungs-Checker in die Release-Pipeline zu hängen lohnt ebenfalls, denn Viewer unterscheiden sich darin, wie gnädig sie bei einem verrutschten /T oder einem leicht falschen Offset sind

Zwei Grenzen im Kopf behalten. Der Object-Materialisierer lehnt Arrays ab 2 GiB weiterhin ab, extrem große Dokumente brauchen vorerst also eine andere Strategie. Und Linearisierung zahlt sich nur aus, wenn der Server HTTP-Range-Requests ehrt; hinter einem Proxy, der Accept-Ranges entfernt, lädt eine linearisierte Datei genauso herunter wie jede andere

FAQ

Hält TPdf.SaveAsLinearized digitale Signaturen gültig?

Nein. TPdf.SaveAsLinearized nummeriert jedes Object um und schreibt es neu, vorhandene Signaturen werden also ungültig. Linearisieren Sie zuerst, dann signieren

Kann PDFium Component eine verschlüsselte PDF linearisieren?

Nur wenn Sie saRemoveSecurity übergeben. Object-Level-Verschlüsselungs-Keys hängen an Objektnummern, die der Writer ändert; die Ausgabe wird also ohne Verschlüsselung geschrieben

Welche PDF-Versionen unterstützt die linearisierte Ausgabe?

TPdf.SaveAsLinearized akzeptiert pvUnknown, um die Quellversion zu behalten, oder jedes Ziel ab PDF 1.2 aufwärts, und lehnt saIncremental ab, denn eine angehängte Revision würde das Layout brechen

Fast-Web-View-Ausgabe, progressives Laden und die Availability-API sind alles Teil der PDFium Component for Delphi and C++Builder, die PDFium für Delphi-, C++Builder- und Lazarus-Projekte verpackt