Technischer Artikel

PDF-Seiten in Delphi mit HotPDF in Bitmap rendern

HotPDF rendert eine geladene PDF-Seite über einen einzigen Aufruf in ein Delphi-TBitmap: RenderLoadedPageToBitmap(PageIndex, DPI). Die Funktion interpretiert den Inhaltsstrom (Content Stream) der Seite und gibt ein im Besitz des Aufrufers befindliches 24-Bit-RGB-Bitmap mit der von Ihnen gewählten Auflösung zurück. Das ist genau das, was eine Miniaturansichten-Leiste (Thumbnail-Strip), eine Druckvorschau oder eine Pipeline für den PDF-zu-Bild-Export benötigt. Dieser Artikel führt durch die API und behandelt anschließend den Teil, der einen praxistauglichen Renderer von einem bloßen Spielzeug unterscheidet: das Zeichnen von Text direkt aus den eingebetteten Schriftprogrammen anstelle von ähnlich aussehenden Systemschriftarten

Warum ist das Rendern einer PDF-Seite schwieriger als das Zeichnen eines Bildes?

Eine PDF-Seite ist kein fertiges Bild. Sie ist ein Programm: ein Strom von Operatoren, die Pfade erstellen, Schriftarten auswählen, Farben festlegen und Glyphen platzieren. Dieses Programm wird gegen das in ISO 32000-1 §8 definierte Grafikmodell ausgeführt. Nichts in der Datei beschreibt, wie ein Pixel aussieht. Um ein Bitmap zu erzeugen, müssen Sie dieses Programm ausführen — eine aktuelle Transformationsmatrix, einen Grafikzustands-Stack (Graphics State Stack) für q/Q, einen Beschneidungspfad (Clipping-Pfad) sowie Füll- und Strichfarbräume verwalten — und das Ergebnis rasterisieren. Deshalb ist „Seite 3 einfach als Bild anzeigen“ die Arbeit eines Inhaltsstrom-Interpreters und keine einfache Dateiformat-Konvertierung

Der Renderer von HotPDF, eingeführt in v2.253.0, besteht aus sechs entkoppelten Einheiten, die dieses Modell widerspiegeln: einem Kern für affine Matrizen für die PDF-Transformationsalgebra [a b c d e f], einem Grafikzustands-Stack, einem Farbraum-Auflöser (DeviceRGB, DeviceGray, DeviceCMYK, Indexed), einem Pfad-Builder, der PDF-Pfadoperatoren an GDI anbindet, einer Schriftmetrik-Schicht, die /Widths-Arrays für korrekte Schrittweiten ausliest, und dem Interpreter, der die Operatoren abarbeitet und die anderen fünf Einheiten steuert. Bild-XObjects durchlaufen denselben Dekodierungs-Stack, den die Bibliothek zur Extraktion nutzt, sodass jeder Bildfilter, den HotPDF zur Extraktion dekodieren kann — einschließlich JPXDecode-komprimierter JPEG-2000-Bilder — auch in der gerenderten Ausgabe dargestellt wird

Rendern einer geladenen Seite in ein TBitmap

RenderLoadedPageToBitmap erwartet einen nullbasierten Seitenindex und einen DPI-Wert, wobei 72 DPI einer PDF-Benutzerraumeinheit auf ein Pixel abbilden. Im Fehlerfall (Index außerhalb des gültigen Bereichs, fehlende Ressourcen) gibt die Funktion nil zurück, anstatt eine Ausnahme auszulösen. So kann ein Viewer eine fehlerhafte Seite überspringen und die Anzeige fortsetzen. Der Aufrufer besitzt das zurückgegebene Bitmap und muss es wieder freigeben

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('report.pdf') > 0 then
    begin
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);  // Seite 1 bei 144 DPI
      if Bmp <> nil then
      try
        Image1.Picture.Assign(Bmp);
      finally
        Bmp.Free;  // Aufrufer besitzt das Bitmap
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Das DPI-Argument übernimmt die Skalierungsarbeit für alle gängigen Szenarien. Eine Miniaturansichten-Leiste rendert bei 36 oder 48 DPI und erhält kleine, schnelle Bitmaps. Eine Bildschirmvorschau bei 96 oder 144 DPI entspricht der typischen Display-Dichte. Eine Export-Pipeline bei 300 DPI liefert Bilder in Druckqualität. Die Seitenrotation aus dem Eintrag /Rotate und der Origin-Flip des /MediaBox (PDF legt den Ursprung unten links fest, GDI oben links) werden innerhalb der Seite-zu-Gerät-Matrix verarbeitet, sodass eine US-Letter-Seite bei 72 DPI exakt als 612×792 Pixel in der richtigen Ausrichtung zurückgegeben wird

