Artykuł techniczny

Połącz zeskanowane obrazy w jeden PDF za pomocą komponentu PDFium w Delphi

Zespół przetwarzający roszczenia przepuszczał trzydzieści lat papierowych akt przez skaner z podajnikiem arkuszy. Skaner wypluwał jeden plik JPEG na stronę do folderu, oznaczając je jako 0001.jpg, 0002.jpg i tak dalej. To, czego archiwum faktycznie potrzebowało, to jeden plik PDF na akta sprawy, z odpowiednio ułożonymi stronami, aby recenzent mógł otworzyć pojedynczy dokument zamiast przeklikiwać się przez sto miniaturek obrazów. Ten ostatni krok, zamiana ponumerowanego stosu skanów w pojedynczy uporządkowany plik PDF, to zadanie do wykonania tutaj

Komponent PDFium obsługuje to bezpośrednio. Poza renderowaniem i ekstrakcją tekstu, komponent potrafi zbudować PDF od podstaw: utworzyć pusty dokument, dodać pustą stronę o dowolnym rozmiarze, upuścić na nią obraz we współrzędnych przestrzeni użytkownika, a następnie zapisać. Cały potok przetwarzania opiera się na komponencie TPdf, więc konwerter wsadowy to pętla po nazwach plików plus garść wywołań

Kształt konwersji

Dla każdego skanu muszą zajść trzy rzeczy. Ty decydujesz o rozmiarze strony, umieszczasz obraz wewnątrz strony, pozostawiając margines, i przechodzisz do następnej strony. Komponent PDFium daje ci jedną metodę dla każdej z tych czynności: AddPage tworzy pustą stronę o danym rozmiarze, AddImage (lub AddPicture, jeśli już posiadasz obiekt TPicture) rysuje mapę bitową na bieżącej stronie, a PageNumber informuje komponent, na którą stronę mają trafić kolejne wywołania rysowania

Jednym z detali, na którym ludzie potykają się, jest układ współrzędnych. Przestrzeń użytkownika w dokumencie PDF umieszcza początek układu w lewym dolnym rogu strony, a oś Y rośnie w górę, czyli odwrotnie do współrzędnych ekranowych, po które programiści Delphi sięgają odruchowo. X, Y, które przekazujesz do AddImage, to lewy dolny róg prostokąta obrazu, a Width, Height to rozmiar umieszczenia w punktach, a nie rozmiar pliku źródłowego w pikselach. Pomyl to, a twoje skany wylądują poza stroną lub do góry nogami w stosunku do oczekiwań

Tworzenie dokumentu i strony dla każdego skanu

Zacznij od pustego dokumentu. CreateDocument alokuje nowy plik PDF i pozostawia komponent aktywny, więc nie ma osobnego kroku otwierania. Następnie przechodzisz przez listę zeskanowanych plików, dla każdego dodajesz stronę, ustawiasz ją jako bieżącą i umieszczasz obraz. Wymiary strony tutaj to A4 w punktach (595 × 842 pionowo), standardowy rozmiar arkusza dla archiwizowanej korespondencji

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;

Każda iteracja tworzy stronę i od razu ustawia PageNumber na nią. Ta druga linia ma znaczenie: AddPage wstawia stronę, ale metody rysowania działają na tej stronie, która jest bieżąca, więc ustawienie PageNumber celuje AddImage na nowo utworzoną stronę. Pomiń to, a twoje obrazy będą nawarstwiać się na tej stronie, która przypadkowo była załadowana wcześniej

W tej pętli ukrywa się jedno założenie: kolejność obiektów Files. Skaner nazywa strony od 0001.jpg do 0100.jpg, ale wyliczanie katalogu nie zawsze zwraca je w sposób posortowany, a w momencie, gdy natrafisz na page9.jpg obok page10.jpg, zwykłe sortowanie znakowe umieści stronę 10 przed stroną 9. Posortuj listę jawnie przed pętlą i preferuj nazwy uzupełnione zerami na etapie skanowania, aby kolejność leksykalna pasowała do kolejności stron. Sekwencja stron to jedna rzecz, którą recenzent zauważa natychmiast, i jest to najtańszy błąd, któremu można zapobiec

Umieszczanie skanu z zachowaniem proporcji obrazu

