Odborný článok

Spájanie naskenovaných obrázkov do jedného PDF pomocou PDFium Component v Delphi

Tím pre spracovanie pohľadávok mal k dispozícii tridsať rokov papierových spisov, ktoré prechádzali cez prieťahový skener. Skener ukladal jeden obrázok JPEG na stránku do priečinka s názvami ako 0001.jpg, 0002.jpg a tak ďalej. Archív však v skutočnosti potreboval jeden súbor PDF na každý spis s usporiadanými stránkami, aby si revízor mohol otvoriť jediný dokument namiesto preklikávania stoviek miniatúr obrázkov. Tento posledný krok, teda premena očíslovanej kopy skenov na jeden usporiadaný dokument PDF, je presne témou tohto článku

PDFium Component to zvláda priamo. Okrem vykresľovania a extrakcie textu dokáže tento komponent vytvoriť dokument PDF úplne od nuly: vytvoriť prázdny dokument, pridať prázdnu stránku s ľubovoľnou veľkosťou, umiestniť na ňu obrázok v súradniciach používateľského priestoru a následne ho uložiť. Celý tento proces prebieha na komponente TPdf, takže hromadný prevodník je v podstate len cyklus nad názvami súborov spojený s niekoľkými volaniami

Dávková pipeline Delphi používa volania AddPage, PageNumber, AddImage a SaveAs PDFium Component na zmenu priečinka očíslovaných skenov na jedno zoradené PDF
Každý sken sa stane jednou stranou vytvorenou AddPage a mieriacou PageNumber skôr, než ju AddImage nakreslí; SaveAs zapíše hotový dokument raz

Priebeh konverzie

Pri každom skene musia prebehnúť tri veci. Rozhodnete o veľkosti stránky, umiestnite obrázok na stránku s vynechaním okrajov a prejdete na ďalšiu stránku. PDFium Component poskytuje metódu pre každú z týchto činností: AddPage vytvorí prázdnu stránku so zadanou veľkosťou, AddImage (alebo AddPicture, ak už držíte objekt TPicture) vykreslí bitmapu na aktuálnu stránku a vlastnosť PageNumber určuje, na ktorú stránku budú smerovať nasledujúce volania vykresľovania

Jedným z detailov, ktorý často pletie vývojárov, je súradnicový systém. Používateľský priestor PDF umiestňuje počiatok do ľavého dolného rohu stránky, pričom os Y rastie smerom nahor – čo je presný opak súradníc obrazovky, po ktorých vývojári v Delphi siahajú inštinktívne. Súradnice X, Y, ktoré odovzdávate metóde AddImage, predstavujú ľavý dolný roh obdĺžnika obrázka a Width, Height určujú veľkosť umiestnenia v bodoch, nie veľkosť zdrojového súboru v pixeloch. Ak si to pomiešate, vaše skeny skončia mimo stránky alebo otočené naopak

Vytvorenie dokumentu a pridanie stránky pre každý sken

Začnite s prázdnym dokumentom. Metóda CreateDocument vytvorí nový súbor PDF a ponechá komponent aktívny, takže nie je potrebný žiadny samostatný krok na otvorenie. Odtiaľ prechádzate zoznam naskenovaných súborov a pre každý z nich pridáte stránku, nastavíte ju ako aktuálnu a umiestnite obrázok. Rozmery stránky sú tu nastavené na formát A4 v bodoch (595 × 842 na výšku), čo je štandardná veľkosť listu pre archivovanú korešpondenciu

procedure TArchiveForm.ScansToPdf(const Files: TStrings; const OutputPath: string);
const
  PageW = 595.0;   // šírka A4 v bodoch
  PageH = 842.0;   // výška A4 v bodoch
  Margin = 36.0;   // pol palcový okraj okolo každého skenu
var
  I: Integer;
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                       // nový, prázdny, už aktívny
    for I := 0 to Files.Count - 1 do
    begin
      Pdf.AddPage(I + 1, PageW, PageH);       // index stránky od 1
      Pdf.PageNumber := I + 1;                // nastaviť novú stránku ako aktuálnu
      PlaceScan(Pdf, Files[I], PageW, PageH, Margin);
    end;
    Pdf.SaveAs(OutputPath);
  finally
    Pdf.Free;
  end;
end;

Každá iterácia vytvorí stránku a okamžite na ňu nastaví vlastnosť PageNumber. Tento druhý riadok je dôležité: metóda AddPage vloží stránku, ale metódy vykresľovania pracujú s tou stránkou, ktorá je aktuálna. Nastavenie PageNumber je to, čo nasmeruje AddImage na stránku, ktorú ste práve vytvorili. Ak to vynecháte, vaše obrázky sa navrstvia na tú stránku, ktorá bola načítaná predtým

V tomto cykle sa skrýva jeden predpoklad: poradie zoznamu Files. Skener pomenuje stránky od 0001.jpg po 0100.jpg, ale výpis adresára ich nemusí vždy vrátiť usporiadané. V momente, keď narazíte na súbor page9.jpg vedľa page10.jpg, bežné triedenie reťazcov zaradí stranu 10 pred stranu 9. Pred spustením cyklu zoznam explicitne usporiadajte a pri skenovaní uprednostňujte názvy doplnené nulami na začiatku, aby lexikálne poradie zodpovedalo poradiu stránok. Poradie stránok je prvá vec, ktorú si revízor všimne, a je to tá najjednoduchšia chyba, ktorej sa dá predísť

Umiestnenie skenu a zachovanie jeho pomeru strán

