Technical Article

Ustvarjanje dokumentov PDF iz nič s PDFium VCL v Delphiju

PDFium ima sloves motorja za prikazovanje dokumentov (viewer engine), izrisovalnika za zavihkom PDF v brskalniku Chrome, zato je prva stvar, ki jo je treba razjasniti, ta, da lahko PDFium VCL zgradi tudi dokument, ki prej sploh ni obstajal. Avtorska stran ovija PDFium-ov API za objekte strani (page-object API): ustvarite prazen dokument, dodate strani z eksplicitnimi dimenzijami in na vsako stran postavite besedilo, vektorske poti ter slike na koordinate, ki jih sami izberete. Ni se vam treba učiti nobenega jezika za opisovanje strani (page description language), prav tako v procesu ni gonilnikov tiskalnika. Kličete metode, knjižnica sestavlja objekte PDF, metoda SaveAs pa rezultat serializira na disk.

Tisto, česar pa ne dobite, je motor za postavitev (layout engine). To je dovolj pomembno, da povemo vnaprej, saj vpliva na vsak spodnji primer. PDFium VCL postavi vsebino tja, kamor mu naročite, v absolutnih koordinatah in nikamor drugam. Ne bo samodejno prelomil odstavka, prelil besedila čez prelom strani ali izračunal tabele iz vrstic in stolpcev. To je vaša naloga. Če ste prišli s pričakovanjem nečesa, kar preliva besedilo kot urejevalnik besedil, se raje takoj prilagodite: to je natančen, nizkonivojski API za umeščanje, ki je bližje risanju na platno kot stavljenju dokumenta. Za generirane račune, certifikate, nalepke in strani poročil, kjer že vnaprej veste, kam spada vsak element, je ta natančnost natanko to, kar želite.

Minimum za ustvarjanje datoteke

Trije klici ločujejo prazen TPdf in shranjen PDF: ustvarjanje dokumenta, dodajanje strani in zapis na disk. Vse ostalo je vsebina, ki jo dodajate vmes.

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;

Ena podrobnost lahko zmede tiste, ki so videli starejše primere kode: po klicu CreateDocument ne nastavite Pdf.Active := True. Lastnost Active sporoča, ali ročaj (handle) dokumenta obstaja, CreateDocument pa ga je že ustvaril, zato je ta lastnost True takoj, ko se klic zaključi. Ponovna nastavitev je v najboljšem primeru brez učinka, v najslabšem pa zavajajoča za naslednjega bralca. Lastnost Active opravi svoje delo ob zaključku: nastavitev na False sprosti dokument v ozadju pred klicem Free, kaj predstavlja pravilen vrstni red sproščanja pomnilnika. Ustvarjanje dokumenta (CreateDocument) in odpiranje datoteke obravnavajte kot medsebojno izključujoči se opravili. Knjižnica zavrne ustvarjanje novega dokumenta na objektu TPdf, ki že ima odprtega drugega, zato ponovna uporaba pomeni, da morate najprej zapreti trenutni dokument.

Koordinate se začnejo v spodnjem levem kotu

Drugi par argumentov pri metodi AddText in pri vsakem klicu za umeščanje predstavlja točko v uporabniškem prostoru PDF. Izhodišče se nahaja v spodnjem levem kotu strani, os X poteka desno, os Y pa poteka navzgor. Ena enota ustreza eni točki (1/72 palca), zato stran formata A4 meri 595 × 842 enot, format US Letter pa 612 × 792 enot. Ta navzgor usmerjena os Y je najpogostejši vir zmede v stilu »moje besedilo je izven strani«, saj zaslonske in bitne koordinate postavijo izhodišče na vrh, os Y pa raste navzdol. Na strani, visoki 842 točk, se naslov pri vrhu nahaja pri Y 780 in ne pri Y 60. Ko vsebina pristane na nepričakovanem mestu, je višina strani minus vaš Y skoraj vedno številka, ki ste jo dejansko imeli v mislih.