Warum zeigen gerenderte PDF-Thumbnails falsche Glyphen?

Falsche oder ungenaue Glyphen in der gerenderten PDF-Ausgabe bedeuten fast immer, dass der Renderer eine Systemschriftart als Ersatz verwendet, anstatt die in der Datei eingebettete Schriftart zu nutzen. Der erste Renderer von HotPDF verhielt sich genau so: Er entfernte das Subset-Präfix von der /BaseFont (wodurch ABCDEF+Arial zu Arial wurde), fragte GDI nach einer Systemschriftart mit diesem Namen und zeichnete den Text damit. Bei einem Dokument, das Arial oder Times New Roman mit Standard-Codierung verwendet, sieht das Ergebnis dem Original sehr ähnlich. Aber es ist eine Näherung, die unter bestimmten Umständen fehlschlägt

Schriftarten mit eingebetteten Subsets sind dabei der ungünstigste Fall. Eine Subset-Schriftart enthält oft nur die vierzig Glyphen, die ein Dokument tatsächlich nutzt, wobei die Zeichencodes in einer datei-internen Reihenfolge zugewiesen sind — Code 1 könnte „T“ sein, Code 2 „h“ usw. Eine Systemschriftart weiß nichts über diese interne Zuweisung, sodass der Text entweder verschwindet oder als völlig falsche Zeichen dargestellt wird. Benutzerdefinierte Encodings, Symbolschriftarten, Barcodeschriftarten und alle Schriftarten, die auf dem Rendering-System nicht installiert sind, scheitern auf dieselbe Weise. Ein Renderer, der sich mit der Ersetzung durch Systemschriftarten begnügt, erzeugt Miniaturansichten, die zwar grob wie die Seite aussehen — bis die Seite die Schriftarten verwendet, die eine Einbettung überhaupt erst notwendig gemacht haben

Einbetten des Glyphen-Renderings: Zeichnen aus dem Schriftprogramm selbst

HotPDF hat diese Lücke über fünf Releases hinweg (v2.268.0 bis v2.272.0) geschlossen, indem die eingebetteten Schriftart-Programme geparkt und ihre Glyphenumrisse als gefüllte GDI-Vektorpfade nachgezeichnet werden. Text auf einer gerenderten Seite stammt nun aus denselben Umrissdaten, die auch ein standardkonformer PDF-Reader verwendet, was bedeutet, dass Subset-Schriftarten, benutzerdefinierte Encodings und nicht installierte Schriften exakt in ihren Formen gerendert werden. Die Abdeckung wurde nach Schrifttyp aufgebaut:

Bei Type0/CIDFontType2-Schriftarten mit einem eingebetteten TrueType-Programm (FontFile2) parst der Renderer die Tabellen glyf und loca direkt: Quadratische Konturen werden in kubische Bézier-Kurven konvertiert, die GDI versteht. Implizite On-Curve-Punkte zwischen aufeinanderfolgenden Off-Curve-Punkten werden rekonstruiert, und zusammengesetzte Glyphen werden rekursiv aufgelöst. Es werden sowohl Identity- als auch explizite Stream-CIDToGIDMap-Layouts unterstützt, und CID-Schrittweiten berücksichtigen die Breitenangaben /W und /DW, sodass zweibytiger Identity-H-Text korrekt positioniert wird