Skan rzadko ma ten sam kształt co strona. Jeśli go rozciągniesz, by wypełnił arkusz, zniekształcisz tekst; jeśli umieścisz go w pełnym rozmiarze w pikselach, przepełni się on za stronę. Rozwiązaniem jest skalowanie za pomocą mniejszego z dwóch współczynników (dopasowanie do szerokości lub wysokości) i wyśrodkowanie tego, co pozostanie. Ponieważ początek układu leży w lewym dolnym rogu, wyśrodkowanie oznacza podzielenie pozostałego miejsca na równe części i dodanie go zarówno do X, jak i Y

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;

To rozwiązanie ładuje plik raz, by odczytać wymiary w pikselach, oblicza pojedynczą, jednolitą skalę i przekazuje prostokąt rozmieszczenia do AddImage. AddImage akceptuje bezpośrednio ścieżkę do pliku i kieruje ją przez ten sam potok przetwarzania obrazów co AddPicture, więc dowolny format rozpoznawany przez jednostki graficzne VCL zadziała bez specjalnego traktowania. Jeśli posiadasz już zdekodowany obraz w obiekcie TPicture z okna podglądu, użyj wywołania AddPicture(Pic, X, Y, DrawW, DrawH) z tym samym prostokątem i pomiń drugie czytanie pliku

Pominięcie dekodowania w przypadku skanów JPEG

Skanery niemal zawsze generują pliki w formacie JPEG. Załadowanie pliku JPEG do TPicture dekoduje go do mapy bitowej, a następnie PDFium koduje go ponownie przy zapisie, czyli wykonuje dwa stratne cykle, których nie potrzebujesz. AddJpegImage osadza oryginalne, skompresowane bajty prosto na stronę, pobierając je ze strumienia, co jest zarówno szybsze, jak i optycznie czystsze przy zadaniach o dużej objętości

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;

Wciąż tak samo musisz wyliczyć wartości X, Y, DrawW i DrawH, ponieważ do skalowania musisz znać wymiary obrazu w pikselach. Odczytaj je z pliku lub w czasie szybkiego przetwarzania nagłówków, a następnie przekaż surowy strumień danych do metody AddJpegImage. W przypadku skanów w formatach PNG i TIFF właściwą drogą jest użycie metody AddImage. Skrót z plikami JPEG zachowaj tylko dla formatu, do którego to faktycznie ma zastosowanie

Oznaczanie każdej strony

Zarchiwizowane skany są łatwiejsze w audytowaniu, gdy każda strona ma wypisaną nazwę pliku źródłowego. AddText rysuje ciąg znaków według współrzędnych przestrzeni użytkownika, więc podpis ląduje tuż pod obrazem. Pamiętaj o odwróconej osi Y: aby wstawić napis pod skanem, od dolnej krawędzi obrazu należy odejmować wartości, a nie je dodawać

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

Ostatnia uwaga na temat zapisu. Metoda SaveAs jest funkcją, która zwraca wartość Boolean, więc w kodzie produkcyjnym sprawdź jej wynik, zamiast zakładać, że zapis się powiódł; w przeciwnym razie zapełniony dysk lub zablokowana ścieżka wyjściowa zawiedzie po cichu. Gdy pętla się zakończy i plik zostanie zapisany, otrzymasz dokładnie to, czego potrzebowało archiwum: jeden uporządkowany dokument PDF na każde akta sprawy ze stronami przeskalowanymi tak, by pasowały na ekran. Gotowe do odczytu w każdej przeglądarce

Te same elementy konstrukcyjne mogą posłużyć przy podobnych zadaniach. Zmień regułę określającą rozmiar poszczególnych stron, a stworzysz książkę ze zdjęciami, po jednym na arkusz; zachowaj tę samą pętlę, ale dane odczytuj z wielostronicowych plików TIFF, a otrzymasz konwerter na potrzeby tworzenia archiwów faksów. Jeśli chcesz uzyskać szerszy obraz na temat tworzenia plików PDF z poziomu programu, zobacz, jak działa tworzenie dokumentów PDF od podstaw za pomocą komponentu PDFium; by na koniec wyrenderować wynik na ekranie, przeczytaj tekst o konwertowaniu stron PDF na obrazy JPEG za pomocą komponentu PDFium

Komponent PDFium z witryny loslab.com łączy interfejsy API do tworzenia dokumentów, renderowania i obsługi tekstu, z których korzystaliśmy w trakcie tworzenia całej tej serii poradników