Tehnični članak

Ustvarjanje dokumentov PDF iz nič s PDFium Component 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 Component 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 Component 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

Diagram štiristopenjskega toka ustvarjanja PDF v PDFium Component za Delphi, od CreateDocument prek AddPage in klicev vsebine do SaveAs
CreateDocument začne prazen dokument v pomnilniku, vsak AddPage postane trenutna stran, SaveAs pa serializira sestavljene objekte PDF na disk
uses
  Vcl.Graphics,   // za clBlack in TColor
  PDFium;         // TPdf je tukaj

procedure CreateBlankPdf(const FileName: string);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                 // prazen dokument v pomnilniku
    Pdf.AddPage(0, 595, 842);           // A4 pokončno, v točkah
    Pdf.AddText('First page', 'Arial', 18, 50, 780);
    Pdf.SaveAs(FileName);               // serializiraj na 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

Diagram PDFium Component, ki protistavlja uporabniškemu prostoru PDF z izhodiščem spodaj levo in Y, ki raste navzgor, zaslonskim koordinatam, katerih Y raste navzdol od zgoraj levo
Naslov blizu vrha strani A4 potrebuje Y okoli 780 v uporabniškem prostoru PDF, zaslobske navade pa bi zapisale Y 60 in posadile besedilo blizu dna

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
  // Naslov v črni barvi, privzeta neprozornost, brez vrtenja
  Pdf.AddText(Title, 'Arial', 20, 50, 780);
  // Svetlejši podnapis avtorja 24 točk pod njim
  Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
  // Bled diagonalen žig osnutka čez stran
  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 Component 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č

Diagram življenjskega cikla vektorske poti v PDFium Component, kjer CreatePath nastavi začetno točko in slog, LineTo in BezierTo izrisujeta obris, AddPath pa predaja risbo
CreatePath pripne začetno točko in vsak slog vnaprej, a se na strani ne pokaže nič, dokler AddPath ne potrdi končane poti
procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
  // Tanka vodoravna črta. Preobremenitev s pravokotnikom nastavi škatlo neposredno:
  // X, Y, Width, Height, nato način polnjenja in barve.
  Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
    True, clBlack, $FF, 1.0);
  Pdf.AddPath;
end;

procedure DrawTriangle(Pdf: TPdf);
begin
  // Preobremenitev s točko: začni pri prvem vozlišču, črta do preostalih, zapri.
  Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
  Pdf.LineTo(300, 300);
  Pdf.LineTo(250, 400);
  Pdf.ClosePath;
  Pdf.AddPath;          // nič se ne izriše, dokler se to ne izvede
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);  // odmiki stolpcev
  RowH = 20;
var
  Y: Double;
  Row: Integer;
begin
  // Vrstica glave
  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);

  // Črta pod glavo
  Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
  Pdf.AddPath;

  // Podatkovne vrstice, Y se pri vsaki iteraciji premakne navzdol
  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);     // dodaj; nova stran postane trenutna
      Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
        'Arial', 10, 50, 30);       // noga blizu spodnjega roba
      // ... tukaj izriši telo te strani ...
    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 Component 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 Component za Delphi, ki to avtorsko pot združuje s funkcijami izrisovanja in ekstrakcije besedila, po katerih je PDFium sicer bolj znan