Technischer Artikel

PDF-Seiten in Delphi mit PDFium Component als JPEG-Bilder rendern

Das Rendern einer PDF-Seite als JPEG besteht aus zwei Vorgängen, die oft zusammen ausgeführt und dann getrennt debuggt werden. Zuerst rastern Sie die Seite in ein Pixel-Bitmap mit einer von Ihnen gewählten Auflösung. Dann übergeben Sie dieses Bitmap an einen JPEG-Encoder und wählen eine Qualität. PDFium Component übernimmt die erste Hälfte durch RenderPage; die zweite Hälfte ist reines VCL, TJPEGImage aus Vcl.Imaging.jpeg. An der Nahtstelle dazwischen liegen die interessanten Entscheidungen, denn die Auflösung, die Sie auf der Rendering-Seite wählen, und die Qualität, die Sie auf der Codierungs-Seite wählen, wägen sich gegeneinander und gegen die Dateigröße ab, und zwar auf eine Weise, die man leicht falsch machen kann

Was man vor dem Schreiben von Code verinnerlichen sollte: Eine PDF-Seite hat keine Pixel. Sie wird in Punkten beschrieben, wobei ein Punkt 1/72 Zoll entspricht, und die Seite ist eine in diesen Punkten gemessene Vektorzeichnung. Wenn Sie PDFium auffordern zu rendern, wählen Sie aus, auf wie viele Pixel diese Zeichnung projiziert werden soll, und diese Wahl ist die DPI (Punkte pro Zoll). Wenn Sie sich hier verrechnen, rendern Sie entweder ein verschwommenes Miniaturbild, wenn Sie eigentlich eine Druckvorlage wollten, oder Sie weisen ein 200-Megapixel-Bitmap für etwas zu, das als 120-Pixel-Vorschau gedacht ist

Von DPI zu Pixelabmessungen

RenderPage benötigt eine ganzzahlige Pixel-Width und -Height, keine DPI. Die erste Aufgabe ist also die Konvertierung. Eine Seite meldet ihre Größe in Punkten über PageWidth und PageHeight (beides Double), und die Umrechnung ist dieselbe, die jeder Rasterisierer verwendet: Pixel entsprechen Punkten mal Ziel-DPI geteilt durch 72. Eine US-Letter-Seite ist 612 mal 792 Punkte groß. Bei 150 DPI wird dies zu 1275 mal 1650 Pixeln; bei 72 DPI bleibt es bei 612 mal 792, also einem Pixel pro Punkt, was der Fall ist, von dem man leicht vergisst, dass er einfach die Identität darstellt

// Pdf.PageNumber must already point at the page you want.
PixelW := Round(Pdf.PageWidth  * Dpi / 72);
PixelH := Round(Pdf.PageHeight * Dpi / 72);
Bitmap := Pdf.RenderPage(0, 0, PixelW, PixelH, ro0, [], clWhite);
// ... use Bitmap ...
Bitmap.Free;   // the function-form RenderPage hands you ownership

Zwei Details in diesen vier Zeilen entscheiden darüber, ob der Code korrekt ist. Das erste ist, dass die Funktionsform von RenderPage ein TBitmap zurückgibt, das Ihnen gehört. PDFium hat es zugewiesen und ist dann verschwunden; wenn Sie es nicht bei jeder Iteration freigeben (Free), verliert ein Batch über einige hundert Seiten einige hundert Bitmaps, und der Prozess bläht sich auf, bis etwas abstürzt. Das zweite ist das Color-Argument, hier clWhite. PDF-Seiten werden normalerweise unter der Annahme eines undurchsichtigen weißen Untergrunds gezeichnet. Wird eine Seite mit Transparenz auf einer falschen Hintergrundfarbe gerendert, entstehen matschige Ränder oder unerwünschte dunkle Höfe. Weiß ist der richtige Standard für fast jedes Dokument; der Parameter existiert für den seltenen Fall, in dem das nicht so ist

Die 0, 0 sind die Left- und Top-Offsets in der Seite, im skalierten Koordinatenraum, und Sie belassen sie bei Null, es sei denn, Sie schneiden etwas zu. Das ro0 steht für die Drehung: Belassen Sie es bei Null, und PDFium berücksichtigt die Drehung, die die Seite bereits in ihrem /Rotate-Eintrag deklariert. Eine im Querformat erstellte Seite wird also auch im Querformat ausgegeben, ohne dass Sie etwas dafür tun müssen

Codieren des Bitmaps als JPEG

Sobald das Bitmap existiert, ist JPEG der einfache Teil und reines Delphi. TJPEGImage.Assign kopiert das Bitmap hinein, CompressionQuality stellt die Qualität auf einer Skala von 1 bis 100 ein und SaveToFile schreibt die Datei. Die einzige Regel für die Reihenfolge lautet, dass die Qualität vor dem Speichern festgelegt werden muss, da sie die Codierung steuert, die durch SaveToFile ausgelöst wird

