Technical Article

Kreiranje PDF dokumenata od nule pomoću PDFium VCL-a u Delphi-ju

PDFium ima reputaciju mehanizma za prikaz (viewer engine), odnosno renderera iza Chrome-ove PDF kartice, tako da je prva stvar koju treba razjasniti to da PDFium VCL takođe može da izgradi dokument koji nikada pre nije postojao. Strana za kreiranje dokumenata obavija PDFium-ov API za objekte stranice (page-object API): pravite prazan dokument, dodajete stranice sa eksplicitnim dimenzijama i postavljate tekst, vektorske putanje i slike na svaku stranicu na koordinatama koje sami odaberete. Nema jezika za opis stranice koji morate da učite niti drajvera za štampu u ovom procesu. Pozivate metode, biblioteka sklapa PDF objekte, a SaveAs serijalizuje rezultat.

Ono što ne dobijate jeste mehanizam za raspoređivanje (layout engine). Ovo je dovoljno važno da se kaže na samom početku, jer to oblikuje svaki primer u nastavku. PDFium VCL postavlja sadržaj tamo gde mu kažete, u apsolutnim koordinatama, i nigde drugde. On neće prelomiti pasus, preneti tekst preko preloma stranice ili izračunati tabelu iz redova i kolona. Ti zadaci su na vama. Ako ste stigli očekujući nešto što prelama prozu na način na koji to radi procesor teksta, prilagodite svoja očekivanja sada: ovo je precizan API za pozicioniranje na niskom nivou, bliži crtanju na platnu (canvas) nego slaganju teksta u dokumentu. Za generisane fakture, sertifikate, nalepnice i stranice izveštaja gde već znate gde svaki element pripada, ta preciznost je upravo ono što želite.

Minimum potreban za generisanje datoteke

Tri poziva stoje između praznog TPdf objekta i sačuvanog PDF-a: kreiranje dokumenta, dodavanje stranice i upisivanje na disk. Sve ostalo je sadržaj koji slažete između njih.

uses
  Vcl.Graphics,   // for clBlack and TColor
  PDFium;         // TPdf lives here

procedure CreateBlankPdf(const FileName: string);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                 // empty in-memory document
    Pdf.AddPage(0, 595, 842);           // A4 portrait, in points
    Pdf.AddText('First page', 'Arial', 18, 50, 780);
    Pdf.SaveAs(FileName);               // serialize to disk
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Jedan detalj može zbuniti ljude koji su videli starije isečke koda: ne dodeljujete Pdf.Active := True nakon CreateDocument. Svojstvo Active prijavljuje da li postoji rukovalac (handle) dokumenta, a CreateDocument ga je već kreirao, tako da je to svojstvo True onog trenutka kada se taj poziv vrati. Ponovno postavljanje je u najboljem slučaju bez efekta, a u najgorem zbunjujuće za sledećeg čitaoca koda. Active opravdava svoje postojanje na izlazu: dodeljivanje vrednosti False oslobađa dokument u pozadini pre poziva Free, što predstavlja čist redosled uništavanja objekata. Tretirajte CreateDocument i otvaranje učitavanjem datoteke kao međusobno isključive operacije. Biblioteka odbija da kreira novi dokument na TPdf objektu koji već ima otvoren dokument, tako da ponovna upotreba znači najpre zatvaranje trenutnog dokumenta.

Koordinate počinju u donjem levom uglu

Drugi par argumenata za AddText, kao i za svaki poziv za pozicioniranje, jeste tačka u PDF korisničkom prostoru. Koordinatni početak se nalazi u donjem levom uglu stranice, X ide desno, a Y ide nagore. Jedna jedinica je jedna tačka (point), odnosno 1/72 inča, tako da stranica formata A4 ima dimenzije 595 sa 842 jedinice, a US Letter 612 sa 792. To Y usmereno nagore je najčešći izvor zabune tipa "moj tekst je van stranice", jer koordinate ekrana i bitmape postavljaju početak na vrhu sa Y koji raste nadole. Na stranici visine 842 tačke, zaglavlje blizu vrha se nalazi oko Y 780, a ne Y 60. Kada element završi na neočekivanom mestu, visina stranice minus vaš Y je skoro uvek broj koji ste zapravo želeli.

AddPage uzima poziciju umetanja kao svoj prvi argument, izražen indeksiranjem od 1, pri čemu je 0 zgodna skraćenica za "početak dokumenta". Prosledite 0 ili 1 za prvu stranicu i stranica će biti umetnuta na početak; prosledite vrednost koja odgovara broju stranica koji dodajete da biste je dodali na kraj. Novo dodata stranica takođe postaje trenutna stranica, ona na koju ciljaju kasniji pozivi za crtanje, tako da nema posebne faze "izbora ove stranice" nakon njenog dodavanja. Ako dodate nekoliko stranica i kasnije morate da crtate na nekoj ranijoj, postavite PageNumber da biste pomerili kursor; dok stranice popunjavate redom kako ih kreirate, možete ih ostaviti na miru.

