Artykuł techniczny

Połącz zeskanowane obrazy w jeden PDF z PDFium Component

Przebieg skanera z podajnikiem arkuszy zostawia Ci folder JPEG-ów, po jednym na stronę, o nazwach 0001.jpg, 0002.jpg i tak dalej. Nikt nie chce przeglądać akt sprawy, klikając przez sto miniaturek; tego, czego potrzebuje archiwum, to jeden PDF na akta sprawy, ze stronami w odpowiedniej kolejności. Ten ostatni krok, zamiana ponumerowanego stosu skanów w pojedynczy uporządkowany 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ń

Potok wsadowy w Delphi używa wywołań PDFium Component AddPage, PageNumber, AddImage i SaveAs, aby zamienić folder ponumerowanych skanów w jeden uporządkowany PDF
Każdy skan staje się jedną stroną utworzoną przez AddPage i wycelowaną przez PageNumber, zanim AddImage ją narysuje; SaveAs zapisuje ukończony dokument raz

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;   // szerokość A4 w punktach
  PageH = 842.0;   // wysokość A4 w punktach
  Margin = 36.0;   // półcalowa ramka wokół każdego skanu
var
  I: Integer;
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                       // nowy, pusty, już aktywny
    for I := 0 to Files.Count - 1 do
    begin
      Pdf.AddPage(I + 1, PageW, PageH);       // indeks strony od 1
      Pdf.PageNumber := I + 1;                // ustaw nową stronę jako bieżącą
      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

Diagram strony A4 pokazuje, jak PDFium Component AddImage umieszcza przeskalowany skan wewnątrz marginesów za pomocą kodu Delphi i początku układu w lewym dolnym rogu
AddImage bierze lewy dolny róg prostokąta rozmieszczenia, więc dopasowanie i wyśrodkowanie są liczone w punktach strony od początku
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 itd. przez jednostki graficzne VCL

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

    // Dopasuj do marginesów bez zniekształcania skanu.
    Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
    DrawW := Pic.Width * Scale;
    DrawH := Pic.Height * Scale;

    // Wyśrodkuj: pozostałe miejsce dzielone równo. Y mierzone od dolnej krawędzi strony.
    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ą JPEG. Załadowanie JPEG-a do TPicture dekoduje go do pełnej mapy bitowej i to mapa bitowa trafia do PDF-a. Dekodowanie nie kosztuje jakości, a PDFium zapisuje piksele bezstratnie, więc strona wygląda tak samo; płacisz rozmiarem i czasem, bo skan, który przyszedł jako zwięzły JPEG, zostaje zapisany jako bezstratnie skompresowane surowe piksele, zwykle wielokrotnie większe niż plik, z którego przyszedł. AddJpegImage osadza oryginalne skompresowane bajty prosto na stronie, pobierając je ze strumienia, więc wynik pozostaje bliski rozmiarowi skanów, a partia pomija całkowicie pracę dekodowania

AddJpegImage osadza oryginalne bajty JPEG na stronie PDFium Component, podczas gdy AddImage dekoduje, a SaveAs ponownie koduje piksele w Delphi
AddJpegImage osadza skompresowane bajty skanera jak są, omijając przebiegi dekodowania i ponownego kodowania, które wykonują AddImage i SaveAs
var
  Stream: TFileStream;
begin
  // ... po AddPage + PageNumber dla bieżącej strony ...
  Stream := TFileStream.Create(FileName, fmOpenRead);
  try
    // Osadza bajty JPEG bez zmian; bez cyklu dekodowania/ponownego kodowania.
    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ć

// Podpis pod skanem: Y maleje w kierunku dolnej krawędzi strony.
Pdf.AddText('File: ' + ExtractFileName(FileName), 'Helvetica', 9,
  X, Y - 14, clGray);

Ostatnia uwaga na temat zapisu. SaveAs zwraca Boolean, a False oznacza, że dokumentu nie udało się serializować, więc sprawdź wynik, zamiast zakładać, że zapis się powiódł. Kłopoty z systemem plików idą drugą drogą: brakujący folder wyjściowy albo zablokowany plik docelowy podnosi wyjątek z warstwy plikowej, więc zadanie wsadowe chce i sprawdzenia wyniku, i obsługi, która zaloguje, którego pliku akt dotyczyła awaria. Gdy pętla się zakończy i plik zostanie zapisany, masz dokładnie to, czego potrzebowało archiwum: jeden uporządkowany PDF na akta sprawy, ze stronami przeskalowanymi do rozmiaru, gotowy do odczytu w dowolnej 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