Műszaki cikk

Szkennelt képek egyesítése egyetlen PDF-be a PDFium Component segítségével Delphiben

Egy kárrendezési csapatnak (claims-processing team) harminc évnyi papíralapú aktát kellett átfuttatnia egy lapolvasó (sheet-fed) szkenneren. A szkenner oldalanként egy JPEG-et köpött ki egy mappába, olyan nevekkel, mint 0001.jpg, 0002.jpg, és így tovább. Amire az archívumnak valójában szüksége volt, az egy ügyiratonkénti egyetlen PDF, amelyben az oldalak sorrendben vannak, hogy az ellenőr egyetlen dokumentumot nyithasson meg ahelyett, hogy száz képbélyegképen (thumbnails) kattintgatna végig. Ez az utolsó lépés, egy számozott halom szkennelt kép egyetlen, rendezett PDF-fé alakítása a mi feladatunk itt

A PDFium Component közvetlenül kezeli ezt. A renderelésen és a szövegkinyerésen (text extraction) túl a komponens a semmiből is képes PDF-et építeni: hozzon létre egy üres dokumentumot, adjon hozzá egy tetszőlegesen méretezett üres oldalt, dobjon rá egy képet arra az oldalra felhasználói térbeli (user-space) koordinátákkal, majd mentse el. Az egész folyamat a TPdf komponensen él, így egy kötegelt konvertáló (batch converter) mindössze egy fájlneveken végiglépdelő ciklusból és egy maroknyi hívásból áll

A konverzió formája

Minden egyes szkenner-kép esetében három dolognak kell történnie. Ön eldönti az oldalméretet, behelyezi a képet az oldalra (margót hagyva), majd továbblép a következő oldalra. A PDFium Component mindegyikre ad egy metódust: az AddPage létrehoz egy üres oldalt adott méretben, az AddImage (vagy az AddPicture, ha már rendelkezik egy TPicture objektummal) belerajzolja a bittérképet (bitmap) az aktuális oldalba, a PageNumber pedig megmondja a komponensnek, hogy a későbbi rajzolási hívások melyik oldalt célozzák

Az az egyetlen részlet, ami megtréfálja az embereket, az a koordináta-rendszer. A PDF felhasználói tere (user space) az origót az oldal bal alsó sarkába teszi, a Y pedig felfelé növekszik, ellentétben a képernyőkoordinátákkal, amihez a Delphi-fejlesztők reflexből nyúlnak. Az X, Y, amit az AddImage-nek átad, a kép téglalapjának bal alsó sarka, a Width, Height pedig az elhelyezési méret pontokban (points), nem pedig a forrásfájl pixelmérete. Ha ezt elvéti, a beszkennelt képei lelógnak majd az oldalról, vagy fejjel lefelé jelennek meg ahhoz képest, ahová várta őket

A dokumentum létrehozása és oldalanként egy szkennelt kép

Kezdje egy üres dokumentummal. A CreateDocument lefoglal egy friss PDF-et, és a komponenst aktívan hagyja, így nincs külön megnyitási lépés. Innentől kezdve végigmegy a szkennelt fájlok listáján, és mindegyikhez hozzáad egy oldalt, aktuálissá teszi, majd elhelyezi a képet. Az oldalméretek itt pontban mért A4-esek (595 × 842 portré), ami az archivált levelezés szabványos lapmérete

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;

Minden iteráció létrehoz egy oldalt, és azonnal beállítja rá a PageNumber-t. Ennek a második sornak nagy jelentősége van: az AddPage beszúrja az oldalt, de a rajzoló metódusok azon az oldalon hatnak, amelyik éppen aktuális, így a PageNumber beállítása az, ami az AddImage-t az éppen elkészített oldalra irányítja. Hagyja ki, és a képei egymásra fognak rakódni azon az oldalon, amelyik éppen be volt töltve

Egy feltételezés bújik meg ebben a ciklusban: a Files (fájlok) sorrendje. Egy szkenner 0001.jpg-től 0100.jpg-ig nevezi el az oldalakat, de a könyvtár felsorolása nem mindig rendezve adja vissza őket, és abban a pillanatban, hogy a page9.jpg-hez ér a page10.jpg mellett, egy sima sztringrendezés a 10. oldalt a 9. oldal elé teszi. A lista rendezését kifejezetten (explicit módon) végezze el a ciklus előtt, és részesítse előnyben a nullákkal kiegészített neveket a szkenneléskor, hogy a lexikális sorrend megegyezzen az oldal sorrendjével. Az oldalak sorrendje az az egyetlen dolog, amit egy ellenőr azonnal észrevesz, és a megelőzése a legolcsóbb hiba

