Technischer Artikel

Gescannte Bilder in Delphi mit PDFium Component zu einem PDF zusammenfügen

Ein Team für die Schadensbearbeitung hatte Papierakten aus dreißig Jahren, die durch einen Einzugsscanner liefen. Der Scanner spuckte ein JPEG pro Seite in einen Ordner aus, benannt als 0001.jpg, 0002.jpg und so weiter. Was das Archiv jedoch eigentlich brauchte, war ein PDF pro Fallakte, mit den Seiten in der richtigen Reihenfolge, damit ein Prüfer ein einzelnes Dokument öffnen konnte, anstatt sich durch hundert Bild-Thumbnails klicken zu müssen. Dieser letzte Schritt, aus einem nummerierten Stapel von Scans ein einziges geordnetes PDF zu machen, ist hier die Aufgabe

PDFium Component erledigt dies direkt. Neben dem Rendern und der Textextraktion kann die Komponente ein PDF von Grund auf neu erstellen: ein leeres Dokument erstellen, eine leere Seite in beliebiger Größe hinzufügen, ein Bild in Benutzerbereichskoordinaten auf dieser Seite platzieren und dann speichern. Die gesamte Pipeline befindet sich in der TPdf-Komponente, sodass ein Stapelkonverter eine Schleife über Dateinamen sowie eine Handvoll Aufrufe ist

Die Form der Konvertierung

Für jeden Scan müssen drei Dinge geschehen. Sie legen die Seitengröße fest, platzieren das Bild mit einem Rand auf der Seite und gehen zur nächsten Seite über. PDFium Component bietet für jeden Schritt eine Methode: AddPage erstellt eine leere Seite in einer bestimmten Größe, AddImage (oder AddPicture, falls Sie bereits ein TPicture haben) zeichnet das Bitmap auf die aktuelle Seite, und PageNumber teilt der Komponente mit, auf welche Seite sich nachfolgende Zeichenaufrufe beziehen

Das einzige Detail, über das man leicht stolpert, ist das Koordinatensystem. Der PDF-Benutzerbereich platziert den Ursprung in der unteren linken Ecke der Seite, wobei Y nach oben zunimmt – das Gegenteil der Bildschirmkoordinaten, nach denen Delphi-Entwickler aus Reflex greifen. Das X, Y, das Sie an AddImage übergeben, ist die untere linke Ecke des Bildrechtecks, und Width, Height sind die Platzierungsgröße in Punkten, nicht die Pixelgröße der Quelldatei. Wenn Sie das verwechseln, landen Ihre Scans außerhalb der Seite oder relativ zu dem Ort, an dem Sie sie erwartet haben, auf dem Kopf

Erstellen des Dokuments und einer Seite pro Scan

Beginnen Sie mit einem leeren Dokument. CreateDocument ordnet ein neues PDF zu und lässt die Komponente aktiv, sodass es keinen separaten Schritt zum Öffnen gibt. Von dort aus durchlaufen Sie die Liste der gescannten Dateien, und für jede fügen Sie eine Seite hinzu, machen sie aktuell und platzieren das Bild. Die Seitenabmessungen betragen hier A4 in Punkten (595 × 842 Hochformat), die Standard-Blattgröße für archivierte Korrespondenz

procedure TArchiveForm.ScansToPdf(const Files: TStrings; const OutputPath: string);
const
  PageW = 595.0;   // A4 width in points
  PageH = 842.0;   // A4 height in points
  Margin = 36.0;   // half-inch border around each scan
var
  I: Integer;
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                       // new, empty, already active
    for I := 0 to Files.Count - 1 do
    begin
      Pdf.AddPage(I + 1, PageW, PageH);       // 1-based page index
      Pdf.PageNumber := I + 1;                // make the new page current
      PlaceScan(Pdf, Files[I], PageW, PageH, Margin);
    end;
    Pdf.SaveAs(OutputPath);
  finally
    Pdf.Free;
  end;
end;

Jeder Iterationsschritt erstellt eine Seite und weist ihr sofort PageNumber zu. Diese zweite Zeile ist wichtig: AddPage fügt die Seite ein, aber die Zeichenmethoden wirken sich auf die jeweils aktuelle Seite aus. Das Festlegen von PageNumber sorgt also dafür, dass AddImage auf die gerade erstellte Seite abzielt. Wenn Sie dies überspringen, stapeln sich Ihre Bilder auf der Seite, die zuvor geladen war

In dieser Schleife verbirgt sich eine Annahme: die Reihenfolge der Files. Ein Scanner benennt Seiten von 0001.jpg bis 0100.jpg, aber eine Verzeichnisaufzählung gibt sie nicht immer sortiert zurück, und sobald Sie auf page9.jpg neben page10.jpg stoßen, setzt eine einfache Zeichenfolgensortierung Seite 10 vor Seite 9. Sortieren Sie die Liste vor der Schleife explizit und bevorzugen Sie beim Scannen nullgepolsterte Namen, damit die lexikalische Reihenfolge der Seitenreihenfolge entspricht. Die Seitenfolge ist das Erste, was einem Prüfer sofort auffällt, und es ist der am einfachsten zu vermeidende Fehler

Einen Scan platzieren und sein Seitenverhältnis beibehalten