CFF-Programme (FontFile3, ob CIDFontType0C, Type1C oder in einem OpenType-Wrapper) erhalten einen vollständigen Interpreter für Type-2-Charstrings: Linien, Kurven, Flex-Kurven, Hint-Masken und lokale/globale Unterprogram Aufrufe mit dem korrekten Bias. CID-keyed CFF-Programme ordnen Zeichencodes über den Zeichensatz (Charset) des Fonts zu, was bei Subset-Schriften wichtig ist, deren Glyphenreihenfolge von der CID-Reihenfolge abweicht. Die Glyphen-spezifische Font-DICT-Auswahl über FDArray/FDSelect wird dabei beachtet. Einfache (Nicht-CID) TrueType-Schriftarten lösen Ein-Byte-Codes über die eigene cmap-Tabelle des eingebetteten Fonts mit einer robusten Untertabellenkette auf — zuerst Unicode-Formate 4 und 12, dann Symbol-Untertabellen mit dem F000-Spiegelbereich für die private Nutzung, dann ältere Macintosh-Formate — während einfache Type1-Schriftarten über die im CFF-Programm integrierte Codierung aufgelöst werden

Zwei Verfeinerungen vervollständigen das System. Erstens werden die /Encoding-Verzeichnisse einfacher Schriftarten gemäß der Priorität von ISO 32000-1 §9.6.6 aufgelöst: /Differences-Arrays überschreiben die Basis-Codierung, welche wiederum das Mapping des Schriftprogramms selbst überschreibt — der Pfad, auf den TeX- und PostScript-basierte Toolchains angewiesen sind, wobei Glyphennamen über die Adobe Glyph List, das CFF-Charset oder die TrueType-cmap aufgelöst werden. Zweitens werden Type3-Schriftarten, deren Glyphen selbst kleine Inhaltsströme sind, mit der kombinierten Schriftmatrix, Schriftgröße und Textmatrix über den Renderer wiedergegeben. Glyphen-Raum-/Widths werden über die /FontMatrix interpretiert, wie es ISO 32000-1 §9.6.5 vorschreibt. Glyphenprozeduren, die eine Begrenzungsbox (Bounding Box) d1 deklarieren, werden darauf beschnitten, sodass eine fehlerhafte Barcode-Glyphe nicht über ihre Zelle hinauszeichnen kann. Kann ein Code nicht zugeordnet werden — bei einem beschädigten Programm oder einem nicht zugewiesenen Zeichen — weicht der Renderer für diese Glyphe auf das Zeichnen mit Systemschriftarten aus, anstatt den Textlauf abzubrechen

Wie macht man wiederholtes Rendern schnell?

Die Antwort, die HotPDF liefert, ist ein Cache für die zuletzt verwendeten Seiten (Most-Recently-Used Page Cache): RenderLoadedPageToBitmapCached hält bis zu RenderCacheCapacity gerenderte Seiten (Standardwert 8), indiziert nach Seitenindex und DPI. Ein Cache-Treffer liefert eine frische, dem Aufrufer gehörende Kopie zurück, ohne den Inhaltsstrom erneut interpretieren zu müssen — typischerweise tausendfach schneller als das erneute Interpretieren der Seite. Dieses Muster passt perfekt für Viewer: Ein Benutzer, der zwischen zwei Seiten hin- und herblättert, oder ein Skalierungsereignis, das dieselbe Seite bei identischer DPI erneut anfordert, trifft jedes Mal den Cache

// Thumbnail-Leiste: Der erste Durchlauf rendert, das Zurückscrollen trifft den Cache
for I := 0 to ThumbCount - 1 do
begin
  Bmp := Pdf.RenderLoadedPageToBitmapCached(I, 48);
  if Bmp <> nil then
  try
    ThumbList.AddThumbnail(I, Bmp);
  finally
    Bmp.Free;
  end;
