Technischer Artikel

Bilder aus PDFs extrahieren mit der PDFium-Komponente in Delphi

PDF speichert Bilder als First-Class-Objekte innerhalb seiner Inhaltsströme. Wenn eine Seite auf ein Foto, einen Scan oder ein Diagramm verweist, leben die Pixeldaten in einem XObject-Wörterbuch neben der Seitengeometrie. Die PDFium-Komponente macht dies durch zwei Eigenschaften von TPdf zugänglich: BitmapCount, was zurückgibt, wie viele eingebettete Bitmaps sich auf der aktuellen Seite befinden, und Bitmap[Index], was eines davon in ein TBitmap dekodiert, das Ihnen gehört und das Sie freigeben müssen. Das ist das gesamte Extraktionsmodell. Die Schleife besteht aus vier Zeilen; was Urteilsvermögen erfordert, ist die umgebende Infrastruktur

Öffnen des Dokuments

Das Erste, was man über TPdf wissen muss, ist, dass Active := True niemals eine Ausnahme auslöst. Ladefehler, falsche Passwörter, beschädigte Dateien: all das wird intern abgefangen und die Komponente bleibt einfach inaktiv. Sie müssen das Flag nach der Zuweisung selbst überprüfen, sonst gehen Sie in die Seitenschleife, während PageCount null zurückgibt, und wundern sich, warum nichts extrahiert wurde

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Active := True;
    if not Pdf.Active then
    begin
      Writeln('Failed to open: ', Pdf.FileName);
      Exit;
    end;
    Writeln(Pdf.PageCount, ' pages');
    // proceed to extraction
  finally
    Pdf.Free;
  end;
end;

Passwortgeschützte Dateien folgen demselben Muster: Weisen Sie Pdf.Password zu, bevor Sie Active := True setzen. Wenn das Passwort falsch ist, bleibt Active False und Sie erhalten keine Ausnahme, die Sie abfangen könnten. In einem Stapelverarbeitungstool, das Hunderte von Dateien verarbeitet, ist dieses stille Verhalten tatsächlich nützlich: Sie sammeln die Fehlschläge in einer Liste, anstatt den Aufrufstapel für jeden einzelnen abzuwickeln

Seiten iterieren und Bitmaps abrufen

BitmapCount ist pro Seite, also setzen Sie Pdf.PageNumber, bevor Sie es lesen. Seitenzahlen sind 1-basiert; der Standardwert ist 0, was bedeutet, dass keine Seite geladen ist. Die Eigenschaft Bitmap[Index] ist 0-basiert und gibt ein dem Aufrufer gehörendes TBitmap zurück. Sie müssen es freigeben. Vernachlässigen Sie die Freigabe innerhalb einer langen Schleife über ein großes Dokument und der Speicher steigt schnell an, da jede Bitmap vor jeder Komprimierung mehrere Megabyte an rohen Pixeldaten umfassen kann

procedure ExtractAllImages(Pdf: TPdf; const OutputDir: string);
var
  Page, Idx: Integer;
  Bmp: TBitmap;
  OutPath: string;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := Page;
    for Idx := 0 to Pdf.BitmapCount - 1 do
    begin
      Bmp := Pdf.Bitmap[Idx];
      if not Assigned(Bmp) then
        Continue;
      try
        OutPath := Format('%s\p%d_img%d.bmp', [OutputDir, Page, Idx + 1]);
        Bmp.SaveToFile(OutPath);
      finally
        Bmp.Free;
      end;
    end;
  end;
end;

Die Assigned-Prüfung ist wichtig. Eine kleine Anzahl von PDF-Generatoren schreibt Bild-XObjects mit Null-Pixel-Dimensionen oder anderweitig fehlerhaften Daten; in diesen Fällen gibt die Komponente nil anstelle einer leeren Bitmap zurück. Einen nil-Rückgabewert als Fehler zu behandeln und die Extraktion zu stoppen, ist der falsche Reflex: Überspringen Sie es, protokollieren Sie die Seite und den Index, falls Sie den Prüfpfad benötigen, und fahren Sie fort. Der Rest der Seite kann immer noch gültige Bilder liefern

Beachten Sie, dass die äußere Schleife Pdf.PageNumber bei jeder Iteration setzt. Diese Zuweisung ist es, die die Seite in den internen Status der Komponente lädt und BitmapCount aussagekräftig macht. Überspringen Sie dies, lesen Sie wiederholt die Zählung derselben Seite. Das Muster fühlt sich beim Schreiben redundant an, aber so ist die API entworfen: Die Seite ist ein Cursor, keine Auflistung

Wahl eines Ausgabeformats

BMP ist verlustfrei und immer ohne zusätzliche Units verfügbar, was es zu einem soliden Standard macht, wenn Sie noch nicht wissen, was das Bild enthält. Wenn die Dateigröße wichtig ist, sagt Ihnen das Pixelformat des zurückgegebenen TBitmap, welcher Codec angemessen ist. Eine 32-Bit-Bitmap trägt einen Alphakanal; PNG bewahrt diesen ohne Verlust. Ein großes 24-Bit-Bild mit kontinuierlichen Tönen ist ein Kandidat für JPEG. Kleinere Bilder oder solche, die mit einer begrenzten Palette gezeichnet wurden, belässt man im Allgemeinen besser als BMP, anstatt sie durch JPEG laufen zu lassen, was bei niedrigen Qualitätseinstellungen Blockartefakte hinzufügt und bei hohen Einstellungen wenig spart