Ein Scan hat selten dieselbe Form wie die Seite. Wenn Sie ihn dehnen, um das Blatt zu füllen, verzerren Sie den Text; wenn Sie ihn in voller Pixelgröße platzieren, läuft er über. Die Lösung besteht darin, um das kleinere der beiden Verhältnisse (Breiten- oder Höhenanpassung) zu skalieren und den verbleibenden Rest zu zentrieren. Da der Ursprung unten links liegt, bedeutet Zentrieren, den verbleibenden Platz gleichmäßig aufzuteilen und ihn sowohl zu X als auch zu Y hinzuzufügen

procedure TArchiveForm.PlaceScan(Pdf: TPdf; const FileName: string;
  PageW, PageH, Margin: Double);
var
  Pic: TPicture;
  AvailW, AvailH, Scale, DrawW, DrawH, X, Y: Double;
begin
  Pic := TPicture.Create;
  try
    Pic.LoadFromFile(FileName);              // BMP, JPG, PNG, etc. via the VCL graphics units

    AvailW := PageW - 2 * Margin;
    AvailH := PageH - 2 * Margin;

    // Fit inside the margins without distorting the scan.
    Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
    DrawW := Pic.Width * Scale;
    DrawH := Pic.Height * Scale;

    // Center: leftover space split evenly. Y measured from the page bottom.
    X := (PageW - DrawW) / 2;
    Y := (PageH - DrawH) / 2;

    Pdf.AddImage(FileName, X, Y, DrawW, DrawH);
  finally
    Pic.Free;
  end;
end;

Dies lädt die Datei einmal, um ihre Pixelabmessungen zu lesen, berechnet eine einheitliche Skalierung und übergibt das Platzierungsrechteck an AddImage. AddImage akzeptiert direkt einen Dateipfad und leitet ihn durch dieselbe Bild-Pipeline wie AddPicture. Daher funktioniert jedes Format, das die VCL-Grafikeinheiten erkennen, ohne Sonderfälle. Wenn Sie das Bild bereits in einem TPicture aus einem Vorschaufenster decodiert haben, rufen Sie AddPicture(Pic, X, Y, DrawW, DrawH) mit demselben Rechteck auf und überspringen Sie das zweite Lesen der Datei

Überspringen der Decodierung für JPEG-Scans

Scanner geben fast immer JPEG aus. Das Laden eines JPEGs in ein TPicture decodiert es in ein Bitmap, und dann codiert PDFium es beim Speichern neu – zwei verlustbehaftete Hin- und Herläufe, die Sie nicht benötigen. AddJpegImage bettet die ursprünglichen komprimierten Bytes direkt aus einem Stream in die Seite ein, was für einen Batch mit hohem Volumen sowohl schneller als auch optisch sauberer ist

var
  Stream: TFileStream;
begin
  // ... after AddPage + PageNumber for the current page ...
  Stream := TFileStream.Create(FileName, fmOpenRead);
  try
    // Embeds the JPEG bytes as-is; no decode/re-encode cycle.
    Pdf.AddJpegImage(Stream, X, Y, DrawW, DrawH);
  finally
    Stream.Free;
  end;
end;

Sie berechnen X, Y, DrawW und DrawH weiterhin auf dieselbe Weise, da Sie die Pixelabmessungen zum Skalieren benötigen. Lesen Sie diese aus der Datei oder über eine schnelle Header-Analyse und übergeben Sie dann den rohen Stream an AddJpegImage. Für PNG- oder TIFF-Scans ist der AddImage-Pfad der richtige; reservieren Sie die JPEG-Abkürzung für das Format, auf das sie tatsächlich zutrifft

Beschriften jeder Seite

Archivierte Scans lassen sich leichter prüfen, wenn jede Seite ihren Quelldateinamen trägt. AddText zeichnet eine Zeichenfolge an einer Benutzerbereichskoordinate, sodass eine Beschriftung direkt unter dem Bild sitzt. Denken Sie an die umgekehrte Y-Achse: Um eine Beschriftung unter dem Scan zu platzieren, subtrahieren Sie vom unteren Rand des Bildes, anstatt etwas hinzuzufügen

// Caption below the scan: Y decreases toward the page bottom.
Pdf.AddText('File: ' + ExtractFileName(FileName), 'Helvetica', 9,
  X, Y - 14, clGray);

Noch ein letzter Punkt zum Speichern. SaveAs ist eine Funktion, die einen Boolean zurückgibt. Überprüfen Sie also im Produktionscode deren Ergebnis, anstatt davon auszugehen, dass das Schreiben erfolgreich war. Ein voller Datenträger oder ein gesperrter Ausgabepfad schlägt ansonsten stillschweigend fehl. Sobald die Schleife abgeschlossen ist und die Datei geschrieben wurde, haben Sie genau das, was das Archiv brauchte: ein geordnetes PDF pro Fallakte, passend skalierte Seiten, bereit zum Lesen in jedem Viewer

Dieselben Bausteine decken verwandte Aufgaben ab. Tauschen Sie die Regel für die Seitengröße aus, und Sie erhalten ein Fotobuch mit einem Bild pro Blatt; behalten Sie die Schleife bei, lesen Sie aber aus einer mehrseitigen TIFF-Quelle, und Sie haben einen Fax-Archiv-Konverter. Wenn Sie sich ein umfassenderes Bild vom programmatischen Erstellen von PDFs machen möchten, lesen Sie PDF-Dokumente von Grund auf neu erstellen mit PDFium Component; um das Ergebnis später wieder auf dem Bildschirm zu rendern, lesen Sie PDF-Seiten in JPEG-Bilder konvertieren mit PDFium Component

PDFium Component von loslab.com bündelt die Dokumentenerstellungs-, Rendering- und Text-APIs, die in dieser Serie verwendet werden