Technický článek

Sloučení naskenovaných obrázků do jednoho PDF s komponentou PDFium v Delphi

Tým zpracovávající pojistné události měl třicet let papírových spisů, které procházely skenerem s podavačem listů. Skener vyplivl jeden JPEG na stránku do složky, pojmenovaný 0001.jpg, 0002.jpg a tak dále. Co ale archiv ve skutečnosti potřeboval, byl jeden PDF dokument na spisový případ, se stránkami popořadě, aby revizor mohl otevřít jediný dokument namísto proklikávání se přes sto miniatur obrázků. Ten poslední krok – přeměnit očíslovanou hromadu skenů na jedno uspořádané PDF – je právě tímto úkolem

Komponenta PDFium to zvládne přímo. Kromě vykreslování a extrakce textu dokáže komponenta postavit PDF od nuly: vytvořit prázdný dokument, přidat čistou stránku libovolné velikosti, umístit na tuto stránku obrázek v souřadnicích uživatelského prostoru a poté uložit. Celé workflow žije na komponentě TPdf, takže dávkový konvertor představuje cyklus přes názvy souborů plus několik volání

Tvar konverze

Pro každý sken se musí stát tři věci. Rozhodnete o velikosti stránky, umístíte obrázek dovnitř stránky s ponecháním okraje a přejdete na další stránku. Komponenta PDFium vám na každou z nich dává jednu metodu: AddPage vytvoří prázdnou stránku dané velikosti, AddImage (nebo AddPicture, pokud už držíte instanci TPicture) nakreslí bitmapu na aktuální stránku, a PageNumber řekne komponentě, na kterou stránku míří následná volání kreslení

Jediný detail, který lidi zaskočí, je souřadnicový systém. Uživatelský prostor (user space) PDF umisťuje počátek do levého dolního rohu stránky s tím, že osa Y roste směrem nahoru, což je přesný opak souřadnic obrazovky, po kterých vývojáři v Delphi sahají ze zvyku. Souřadnice X, Y, které předáváte funkci AddImage, je levý dolní roh obdélníku obrázku, a Width, Height jsou rozměry umístění v bodech (points), nikoli v pixelech zdrojového souboru. Pokud si to spletete, vaše skeny přistanou mimo stránku nebo budou vzhůru nohama vůči tomu, kde jste je očekávali

Vytvoření dokumentu a stránky pro každý sken

Začněte s prázdným dokumentem. CreateDocument alokuje nové PDF a ponechá komponentu aktivní, takže neexistuje žádný samostatný krok pro otevření. Odtud procházíte seznam naskenovaných souborů a pro každý z nich přidáte stránku, uděláte z ní aktuální a umístíte obrázek. Rozměry stránky jsou zde A4 v bodech (595 × 842 na výšku), standardní velikost listu pro archivovanou korespondenci

procedure TArchiveForm.ScansToPdf(const Files: TStrings; const OutputPath: string);
const
  PageW = 595.0;   // šířka A4 v bodech
  PageH = 842.0;   // výška A4 v bodech
  Margin = 36.0;   // půlpalcový okraj kolem každého skenu
var
  I: Integer;
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                       // nové, prázdné, již aktivní
    for I := 0 to Files.Count - 1 do
    begin
      Pdf.AddPage(I + 1, PageW, PageH);       // index stránky založený na 1
      Pdf.PageNumber := I + 1;                // učiní novou stránku aktuální
      PlaceScan(Pdf, Files[I], PageW, PageH, Margin);
    end;
    Pdf.SaveAs(OutputPath);
  finally
    Pdf.Free;
  end;
end;

Každá iterace vytvoří stránku a okamžitě na ni nastaví PageNumber. Tento druhý řádek je důležitý: AddPage vloží stránku, ale kreslící metody působí na tu stránku, která je zrovna aktuální, takže právě nastavení PageNumber je to, co nasměruje AddImage na stránku, kterou jste zrovna vytvořili. Pokud to přeskočíte, vaše obrázky se naskládají na tu stránku, která byla náhodou načtena předtím

Jeden předpoklad se skrývá v této smyčce: řazení prvků v seznamu Files. Skener pojmenovává stránky 0001.jpg0100.jpg, ale procházení adresáře je nemusí vždy vrátit seřazené, a v momentě kdy narazíte na page9.jpg vedle page10.jpg, obyčejné lexikální řazení řetězců postaví stranu 10 před stranu 9. Explicitně seznam seřaďte před smyčkou, a preferujte názvy doplněné nulami už při samotném skenování, takže lexikální pořadí odpovídá pořadí stránek. Sled stránek je to první, čeho si revizor okamžitě všimne, a je to ta nejlacinější chyba, které se dá zabránit

Umístění skenu se zachováním jeho poměru stran