Egy szkennelt kép elhelyezése és a képarány megtartása

Egy beszkennelt kép ritkán azonos alakú az oldallal. Ha megnyújtja, hogy kitöltse a lapot, a szöveg eltorzul; ha teljes pixelméretben helyezi el, akkor lelóg (overflows). A megoldás az, hogy a szélességhez vagy a magassághoz illeszkedő (width-fit vagy height-fit) két arány közül a kisebbikkel léptékezze, a fennmaradó részt pedig középre igazítsa. Mivel az origó a bal alsó sarokban van, a középre igazítás azt jelenti, hogy a megmaradó helyet egyenletesen elosztjuk, és hozzáadjuk az X-hez és az Y-hoz is

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;

Ez egyszer tölti be a fájlt, hogy leolvassa a pixelméreteit, kiszámít egyetlen egységes méretezést (scale), és átadja az elhelyezési téglalapot az AddImage-nek. Az AddImage közvetlenül fogad egy fájlelérési utat, és ugyanazon a kép-csővezetéken (image pipeline) vezeti át, mint az AddPicture, így bármilyen formátum, amit a VCL grafikus egységei felismernek, különösebb speciális esetek (special-casing) nélkül működik. Ha az előnézeti panelről a képet már dekódolva egy TPicture-ben tárolja, hívja az AddPicture(Pic, X, Y, DrawW, DrawH) függvényt ugyanazzal a téglalappal, és hagyja ki a második fájlolvasást

A dekódolás kihagyása JPEG képeknél

A szkennerek szinte mindig JPEG formátumot adnak ki. A JPEG TPicture-be történő betöltése dekódolja azt egy bittérképpé, majd a PDFium mentéskor újra kódolja, vagyis két veszteséges körutat (lossy round trips) tesz meg, amire önnek nincs szüksége. Az AddJpegImage az eredeti tömörített bájtokat egy folyamból (stream) egyenesen az oldalba ágyazza, ami gyorsabb és vizuálisan is tisztább egy nagy volumenű kötegelt műveletnél

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;

Az X, Y, DrawW és DrawH értékeket továbbra is ugyanúgy kell kiszámítania, mivel a méretezéshez szüksége van a pixelméretekre. Olvassa ki ezeket a fájlból vagy egy gyors fejléc-elemzésből (header parse), majd adja át a nyers folyamot (raw stream) az AddJpegImage-nek. PNG vagy TIFF szkennelések esetén az AddImage a helyes útvonal; a JPEG parancsikont (shortcut) tartsa fenn arra a formátumra, amelyre az valójában vonatkozik

Az egyes oldalak feliratozása

Az archivált szkennelt dokumentumokat könnyebb ellenőrizni, ha minden oldal tartalmazza a forrásfájl nevét. Az AddText egy sztringet rajzol ki a felhasználói térbeli (user-space) koordinátákhoz, így a felirat közvetlenül a kép alá kerül. Ne feledje a fordított (inverted) Y tengelyt: ahhoz, hogy a feliratot a szkennelt kép alá helyezze, a kép alsó éléből kell kivonnia, ahelyett, hogy hozzáadna

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

Még egy utolsó megjegyzés a mentéssel kapcsolatban. A SaveAs egy olyan függvény, amely logikai (Boolean) értéket ad vissza, így az éles (production) kódban inkább ellenőrizze az eredményét, ahelyett, hogy feltételezné az írás sikerességét; ellenkező esetben a teli lemez vagy a zárolt kimeneti útvonal csendben meghiúsul (fails quietly). Amikor a ciklus véget ér, és a fájl kiírásra kerül, pontosan azt kapja, amire az archívumnak szüksége volt: egy darab sorrendbe állított PDF-et minden egyes ügyirathoz (case file), az oldalak a mérethez igazítva, és bármilyen megjelenítőben olvashatóan

Ugyanezek az építőelemek lefedik a kapcsolódó munkákat is. Cserélje fel az oldalankénti méretezési szabályt, és egy fotókönyvet kap, oldalanként egy képpel; tartsa meg a ciklust, de olvasson egy többoldalas TIFF forrásból, és máris van egy fax-archívum konvertálója (fax-archive converter). Ha átfogóbb képet szeretne kapni a PDF-ek programozott építéséről, lásd: PDF-dokumentumok készítése az alapoktól a PDFium Component segítségével; ahhoz pedig, hogy az eredményt később visszarendelhesse a képernyőre, lásd: PDF oldalak konvertálása JPEG képekké a PDFium Component segítségével

A loslab.com-ról származó PDFium Component magában foglalja az ebben a sorozatban használt dokumentum-létrehozási, renderelési és szöveges API-kat