Tehnički članak

Spajanje skeniranih slika u jedan PDF pomoću PDFium Component-a u Delphiju

Tim za obradu potraživanja imao je trideset godina papirnatih spisa koji su prolazili kroz skener s uvlačenjem listova (sheet-fed scanner). Skener je izbacivao (spat out) jedan JPEG po stranici u mapu (folder), s imenima 0001.jpg, 0002.jpg, i tako dalje. Ono što je arhivi zapravo trebalo bio je jedan PDF po spisu predmeta (case file), sa stranicama po redu, tako da recenzent (reviewer) može otvoriti jedan dokument umjesto klika kroz stotinu sličica (thumbnails) slika. Taj zadnji korak, pretvaranje numerirane hrpe skeniranja (scans) u jedan uređeni PDF, posao je ovdje

PDFium Component se njime bavi izravno. Osim renderiranja i izdvajanja teksta, komponenta može izgraditi PDF od nule: stvoriti prazan dokument, dodati praznu stranicu veličine po želji, baciti sliku na tu stranicu u koordinatama korisničkog prostora (user-space coordinates), a zatim spremiti. Cijeli cjevovod (pipeline) živi na komponenti TPdf, tako da je paketni pretvarač (batch converter) petlja preko imena datoteka plus pregršt poziva

Oblik konverzije

Tri stvari se moraju dogoditi za svako skeniranje. Odlučujete o veličini stranice, postavljate sliku unutar stranice ostavljajući marginu (margin), te prelazite na sljedeću stranicu. PDFium Component daje vam po jednu metodu za svaku: AddPage stvara praznu stranicu zadane veličine, AddImage (ili AddPicture ako već držite TPicture) crta bitmapu u trenutnu stranicu, a PageNumber govori komponenti cilj kojih su stranica naknadni pozivi crtanja (subsequent draw calls target)

Jedan detalj koji sapliće (trips up) ljude je koordinatni sustav. PDF korisnički prostor stavlja ishodište (origin) u donji lijevi kut stranice, s Y koji se povećava prema gore, suprotno od koordinata zaslona za kojima Delphi programeri posežu po refleksu. X, Y koji prosljeđujete (pass) u AddImage je donji lijevi kut pravokutnika slike, a Width, Height (Širina, Visina) su veličina postavljanja (placement size) u točkama, a ne veličina piksela izvorne datoteke. Shvatite (get) to unatrag i vaše skeniranje slijeće (land) s (off) stranice ili naopako u odnosu na mjesto na kojem ste ih očekivali

Stvaranje dokumenta i stranice po skeniranju

Započnite s praznim dokumentom. CreateDocument dodjeljuje (allocates) svježi PDF i ostavlja komponentu aktivnom, tako da ne postoji poseban korak za otvaranje. Od tamo hodate (walk) po popisu skeniranih datoteka, a za svaku dodajete stranicu, činite je trenutnom (current) i postavljate sliku. Dimenzije stranice ovdje su A4 u točkama (595 × 842 portret), standardna veličina lista za arhiviranu korespondenciju

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;

Svaka iteracija stvara stranicu i odmah joj postavlja PageNumber. Ta druga linija je važna: AddPage ubacuje stranicu, ali metode crtanja (draw methods) djeluju na bilo kojoj stranici koja je trenutna, tako da je postavljanje PageNumber ono što cilja (aims) AddImage na stranicu koju ste upravo napravili. Preskočite (skip) to i vaše će se slike naslagati na bilo koju stranicu koja je slučajno (happened to) bila učitana prije

Jedna se pretpostavka skriva u toj petlji (loop): redoslijed Files. Skener imenuje stranice 0001.jpg do 0100.jpg, ali nabrajanje direktorija (directory enumeration) ne vraća ih uvijek razvrstane (sorted), a u trenutku kada pogodite page9.jpg pored page10.jpg, obično sortiranje stringova (string sort) stavlja stranicu 10 prije stranice 9. Eksplicitno sortirajte popis (list) prije petlje i radije koristite imena ispunjena nulom (zero-padded) u vrijeme skeniranja tako da leksički redoslijed odgovara redoslijedu stranica. Redoslijed stranica jedina je stvar koju recenzent (reviewer) odmah primijeti i najjeftinija je pogreška za spriječiti