uses
  Vcl.Graphics, Vcl.Imaging.jpeg, PDFium;

procedure SavePageAsJpeg(Pdf: TPdf; PageNumber, Dpi, Quality: Integer;
  const FileName: string);
var
  Bitmap: TBitmap;
  Jpeg: TJPEGImage;
begin
  Pdf.PageNumber := PageNumber;
  Bitmap := Pdf.RenderPage(0, 0,
    Round(Pdf.PageWidth  * Dpi / 72),
    Round(Pdf.PageHeight * Dpi / 72),
    ro0, [], clWhite);
  try
    Jpeg := TJPEGImage.Create;
    try
      Jpeg.Assign(Bitmap);
      Jpeg.CompressionQuality := Quality;   // 1..100
      Jpeg.SaveToFile(FileName);
    finally
      Jpeg.Free;
    end;
  finally
    Bitmap.Free;
  end;
end;

Dieses verschachtelte try/finally mag für eine Ein-Seiten-Hilfsfunktion umständlich aussehen, ist aber für einen Batch genau richtig. Der innere Block gibt den Encoder frei, der äußere Block das Bitmap, und wenn bei einem davon eine Ausnahme auftritt, wird trotzdem freigegeben, was ihm gehört. Wenn Sie diese Blöcke zu einem einzigen zusammenfassen, kann eine Ausnahme während der Codierung das Bitmap stranden lassen. Bei einem langen Durchlauf ist das der Unterschied zwischen einem Konverter, der bis zum Ende durchläuft, und einem, der bei Seite 300 mit einer beschädigten Datei und einem „Out-of-Memory“-Dialog abstürzt

DPI und Qualität gemeinsam auswählen

Die beiden Regler sind nicht unabhängig vom Zweck der Ausgabe, und der häufigste Fehler besteht darin, aus reiner Vorsicht beide nach oben zu drehen. Ein Web-Thumbnail, das mit 300 DPI gerendert und mit einer Qualität von 95 gespeichert wird, ist mehrere hundert Kilobyte groß und gibt vor, ein 120-Pixel-Bild zu sein; der Browser verwirft beim Herunterskalieren fast alles davon. Passen Sie die Auflösung an die Pixel an, die für die Ausgabe tatsächlich benötigt werden, und wählen Sie dann eine Qualität, die die verlustbehaftete JPEG-Kompression ohne sichtbare Artefakte übersteht

AusgabeDPIJPEG-Qualität
Listen-Thumbnail7260-70
Vorschau auf dem Bildschirm96-15080-85
Detailansicht200-30085-95
Druckvorlage300-60090-100

Die JPEG-Qualität verdient ein eigenes Wort der Vorsicht. Es ist kein linearer Regler. Der Sprung von 70 auf 85 bringt eine echte visuelle Verbesserung bei nur moderatem Dateiwachstum; der Sprung von 95 auf 100 verdoppelt die Datei ungefähr für einen Unterschied, den fast niemand sehen kann, denn Qualität 100 ist immer noch nicht verlustfrei, es wird nur nicht mehr viel verworfen. Bei textlastigen Seiten verschmiert die blockbasierte JPEG-Kompression die scharfen Kanten von Schriftzeichen zu einem schwachen "Ringing". Das ist der Grund, warum eine Qualität unter etwa 80 bei einer eigentlich gestochen scharfen Ausgabe zu einem wie gescannt aussehenden Text führt. Wenn die Seiten hauptsächlich aus Text bestehen und Sie das Format wechseln können, rendert PNG diesen Text ohne dieses "Ringing". JPEG hat seinen berechtigten Platz bei fotografischen und gemischten Inhalten, bei denen seine Kompression tatsächlich zu kleineren Dateien führt

Schnellere, kleinere Thumbnails

Wenn das Ziel eher ein Thumbnail als eine originalgetreue Reproduktion ist, können Sie den Renderer anweisen, weniger Arbeit zu leisten. Der Options-Parameter akzeptiert eine Menge von TRenderOption-Flags, und einige davon tauschen Genauigkeit gegen Geschwindigkeit ein – genau so, wie es für eine kleine Vorschau gewünscht ist. reGrayscale verwirft die Farbe, was sowohl ein schnelleres Rendern ermöglicht als auch ein kleineres Bitmap zum Codieren erzeugt. reNoSmoothImage und reNoSmoothPath überspringen das Anti-Aliasing, das in der Skalierung eines Thumbnails ohnehin unsichtbar ist

function RenderThumbnail(Pdf: TPdf; PageNumber, MaxW, MaxH: Integer): TBitmap;
var
  Scale: Double;
begin
  Pdf.PageNumber := PageNumber;
  // Fit the page inside MaxW x MaxH while preserving aspect ratio.
  Scale := Min(MaxW / Pdf.PageWidth, MaxH / Pdf.PageHeight);
  Result := Pdf.RenderPage(0, 0,
    Round(Pdf.PageWidth  * Scale),
    Round(Pdf.PageHeight * Scale),
    ro0, [reGrayscale, reNoSmoothImage], clWhite);