Sken má zřídkakdy stejný tvar jako stránka. Pokud jej roztáhnete, abyste zaplnili list, zkreslíte text; pokud jej umístíte v plné velikosti pixelů, přeteče. Opravou je provést škálování menším z těchto dvou poměrů, přizpůsobením šířky (width-fit) nebo výšky (height-fit), a to co zbyde, vycentrovat. Protože počátek sedí v levém dolním rohu, centrování znamená rozdělit zbývající místo rovnoměrně a přidat jej jak k X, tak i k 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 atd. přes VCL grafické jednotky

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

    // Přizpůsobit okrajům bez zkreslení skenu.
    Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
    DrawW := Pic.Width * Scale;
    DrawH := Pic.Height * Scale;

    // Vycentrování: zbylý prostor se rozdělí napůl. Y se měří od spodku stránky.
    X := (PageW - DrawW) / 2;
    Y := (PageH - DrawH) / 2;

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

Toto načte soubor jednou pro přečtení jeho rozměrů v pixelech, vypočítá jediné stejnoměrné měřítko a předá obdélník s umístěním funkci AddImage. AddImage přijímá cestu k souboru přímo a směruje ji přes stejný obrazový tok jako AddPicture, takže jakýkoli formát, který grafické VCL jednotky rozpoznají, bude fungovat bez výjimek. Pokud už máte obrázek dekódovaný v instanci TPicture z nějakého panelu náhledu, zavolejte AddPicture(Pic, X, Y, DrawW, DrawH) se stejným obdélníkem a přeskočte to druhé načítání souboru

Přeskočení dekódování u JPEG skenů

Skenery téměř vždy chrlí formát JPEG. Načtení JPEGu do objektu TPicture jej dekóduje na bitmapu a poté jej PDFium při ukládání znovu kóduje, což jsou dva ztrátové kroky sem a tam, které nepotřebujete. AddJpegImage vloží původní komprimované bajty ze streamu rovnou do stránky, což je u velkoobjemové dávky rychlejší i vizuálně čistší

var
  Stream: TFileStream;
begin
  // ... po AddPage + PageNumber pro aktuální stránku ...
  Stream := TFileStream.Create(FileName, fmOpenRead);
  try
    // Vloží bajty JPEG přesně tak, jak jsou; žádný cyklus dekódování/překódování.
    Pdf.AddJpegImage(Stream, X, Y, DrawW, DrawH);
  finally
    Stream.Free;
  end;
end;

Pořád vypočítáte X, Y, DrawW a DrawH stejným způsobem, protože rozměry v pixelech pro účely měřítka potřebujete. Ty si přečtete ze souboru nebo rychlým rozborem hlavičky a pak funkci AddJpegImage podstrčíte přímo surový datový proud. U skenů v PNG nebo TIFF je správnou cestou AddImage; ponechte zkratku pro JPEG tomu formátu, na který se ve skutečnosti vztahuje

Štítkování každé stránky

Archivované skeny se snadněji kontrolují, když každá strana nese název svého zdrojového souboru. AddText vykreslí řetězec na souřadnici v uživatelském prostoru, takže popisek sedí těsně pod obrázkem. Pamatujte na otočenou osu Y: chcete-li umístit štítek pod sken, od spodní hrany obrázku odečítáte, místo abyste k ní přičítali

// Titulek pod skenem: Y se směrem ke spodnímu okraji stránky zmenšuje.
Pdf.AddText('Soubor: ' + ExtractFileName(FileName), 'Helvetica', 9,
  X, Y - 14, clGray);

A nakonec ještě jedna poznámka k ukládání. SaveAs je funkce vracející Boolean, takže v produkčním kódu její výsledek kontrolujte místo toho, abyste předpokládali, že zápis uspěl; plný disk nebo zamčená výstupní cesta jinak tiše selže. Jakmile se smyčka dokončí a soubor je zapsán, máte přesně to, co archiv požadoval: jedno seřazené PDF na jeden spisový případ, stránky ve velikosti uzpůsobené pro přizpůsobení, připraveno ke čtení v libovolném prohlížeči

Stejné stavební bloky pokrývají příbuzné úkoly. Změňte pravidlo pro velikost jednotlivé stránky a máte fotoknihu s jedním obrázkem na každý list; zachovejte smyčku ale čtěte z vícestránkového TIFF zdroje, a máte konvertor faxového archivu. Pokud byste chtěli mít širší pohled na to, jak se dají tvořit PDF z kódu, podívejte se na článek vytváření PDF dokumentů od nuly s komponentou PDFium; abyste vyrendrovali výsledek zpátky na obrazovku, přečtěte si článek převádění stránek PDF na obrázky JPEG pomocí komponenty PDFium

Komponenta PDFium Component z loslab.com obsahuje v balení API rozhraní pro tvorbu dokumentů, renderování i práci s textem, tak, jak je použito v celém tomto seriálu