Postavljanje skeniranja i zadržavanje njegovog omjera (aspect ratio)

Skeniranje je rijetko istog oblika kao stranica. Ako ga rastegnete (stretch) da ispuni list (sheet), izobličite tekst; ako ga postavite na punu veličinu piksela, on se prelijeva (overflows). Rješenje je skaliranje prema manjem od dva omjera, prilagođavanju po širini (width-fit) ili prilagođavanju po visini (height-fit), te centriranju onoga što je ostalo (left over). Budući da se ishodište (origin) nalazi na donjem lijevom dijelu, centriranje znači ravnomjerno podijeliti (splitting evenly) preostali prostor i dodati ga u X i 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, 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;

Ovo učitava datoteku jednom da bi pročitao njezine dimenzije piksela, izračunava (computes) jednu ujednačenu skalu i prosljeđuje (passes) pravokutnik za postavljanje u AddImage. AddImage izravno prihvaća putanju datoteke (file path) i usmjerava (routes) je kroz isti slikovni cjevovod (pipeline) kao i AddPicture, tako da bilo koji format koji prepoznaju VCL grafičke jedinice (graphics units) radi bez posebnog kućišta (special-casing). Ako već imate dekodiranu sliku u TPicture-u iz okna za pretpregled (preview pane), pozovite AddPicture(Pic, X, Y, DrawW, DrawH) s istim pravokutnikom i preskočite drugo čitanje datoteke

Preskakanje dekodiranja za JPEG skeniranja

Skeneri gotovo uvijek emitiraju JPEG. Učitavanje JPEG-a u TPicture dekodira (decodes) ga u bitmapu, a zatim ga PDFium ponovno kodira pri spremanju, dva povratna puta s gubitcima (lossy round trips) koja vam ne trebaju. AddJpegImage umeće izvorne komprimirane bajtove ravno u stranicu iz toka (stream), što je brže i vizualno čišće za veliku seriju (batch)

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;

Još uvijek izračunavate X, Y, DrawW i DrawH na isti način, budući da su vam potrebne dimenzije piksela za skaliranje. Pročitajte ih iz datoteke ili kroz brzo raščlanjivanje zaglavlja (header parse), a zatim predajte sirovi (raw) tok u AddJpegImage. Za PNG ili TIFF skeniranja, put AddImage je onaj pravi; rezervirajte JPEG prečac za format na koji se zapravo odnosi

Označavanje svake stranice

Arhivirana skeniranja lakše je provjeravati (audit) kada svaka stranica nosi svoje izvorno (source) ime datoteke. AddText crta niz na koordinati korisničkog prostora, tako da se natpis (caption) nalazi točno ispod slike. Sjetite se obrnute (inverted) Y osi: da biste stavili oznaku (label) ispod skeniranja, oduzimate (subtract) od donjeg ruba (bottom edge) slike umjesto da je dodajete na nju

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

Još jedna zadnja točka (point) o spremanju. SaveAs je funkcija koja vraća Boolean, pa u produkcijskom kodu (production code) provjerite njen rezultat radije nego da pretpostavite da je pisanje (write) uspjelo; inače puni disk ili zaključana (locked) izlazna (output) putanja ne uspijevaju (fails) potiho (quietly). Nakon što petlja završi i datoteka se zapiše, imate točno ono što je arhiva (archive) trebala: jedan poredani PDF po dosjeu (case file), stranice u omjerima za prilagođavanje (scaled to fit), spremno za čitanje u bilo kojem pregledniku (viewer)

Isti građevni blokovi (building blocks) pokrivaju (cover) povezane poslove. Zamijenite (swap) pravilo određivanja veličine po stranici i dobit ćete knjigu fotografija (photo book) s jednom slikom po listu (sheet); zadržite petlju, ali čitajte iz višestraničnog TIFF izvora i imate pretvarač faks-arhive (fax-archive converter). Ako želite širu sliku izrade PDF-ova programski, pogledajte stvaranje PDF dokumenata od nule s PDFium Component-om; da biste rezultat kasnije renderirali natrag na zaslon, pogledajte pretvaranje PDF stranica u JPEG slike s PDFium Component-om

PDFium Component s loslab.com objedinjuje (bundles) kreiranje dokumenta, renderiranje i API-je za tekst koji se koriste u cijeloj ovoj seriji