Metoda AddPage sprejme položaj vstavljanja kot svoj prvi argument, ki se začne z 1, pri čemer 0 predstavlja priročno oznako za »začetek dokumenta«. Posredujte 0 or 1 za prvo stran in stran bo vstavljena na začetek; posredujte vrednost, ki ustreza trenutnemu številu strani, da jo dodate na konec. Na novo dodana stran postane tudi trenutna stran, ki jo ciljajo naslednji klici za risanje, zato po dodajanju ni potreben ločen korak za izbiro strani. Če dodate več strani in želite pozneje risati na eno izmed prejšnjih, nastavite PageNumber za premik kazalca; medtem ko strani zapolnjujete v vrstnem redu ustvarjanja, pa to lastnost lahko pustite pri miru.

Pisanje besedila in pravilo glede pisav, ki tiho povzroči napako

Podpis metode AddText vsebuje vse, kar posamezen izpis potrebuje: niz znakov, ime pisave, velikost v točkah, sidro X in Y, neobvezno barvo, bajt alfa za prosojnost in kot vrtenja v stopinjah.

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;

Bajt alfa obsega vrednosti od $00 (nevidno) do $FF (neprozorno), kaj ustvari vodni žig namesto polnega bloka: $30 predstavlja približno 19-odstotno neprozornost, skozi katero je mogoče brati. Kot zavrti izpis v nasprotni smeri urinega kazalca okoli sidra, zato 45 stopinj ustvari klasičen napis od kota do kota. Za to ne potrebujete posebne funkcije za vodne žige. Vodni žig je le velik, polprosojen, rotiran klic AddText, izris pred ali po telesu dokumenta pa določa, ali se nahaja pod vsebino ali nad njo.

Pisave si zaslužijo posebno pozornost, saj je način napake tih. Ko posredujete ime pisave, PDFium VCL od operacijskega sistema zahteva podatke TrueType te pisave in jih vgradi v dokument, zato se datoteka, zgrajena na vašem računalniku, izriše enako na računalniku, ki te pisave nima nameščene. Težava pa nastane, ko se ime ne razreši: zaradi tipkarske napake ali ker pisave preprosto ni na računalniku, kjer se dokument gradi. Izjema se ne sproži. Knjižnica namesto tega ustvari objekt besedila, ki nosi ime le kot oznako brez vgrajenih podatkov, ter prepusti bralniku, da uporabi tisto pisavo, ki se mu zdi podobna. Besedilo se prikaže v vaših preizkusih, deluje pravilno, vendar pa spremeni metriko ali glife takoj, ko datoteko odprete na sistemu z drugačnimi pisavami. Uporabljajte imena, za katera veste, da so prisotna na ciljnem sistemu, seznam pisav obravnavajte kot odvisnost pri namestitvi in odprite vzorec v bralniku na čistem sistemu, preden zaupate izhodu.

Vektorske oblike: gradnja poti in njena potrditev

Črte, pravokotniki in zapolnjena območja se rišejo prek poti. Pot odprete z metodo CreatePath, ki hkrati določi začetno točko in celoten slog: način polnjenja, barvo polnila in črte z lastnimi bajti alfa, debelino črte, zaključke in spoje črt. Nato jo razširite z metodami LineTo, BezierTo in ClosePath, na koncu pa metoda AddPath potrdi končano pot na stran. Ta korak potrditve je enostavno pozabiti, če pa ga izpustite, se ne izriše nič.

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;

Dve preobremenitvi pokrivata najpogostejše primere. Oblika s štirimi koordinatami sprejme X, Y, širino in višino ter ustvari poravnan pravokotnik v enem klicu, kar uporabite za risanje črt, robov celic ali zapolnjenih ozadij. Oblika z dvema koordinatama nastavi le začetno točko, preostali del obrisa pa izrišete sami z LineTo in BezierTo. Način polnjenja določa, kako se barvajo prekrivajoča se območja: fmWinding ustreza večini polnih oblik, fmAlternate upravlja izreze in samosekajoče se obrise, fmNone pa pusti le obrobo brez polnila, kar uporablja zgornja ločilna črta.