Pisanje teksta i pravilo o fontovima koje tiho pravi probleme

Potpis metode AddText nosi sve što je jednom ispisu potrebno: string, naziv fonta, veličinu u tačkama, X i Y sidro (anchor), a zatim opcione parametre za boju, bajt transparentnosti (alpha) i ugao rotacije u stepenima.

procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
  // Title in black, default opacity, no rotation
  Pdf.AddText(Title, 'Arial', 20, 50, 780);
  // A lighter byline 24 points below it
  Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
  // A faint diagonal draft stamp across the page
  Pdf.AddText('DRAFT', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;

Alfa bajt ide od $00 (nevidljivo) do $FF (neprozirno), što čini da pečat "DRAFT" bude vodeni žig, a ne čvrst blok: $30 je otprilike devetnaest procenata neprozirnosti, dovoljno da se čita kroz njega. Ugao rotira ispis suprotno od smera kazaljke na satu oko njegovog sidra, tako da 45 stepeni daje klasičan pečat od ugla do ugla. Ništa od ovoga ne zahteva posebnu funkciju vodenog žiga. Vodeni žig je samo veliki, poluprovidan, rotirani poziv AddText, a njegovo iscrtavanje pre ili posle tela dokumenta odlučuje da li stoji iza ili ispred sadržaja.

Fontovi zaslužuju pažljivu rečenicu, jer je način neuspeha tih. Kada prosledite naziv fonta, PDFium VCL traži od operativnog sistema TrueType podatke tog fonta i ugrađuje ih u dokument, zbog čega se datoteka napravljena na vašoj mašini renderuje identično na onoj koja nikada nije imala instaliran taj font. Zamka je ono što se dešava kada se naziv ne razreši: greška u kucanju ili font koji jednostavno nije prisutan na mašini gde se vrši build. Nema izuzetka. Biblioteka se vraća na kreiranje tekstualnog objekta koji nosi naziv samo kao oznaku, bez ugrađenih podataka, i ostavlja čitaču da zameni font onim što smatra bliskim. Tekst se pojavljuje u vašim testovima, izgleda uverljivo, a pomera metriku ili glifove onog trenutka kada se datoteka otvori negde sa drugačijim instaliranim fontovima. Koristite imena za koja znate da su prisutna na mašini koja generiše PDF, tretirajte listu fontova kao zavisnost pri implementaciji i otvorite uzorak u čitaču na čistom sistemu pre nego što verujete izlazu.

Vektorski oblici: izgradite putanju, a zatim je primenite

Linije, pravougaonici i popunjeni regioni idu kroz putanju (path). Otvarate je pomoću CreatePath, što postavlja početnu tačku i sve stilove odjednom: režim popunjavanja (fill mode), boje popune i linije sa sopstvenim alfa bajtovima, širinu linije, završetke i spojeve linija. Zatim je produžavate pomoću LineTo, BezierTo i ClosePath, i na kraju AddPath primenjuje (commits) završenu putanju na stranicu. Korak primene je lako zaboraviti, a ako ga preskočite, ništa neće biti nacrtano.

procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
  // A thin horizontal rule. The rectangle overload sets a box directly:
  // X, Y, Width, Height, then fill mode and colors.
  Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
    True, clBlack, $FF, 1.0);
  Pdf.AddPath;
end;

procedure DrawTriangle(Pdf: TPdf);
begin
  // Point overload: start at the first vertex, line to the rest, close.
  Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
  Pdf.LineTo(300, 300);
  Pdf.LineTo(250, 400);
  Pdf.ClosePath;
  Pdf.AddPath;          // nothing is drawn until this runs
end;

Dva preopterećenja (overloads) pokrivaju uobičajene slučajeve. Oblik sa četiri koordinate uzima X, Y, širinu i visinu i daje vam pravougaonik poravnat sa osama u jednom pozivu, što je ono što koristite za crtanje linije razdvajanja, okvira ćelije ili popunjenog pozadinskog panela. Oblik sa dve koordinate postavlja samo početnu tačku, a ostatak konture sami iscrtavate pomoću LineTo i BezierTo. Režim popunjavanja kontroliše kako se slikaju preklapajući regioni: fmWinding odgovara većini čvrstih oblika, fmAlternate upravlja izrezima i konturama koje se same presecaju, dok fmNone ostavlja samo iscrtanu putanju bez popune, što je ono što koristi gornja linija razdvajanja.

Tabele su putanje i tekst, sklopljeni ručno

Budući da ne postoji primitiv za tabelu, tabela se realizuje kroz petlju. Odlučujete o X ofsetima kolona i visini reda, upisujete svaku ćeliju pomoću AddText i crtate linije pomoću pravougaonih putanja. Aritmetika je vaša, ali je jednostavna, i jednom napisana se generalizuje na bilo koju mrežu koja vam je potrebna.

procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
  ColX: array[0..2] of Double = (0, 110, 210);  // column offsets
  RowH = 20;
var
  Y: Double;
  Row: Integer;
begin
  // Header row
  Pdf.AddText('Item', 'Arial', 10, Left + ColX[0], Top);
  Pdf.AddText('Qty', 'Arial', 10, Left + ColX[1], Top);
  Pdf.AddText('Price', 'Arial', 10, Left + ColX[2], Top);

  // Rule under the header
  Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
  Pdf.AddPath;

  // Data rows, stepping Y downward each iteration
  Y := Top;
  for Row := 1 to 3 do
  begin
    Y := Y - RowH;
    Pdf.AddText('Item ' + IntToStr(Row), 'Arial', 9, Left + ColX[0], Y);
    Pdf.AddText(IntToStr(Row * 2), 'Arial', 9, Left + ColX[1], Y);
    Pdf.AddText('$' + IntToStr(Row * 10) + '.00', 'Arial', 9, Left + ColX[2], Y);
  end;
end;

Obratite pažnju na Y koji se spušta nadole za visinu reda u svakom prolazu, ponovo zato što je smer nagore pozitivan. Ovo je takođe mesto gde se vidi odsustvo merenja teksta: ništa ne sprečava dugačko ime stavke da se prelije u sledeću kolonu, jer biblioteka ne zna koliku je širinu vaš string zauzeo pri renderovanju. Za izlaze fiksnog formata gde kontrolišete podatke, velikodušno dimenzionišete kolone i nastavljate dalje. Za zaista promenljiv sadržaj, ili ograničavate unose ili sami merite širine glifova pre nego što ih postavite, što je tačka u kojoj namenska biblioteka za slaganje teksta počinje da isplaćuje svoju investiciju.

Slike i više stranica

Rasterski sadržaj dolazi preko pomoćnih funkcija za slike. AddPicture uzima učitanu TPicture sliku i postavlja je u tačku, sa opcionom širinom i visinom za skaliranje; AddImage prihvata putanju datoteke ili direktno TBitmap, a AddJpegImage strimuje JPEG bajtove bez posredovanja bitmape. Kao i kod svega ostalog, koordinate pozicioniranja su donji levi ugao slike u korisničkom prostoru, a širina i visina su veličina na stranici izražena u tačkama (points), a ne dimenzije u pikselima izvora.

procedure CreateMultiPageReport(const FileName: string; PageCount: Integer);
var
  Pdf: TPdf;
  P: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;
    for P := 1 to PageCount do
    begin
      Pdf.AddPage(P, 595, 842);     // append; the new page becomes current
      Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
        'Arial', 10, 50, 30);       // footer near the bottom edge
      // ... draw this page's body here ...
    end;
    Pdf.SaveAs(FileName);
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Višestrani dokument je zapravo obrazac jedne stranice u petlji. Svaki poziv AddPage dodaje stranicu i postavlja je kao trenutnu, tako da telo i podnožje koje sledeće nacrtate sleću na stranicu koju ste upravo dodali. Ne menjate PageNumber unutar ove petlje, jer je dodavanje stranice već pomerilo kursor tamo; PageNumber vam je potreban samo kada se vraćate na stranicu van redosleda kreiranja. Pozovite SaveAs jednom na kraju, nakon što se popuni poslednja stranica. Ako vam je potreban arhivski profil, a ne obična datoteka, isti objekat dokumenta izlaže SaveAsPdfA i druge varijante usaglašenosti, tako da je izbor izlaznog standarda samo drugaakiji poziv za čuvanje, a ne drugačiji put kreiranja.

Gde se ovo uklapa

Iskrena formulacija je da je PDFium VCL API za kreiranje dokumenata veran, tanak sloj preko PDFium-ovog modela objekata stranice (page-object model): stvarno kreiranje dokumenata, stvarni ugrađeni fontovi, stvarni vektorski i rasterski sadržaj, serijalizovan u datoteku usklađenu sa standardima. To nije, niti se pretvara da jeste, mehanizam za dinamički prelom dokumenta (reflowing document engine). Granična linija je raspored teksta. Ako je vaš izlaz šablonizovan (fakture, sertifikati, nalepnice, kontrolne table renderovane u fiksnu mrežu), model apsolutnih koordinata je direktan i brz, a kod ostaje čitljiv. Ako je vaš izlaz tekst u slobodnoj formi koji mora samostalno da se prelama i deli na stranice, moraćete da izgradite sopstveni mehanizam rasporeda na vrhu ovih poziva, a to je onda pogrešan alat za taj posao. Znati na kojoj ste strani te linije je najveći deo odluke.

Metode kreiranja opisane ovde su deo PDFium VCL komponente za Delphi, koja uparuje ovaj put kreiranja sa funkcijama renderovanja i ekstrakcije teksta po kojima je PDFium poznatiji.