end;

Der Fall des Thumbnails zeigt auch den saubereren Ansatz, um über die Größenanpassung nachzudenken. Anstatt über die DPI zu gehen, berechnen Sie einen einzelnen Skalierungsfaktor, der die Seite in einen Begrenzungsrahmen einpasst und das Seitenverhältnis beibehält. Genau das bewirkt das Min der beiden Verhältnisse. Sowohl eine Seite im Hochformat als auch eine im Querformat landen ohne Verzerrung im selben Rahmen, und Sie müssen nie darüber nachdenken, welche DPI der Vorgabe "einpassen in 200 mal 280" entspricht. Ein Vorbehalt bei reGrayscale: Es wandelt Rasterbildinhalte in Grau um, aber Vektorfüllungen und Text behalten ihre Farbwerte in der Engine. Eine Seite, die größtenteils aus Vektorgrafiken besteht, kann also weniger monochrom zurückkommen, als der Name des Flags vermuten lässt. Für ein echtes Voll-Graustufen-Ergebnis ist die Konvertierung des gerenderten Bitmaps mit GrayscalePdfBitmap der zuverlässigere Weg

Stapelverarbeitung eines ganzen Dokuments

Um das Ganze für ein vollständiges Dokument zusammenzusetzen, wird eine Schleife über PageCount ausgeführt, wobei PageNumber Seite für Seite weitergeschaltet wird. Seiten sind 1-basiert: Seite eins ist PageNumber := 1, und die Schleife läuft bis PageCount einschließlich, nicht PageCount - 1. Eine weitere Sache, die der Batch beachten muss, ist die vertragliche Regelung für stilles Laden. Das Setzen von Active := True löst bei einer beschädigten Datei oder einem falschen Passwort niemals eine Ausnahme aus; es belässt Active lediglich auf False. Überprüfen Sie dies, bevor Sie eine einzige Seite rendern, sonst arbeitet der erste Aufruf von RenderPage gegen ein Dokument, das nie geöffnet wurde

procedure ExportAllPages(const PdfPath, OutDir: string; Dpi, Quality: Integer);
var
  Pdf: TPdf;
  I, Digits: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := PdfPath;
    Pdf.Active := True;
    if not Pdf.Active then
      raise Exception.Create('Could not open ' + PdfPath);

    Digits := Length(IntToStr(Pdf.PageCount));   // zero-pad so files sort right
    for I := 1 to Pdf.PageCount do
      SavePageAsJpeg(Pdf, I, Dpi, Quality,
        Format('%s\page_%.*d.jpg', [OutDir, Digits, I]));
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Das Auffüllen mit Nullen (Zero-Padding) durch Digits ist eine Kleinigkeit, die einem später einen ganzen Nachmittag rettet. Wenn Sie die Dateien von page_1.jpg bis page_10.jpg benennen, setzt jedes Tool, das sie als Zeichenfolgen sortiert, page_10 direkt hinter page_1 und bringt die Reihenfolge durcheinander. Ein Padding auf die Breite der höchsten Seitenzahl, sodass ein 300-seitiges Dokument page_001.jpg ergibt, sorgt dafür, dass die lexikalische Reihenfolge und die Seitenreihenfolge in allen nachfolgenden Schritten identisch bleiben

Bei Dokumenten, die so groß sind, dass die Konvertierung spürbar Zeit in Anspruch nimmt, sollten Sie den Vorgang außerhalb des UI-Threads ausführen oder Nachrichten zwischen den Seiten weiterleiten, damit die Anwendung reaktionsfähig bleibt. Bieten Sie dem Benutzer außerdem eine Möglichkeit zum Abbrechen. Wenn Sie sehr große Seiten rendern und einen Abbruch wünschen, der mitten in einer Seite und nicht erst zwischen den Seiten greift, bietet PDFium Component einen progressiven Rendering-Pfad mit einem Cancellation Token. Dies ist zwar ein schwergewichtigerer Mechanismus, als ihn die meisten Batch-Exporte benötigen, aber er steht zur Verfügung, wenn das Rendern einer einzelnen Seite bei 600 DPI bereits langsam genug ist, um das Programm zu blockieren

Eine letzte Kombination, die es wert ist, gekannt zu werden. Das Rastern einer Seite verwirft deren Textebene: Das JPEG besteht aus Pixeln, und die darin enthaltenen Wörter sind nicht mehr auswählbar oder durchsuchbar. Wenn Sie sowohl ein Bild als auch den zugrunde liegenden Text benötigen, rendern Sie das Bild und extrahieren Sie den Text separat, was im begleitenden Artikel über das Extrahieren von Text aus PDF-Dokumenten mit PDFium Component behandelt wird. Die hier gezeigten RenderPage-Überladungen und Rendering-Optionen sind Teil der PDFium Component für Delphi und C++Builder