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

Egy Delphi köteges folyamat a PDFium Component AddPage, PageNumber, AddImage és SaveAs hívásait használja, hogy számozott szkennelések mappáját egyetlen rendezett PDFfé alakítsa
Minden szkennelés egyetlen oldallá válik, amelyet az AddPage hoz létre, a PageNumber célz, mielőtt az AddImage megrajzolná; a SaveAs egyszer írja meg a kész dokumentumot

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 szélesség pontokban
  PageH = 842.0;   // A4 magasság pontokban
  Margin = 36.0;   // fél hüvelykes keret minden szkennelés körül
var
  I: Integer;
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                       // új, üres, már aktív
    for I := 0 to Files.Count - 1 do
    begin
      Pdf.AddPage(I + 1, PageW, PageH);       // 1-alapú oldalindex
      Pdf.PageNumber := I + 1;                // tegye az új oldalt aktuálissá
      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

Egy A4 oldal diagramja megmutatja, hogyan helyezi el a PDFium Component AddImage egy skálázott szkennelést a margókon belül Delphi kóddal és bal alsó origóval
Az AddImage az elhelyezési téglalap bal alsó sarkát veszi, ezért az illesztés és a középre igazítás oldalpontokban számolódik az origótól
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 stb. a VCL grafikus egységein keresztül

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

    // Férjen bele a margókba a szkennelés torzítása nélkül.
    Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
    DrawW := Pic.Width * Scale;
    DrawH := Pic.Height * Scale;

    // Középre: a maradék hely egyenlően oszlik el. Az Y az oldal aljától mérve.
    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

Az AddJpegImage beágyazza az eredeti JPEG bájtokat egy PDFium Component oldalba, míg az AddImage dekódol, a SaveAs pedig újrakódolja a pixeleket Delphi-ben
Az AddJpegImage a szkenner tömörített bájtokat változatlanul ágyazza be, elkerülve azokat a dekódolási és újrakódolási meneteket, amelyeket az AddImage és a SaveAs végez
var
  Stream: TFileStream;
begin
  // ... az AddPage + PageNumber után az aktuális oldalhoz ...
  Stream := TFileStream.Create(FileName, fmOpenRead);
  try
    // A JPEG bájtokat önmagában ágyazza be; nincs dekódolás/újrakódolás ciklus.
    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

// Felirat a szkennelés alatt: az Y az oldal alja felé csökken.
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