Sken má zriedka rovnaký tvar ako stránka. Ak ho roztiahnete na celú plochu listu, skreslíte text; ak ho umiestnite v plnej veľkosti v pixeloch, pretečie mimo okrajov. Riešením je zmeniť mierku podľa menšieho z dvoch pomerov (prispôsobenie na šírku alebo prispôsobenie na výšku) a zvyšný priestor vycentrovať. Keďže počiatok súradníc leží vľavo dole, centrovanie znamená rovnomerné rozdelenie zostávajúceho priestoru a jeho pripočítanie k X aj Y

Diagram strany A4 ukazuje, ako AddImage PDFium Component umiestni škálovaný sken vnútri okrajov pomocou kódu Delphi a pôvodu vľavo dole
AddImage berie ľavý dolný roh umiestňovacieho obdĺžnika, takže prispôsobenie a centrovanie sa počítajú v bodoch strany od počiatku
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 atď. cez grafické jednotky VCL

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

    // Prispôsobiť do okrajov bez skreslenia skenu.
    Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
    DrawW := Pic.Width * Scale;
    DrawH := Pic.Height * Scale;

    // Vycentrovať: zostávajúci priestor rozdelený rovnomerne. Y sa meria od spodného okraja stránky.
    X := (PageW - DrawW) / 2;
    Y := (PageH - DrawH) / 2;

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

Tento kód načíta súbor raz, aby zistil jeho rozmery v pixeloch, vypočíta jednotnú mierku a odovzdá cieľový obdĺžnik metóde AddImage. Metóda AddImage prijíma priamo cestu k súboru a smeruje ju cez rovnaký proces spracovania obrázkov ako AddPicture, takže akýkoľvek formát, ktorý grafické jednotky VCL rozpoznajú, funguje bez potreby špeciálnych úprav. Ak už máte obrázok dekódovaný v objekte TPicture (napríklad z panela náhľadu), zavolajte AddPicture(Pic, X, Y, DrawW, DrawH) s rovnakým obdĺžnikom a vynechajte druhé čítanie súboru

Vynechanie dekódovania pri skenoch vo formáte JPEG

Skenery takmer vždy produkujú formát JPEG. Načítanie JPEG do TPicture ho dekóduje na bitmapu a PDFium ho pri ukladaní znova zakóduje, čo predstavuje dva stratové prepočty, ktoré vôbec nepotrebujete. Metóda AddJpegImage vloží pôvodné komprimované bajty priamo do stránky zo streamu, čo je rýchlejšie a vizuálne čistejšie pri hromadnom spracovaní

AddJpegImage vloží pôvodné bajty JPEG do strany PDFium Component, zatiaľ čo AddImage dekóduje a SaveAs znovu zakóduje pixely v Delphi
AddJpegImage vloží komprimované bajty skenera tak, ako sú, a vyhne sa tak priebehom dekódovania a opätovného kódovania, ktoré vykonávajú AddImage a SaveAs
var
  Stream: TFileStream;
begin
  // ... po AddPage + PageNumber pre aktuálnu stránku ...
  Stream := TFileStream.Create(FileName, fmOpenRead);
  try
    // Vloží bajty JPEG v pôvodnej podobe; žiadny cyklus dekódovania/prekódovania.
    Pdf.AddJpegImage(Stream, X, Y, DrawW, DrawH);
  finally
    Stream.Free;
  end;
end;

Súradnice X, Y, DrawW a DrawH stále počítate rovnakým spôsobom, pretože na zmenu mierky potrebujete poznať rozmery v pixeloch. Tieto rozmery prečítajte zo súboru alebo rýchlym rozborom hlavičky a potom odovzdajte surový stream metóde AddJpegImage. Pre skeny vo formáte PNG alebo TIFF je správnou voľbou metóda AddImage; skratku s JPEG si vyhraďte iba pre formát, na ktorý sa skutočne vzťahuje

Označenie každej stránky

Archivované skeny sa jednoduchšie kontrolujú, keď každá stránka nesie názov svojho zdrojového súboru. Metóda AddText vykreslí reťazec na zadaných súradniciach používateľského priestoru, takže popisok sa umiestni priamo pod obrázok. Nezabudnite na obrátenú os Y: ak chcete umiestniť popisok pod sken, odčítavate od dolného okraja obrázka, namiesto toho, aby ste k nemu pripočítavali

// Popisok pod skenom: Y klesá smerom k spodnému okraju stránky.
Pdf.AddText('File: ' + ExtractFileName(FileName), 'Helvetica', 9,
  X, Y - 14, clGray);

Posledný bod k ukladaniu. SaveAs is a function that returns a Boolean, so in production code check its result rather than assuming the write succeeded; a full disk or a locked output path fails quietly otherwise. Po dokončení cyklu a zápise súboru máte presne to, čo archív vyžadoval: jeden usporiadaný dokument PDF na spis so stránkami prispôsobenými na šírku, pripravený na čítanie v ľubovoľnom prehliadači

Rovnaké stavebné kamene pokrývajú aj príbuzné úlohy. Zmeňte pravidlo určovania veľkosti stránky a získate fotoknihu s jedným obrázkom na list; ponechajte cyklus, ale čítajte z viacstránkového zdroja TIFF a získate prevodník faxového archívu. Ak sa chcete dozvedieť viac o programovom vytváraní PDF, prečítajte si článok o vytváraní PDF dokumentov úplne od nuly pomocou PDFium Component; ak chcete výsledok neskôr zobraziť na obrazovke, pozrite si článok o prevode PDF stránok na JPEG obrázky pomocou PDFium Component

Komponent PDFium Component od loslab.com spája rozhrania pre vytváranie dokumentov, vykresľovanie a prácu s textom použité v tejto sérii