Tabele so poti in besedilo, sestavljeni ročno

Ker ni osnovnega elementa za tabele, tabelo zgradite z zanko. Določite odmike X za stolpce in višino vrstic, vsako celico izpišete z AddText, ločilne črte pa narišete s pravokotnimi potmi. Izračuni so vaši, vendar so preprosti, ko pa so enkrat napisani, jih je mogoče posplošiti na katero koli mrežo.

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;

Upoštevajte, da se Y v vsakem koraku zmanjšuje za višino vrstice, saj je smer navzgor pozitivna. Tukaj se pokaže tudi odsotnost merjenja besedila: nič ne preprečuje, da bi se dolga imena elementov prelila v naslednji stolpec, saj knjižnica ne ve, kako širok je vaš izrisani niz. Za izhode s fiksnim formatom, kjer sami nadzorujete podatke, stolpce dimenzionirate dovolj široko in nadaljujete. Za resnično spremenljivo vsebino pa morate bodisi omejiti vnose bodisi sami izmeriti širino glifov pred postavitvijo, kar je točka, ko namenske knjižnice za kompozicijo postanejo smiselne.

Slike in več strani

Rastrska vsebina se uvozi prek pomočnikov za slike. Metoda AddPicture sprejme naložen TPicture in ga postavi na točko z neobvezno širino in višino za skaliranje; AddImage neposredno sprejme pot do datoteke ali TBitmap, AddJpegImage pa prenaša bajte JPEG brez vmesnih pretvorb v bitno sliko. Kot pri vsem ostalem so koordinate postavitve spodnji levi kot slike v uporabniškem prostoru, širina in višina pa predstavljata velikost na strani v točkah in ne dimenzij slikovnih pik vira.

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;

Večstranski dokument je le vzorec ene strani v zanki. Vsak klic AddPage doda stran na konec in jo nastavi kot trenutno, zato se telo in noga, ki ju izriše zatem, pojavita na pravkar dodani strani. V tej zanki vam ni treba znova določati PageNumber, saj je dodajanje strani kazalec že premaknilo tja; PageNumber potrebujete le, ko se vrnete na stran izven vrstnega reda ustvarjanja. Kličite SaveAs enkrat na koncu, ko so zapolnjene vse strani. Če potrebujete arhivski profil namesto navadne datoteke, isti objekt dokumenta ponuja SaveAsPdfA in druge različice skladnosti, zato je izbira izhodnega standarda le drugačen shranjevalni klic in ne drugačen način gradnje.

Kje se ta rešitev uporablja

Iskrena opredelitev je, da je ustvarjalni API za PDFium VCL tanek in zanesljiv sloj nad PDFium-ovim modelom objektov strani: omogoča dejansko ustvarjanje dokumentov, vgrajevanje pisav, delo z vektorsko in rastrsko vsebino ter shranjevanje v standardne datoteke. Ni pa in se ne pretvarja, da je motor za samodejno prelivanje dokumentov. Razmejitvena linija je postavitev besedila. Če so vaši izhodi predloge, računi, certifikati, nalepke ali nadzorne plošče na fiksni mreži, je model z absolutnimi koordinatami neposreden, hiter, koda pa ostane čitljiva. Če pa je vaš izhod dolga proza, ki se mora samodejno prelivati in razporejati na strani, bi na vrhu teh klicih gradili lasten motor za postavitev, kar pa je napačno orodje za to delo. Odločitev je predvsem v tem, na kateri strani te linije se nahajate.

Metode ustvarjanja, opisane tukaj, so del komponente PDFium VCL Component za Delphi, ki to avtorsko pot združuje s funkcijami izrisovanja in ekstrakcije besedila, po katerih je PDFium sicer bolj znan.