end;

// Nach der In-Place-Bearbeitung einer geladenen Seite:
Pdf.InvalidateRenderedPageCache;  // Das nächste Rendern spiegelt die Änderung wider

Bedenken Sie den Speicherverbrauch, bevor Sie die Kapazität erhöhen. Eine US-Letter-Seite bei 300 DPI ist 2550×3300 Pixel groß, was als 24-Bit-Bitmap etwa 25 MB entspricht. Acht gecachte Seiten bei Exportauflösung belegen somit rund 200 MB. Bei einer Thumbnail-DPI kosten dieselben acht Einträge weit unter einem Megabyte. Dimensionieren Sie die RenderCacheCapacity passend für die DPI-Werte, die Sie tatsächlich cachen, und rufen Sie nach jeder In-Place-Bearbeitung InvalidateRenderedPageCache auf — der Cache ist nur nach Seite und DPI indiziert und erkennt nicht, ob sich der darunterliegende Inhalt geändert hat. Das Laden eines neuen Dokuments leert den Cache automatisch

Ein zweiter Cache arbeitet unterhalb des Seiten-Caches: Dekodierte Bild-XObjects werden in einem speicherbegrenzten Bereich gehalten, der durch ImageCacheMaxBytes (Standardwert 32 MB) begrenzt ist, wobei der am längsten nicht genutzte Eintrag zuerst verworfen wird (LRU-Verfahren). Ein Firmenlogo oder ein Briefkopf-Bild, das sich auf jeder Seite wiederholt, wird so nur einmal pro Dokumentenladung dekodiert, anstatt bei jedem Do-Operator. Dies halbiert die Renderzeit für Seiten mit gemeinsam genutzten Bildern und beschleunigt den Export von mehrseitigen TIFFs im gleichen Maße. InvalidateRenderedPageCache leert auch diesen Cache

Was immer noch nur näherungsweise gerendert wird

Der Renderer zielt auf die gängige Dokumenten-PDF-Untermenge ab, und es ist wichtig zu wissen, wo die Grenzen liegen. CalRGB-, Lab- und ICC-basierte Farbräume werden angenähert und nicht vollständig farbverwaltet — Standard-Gerätefarbräume, indizierte Paletten und abgetastete Type-0-Farbsuchen (Color LUT) werden verarbeitet, aber eine Druckproduktionsdatei, die sich auf ICC-Rendering-Intents verlässt, wird farblich nicht exakt dargestellt. Shading-Muster (sh) und Füllmethoden jenseits von einfachem Alpha liegen ebenfalls außerhalb des Funktionsumfangs, und die Rekursion von Form-XObjects ist als Endlosschleifenschutz in der Tiefe begrenzt. Für Rechnungen, Berichte, Verträge und Formulare — Seiten, die aus Text, Pfaden und Bildern bestehen — ist die Ausgabe originalgetreu. Für einen Designentwurf voller Verläufe und Transparenzgruppen sollten Sie das Bitmap jedoch als Vorschau und nicht als Proof betrachten

Die praktische Bedeutung: Wenn Ihre Pipeline Dokumente mit HotPDF generiert oder typische Geschäfts-PDFs verarbeitet, gibt RenderLoadedPageToBitmap diese mit den exakten eingebetteten Glyphenformen, korrekten CID-Schrittweiten und der korrekten Seitengeometrie wieder. Die Näherungen liegen in den Randbereichen des Grafikmodells, die in Geschäftsdokumenten selten vorkommen

RenderLoadedPageToBitmap, seine gecachte Variante und die hier beschriebene Rendering-Pipeline für eingebettete Glyphen werden als Teil von HotPDF Component für Delphi und C++Builder ausgeliefert — einer nativen VCL-Bibliothek ohne externe DLL-Abhängigkeiten, die PDF-Erstellung, -Bearbeitung, -Textextraktion und -Seitenrendering in einem Paket abdeckt