procedure SaveBitmap(Bmp: TBitmap; const FileName: string);
var
  Jpg: TJPEGImage;
begin
  case UpperCase(ExtractFileExt(FileName)) of
    '.JPG', '.JPEG':
      begin
        Jpg := TJPEGImage.Create;
        try
          Jpg.Assign(Bmp);
          Jpg.CompressionQuality := 85;
          Jpg.SaveToFile(FileName);
        finally
          Jpg.Free;
        end;
      end;
  else
    Bmp.SaveToFile(FileName);  // BMP: lossless, no extra units
  end;
end;

In der Praxis wird die Formatwahl von Bmp.PixelFormat und den Dimensionen bestimmt. Wenn PixelFormat = pf32bit ist, benötigen Sie ein Format, das Alpha trägt; PNG ist die offensichtliche Wahl, obwohl es in älteren Delphi-Versionen die PNGImage-Unit erfordert. Für 24-Bit-Bilder, die breiter als etwa 300 Pixel sind, bietet JPEG bei Qualität 85 eine Reduzierung der Größe auf ein Drittel gegenüber BMP ohne wahrnehmbaren Verlust bei den meisten fotografischen Inhalten. Unterhalb dieser Schwelle ist BMP in der Größe vergleichbar und vermeidet jegliche Qualitätsentscheidung gänzlich

Was BitmapCount zählt und was nicht

PDF unterscheidet zwischen Bild-XObjects und Vektorgrafiken, die mit Pfadoperatoren gezeichnet wurden. Eine Seite, die visuell komplex aussieht, kann einen BitmapCount von null zurückgeben, wenn jedes Element ein Vektor ist. Eingescannte Seiten geben fast immer genau eins zurück: Der Scanner schreibt den gesamten Scan als ein einziges ganzseitiges Bild-XObject mit der Auflösung, auf die der Scanner eingestellt war. Seiten, die gesetzten Text mit eingebetteten Fotos mischen, geben einen Eintrag pro Foto zurück. Dekorative Linien, schattierte Hintergründe und Tabellenränder erscheinen in der Bitmap-Zählung normalerweise überhaupt nicht

Die Zählung schließt auch keine Inline-Bilder ein, ein selten verwendetes PDF-Konstrukt, bei dem Bilddaten direkt im Inhaltsstrom der Seite eingebettet sind, anstatt als benanntes XObject. Diese fallen außerhalb dessen, was diese API zugänglich macht; sie sind in realen Dokumenten selten genug, dass die meisten Extraktionstools sie einfach nicht handhaben

Ein Detail, das es wert ist, beachtet zu werden: Der BitmapCount, den Sie lesen, gilt für die aktuelle Seite ab der letzten Zuweisung von PageNumber. Wenn Ihr Code verzweigt oder eine Funktion aufruft, die PageNumber zwischen dem Zählen und dem Abrufen ändert, lesen Sie möglicherweise weniger Bilder, als Sie Platz dafür zugewiesen haben, oder indexieren über das Ende hinaus. Halten Sie das Lesen der Zählung und die Bitmap[]-Schleife auf derselben Seite, ohne dazwischen PageNumber anzufassen

Verwendung von TPdfView in einer Formularanwendung

Die TPdfView-Komponente legt dieselben Eigenschaften BitmapCount und Bitmap[] offen, aber die Seite, von der sie liest, ist die aktuell angezeigte Seite der Ansicht, nicht TPdf.PageNumber. Die beiden Seitenzeiger sind unabhängig; das Setzen des einen bewegt den anderen nicht. In einer VCL-Formularanwendung mit einem Live-Viewer können Sie Pdf.PageNumber := N aufrufen, um die Extraktion durch TPdf zu steuern, während der Viewer auf dem bleibt, wohin der Benutzer zuletzt gescrollt hat. Diese Trennung ist beabsichtigt und hält den Anzeigezustand des Viewers sauber, während eine Hintergrundextraktion läuft

Speicher und Leistung bei Stapelverarbeitungsaufträgen

Über ein großes Archiv hinweg ist das Speicherbudget die Hauptsache, auf die man achten muss. Jeder Bitmap[]-Aufruf weist ein neues TBitmap auf dem Heap zu, und auf einer eingescannten Seite mit 300 DPI sind das leicht 25 MB an rohen Pixeldaten vor jeder Codierung. Wenn Sie Seiten in einer engen Schleife verarbeiten, ohne zwischen den Iterationen freizugeben, wächst das Working Set linear mit der Anzahl der Bilder. Die richtige Form ist immer: eine Bitmap abrufen, tun, was Sie tun müssen, sie freigeben, die nächste abrufen. Wenn Sie Referenzen auf mehrere Bitmaps gleichzeitig für einen Vergleichsschritt halten müssen, zählen Sie diese zuerst mit BitmapCount und weisen Sie Ihren Container entsprechend zu, dann geben Sie jede einzelne frei, sobald Sie mit ihr fertig sind, anstatt dies auf die Bereinigung am Ende des Dokuments zu verschieben. Bei einem Dokument mit 500 eingescannten Seiten kann dieser Unterschied den Unterschied zwischen 25 MB und 12 GB Spitzen-RSS ausmachen

Die hier gezeigten Eigenschaften BitmapCount und Bitmap[] sind Teil der PDFium-Komponente für Delphi und C++Builder