Technical Article

Delphi PDF izvješća s HotPDF-om: TextOut, fontovi i slike

Generiranje izvješća svodi se na postavljanje tri stvari na stranicu i njihovo usklađivanje oko položaja: tekst na poznatim koordinatama, fontovi koji se na poslužitelju prikazuju isto kao i na vašem računalu te slike prilagođene veličine. Sve ostalo što knjižnica za izvješća radi organizirano je oko te tri stavke. HotPDF, losLabova knjižnica za generiranje PDF-a za Delphi i C++Builder, daje vam svaku od njih kao izravan poziv na objektu stranice, a jedina stvarna prepreka je koordinatni sustav u pozadini, koji radi suprotno od VCL platna (canvas) na koje ste navikli. Najprije riješite tu orijentaciju i ostatak rasporeda prestat će vam stvarati probleme.

Postavljanje teksta i donje lijevo ishodište

Gotovo svačije prvo izvješće ispadne naopako. Naslov završi blizu donjeg ruba, a svaki redak ispod njega penje se prema vrhu. Ništa ne radi pogrešno. Korisnički prostor PDF-a (PDF user space), definiran u standardu ISO 32000-1 §8.3, postavlja ishodište u donji lijevi kut s osi Y koja raste prema gore, što je zrcalna slika GDI platna gdje Y raste prema dolje od gornjeg lijevog kuta. Pet minuta provedenih u prihvaćanju ove činjenice spašava vas od kasnijeg prepisivanja cijelog rasporeda kada brojevi prestanu imati smisla.

Središnji poziv objekta stranice je TextOut(X, Y, Angle, Text). X i Y određuju položaj teksta u točkama od donjeg lijevog kuta, a Angle ga rotira u stupnjevima, što je način na koji se crta dijagonalni pečat nacrta (DRAFT) ili kopije (COPY) bez ikakve posebne podrške. Trik koji omogućuje da intuicija izgrađena na VCL-u nastavi raditi jest izražavanje Y kao visine stranice umanjene za udaljenost koju želite od vrha:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice-0001.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(50, 792 - 50, 0, 'INVOICE');       // 50pt from top of Letter
    Pdf.CurrentPage.SetFont('Arial', [], 10);
    Pdf.CurrentPage.TextOut(50, 792 - 70, 0, 'Date: 2026-06-11');
    Pdf.CurrentPage.TextOut(300, 400, 45, 'COPY');              // rotated stamp
    Pdf.AddPage;                                                // CurrentPage now points here
    Pdf.CurrentPage.SetFont('Arial', [], 10);                   // font state does not carry over
    Pdf.CurrentPage.TextOut(50, 742, 0, 'Page 2 detail rows');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Dva stanja ponašanja u tom kodu odgovorna su za većinu bugova koji se pojavljuju tek na drugoj stranici. AddPage ponovno usmjerava CurrentPage na stranicu koju je upravo stvorio, pa referenca stranice koju ste ranije spremili u predmemoriju više ne crta tamo gdje očekujete. Odabir fonta također je po stranici, a ne po dokumentu. Ako preskočite SetFont nakon AddPage, prvi TextOut na novoj stranici vraća se na zadanu vrijednost s kojom je stranica započela, a ne na podebljani font naslova koji ste postavili prije tri stranice. Sigurna navika je tretirati \"početak nove stranice\" i \"ponovno uspostavljanje stanja teksta\" kao jedan neodvojiv korak u petlji izvješća.

Fontovi koji postoje na poslužitelju, a ne samo na vašem računalu

Većina problema s fontovima zapravo su problemi s implementacijom (deployment) pod krinkom. Vaše razvojno računalo ima instaliran korporativni font, pa izvješće na vašem zaslonu izgleda ispravno i biva isporučeno. Produkcijski poslužitelj pokreće zadatak pod servisnim računom koji nikada nije imao instaliran taj font, renderer tiho zamjenjuje font nečim što može pronaći, a prva stvar koju čujete je kupac koji pita zašto se zaglavlje promijenilo. Rješenje je prestati vjerovati OS-ovom direktoriju fontova i učitati font iz datoteke koju vaš instalacijski program postavlja na disk. HotPDF-ov poziv za registraciju Unicode fonta prima putanju i radi točno to:

Pdf.RegisterUnicodeTTF('C:\ProgramData\MyApp\Fonts\NotoSans.ttf');
Pdf.CurrentPage.SetFont('NotoSans', [], 12);
Pdf.CurrentPage.TextOut(50, 700, 0, WideString('Łódź - Unicode test ✓'));

TextOut izravno prihvaća WideString, što je važnije nego što se na prvi pogled čini. Ime kupca s naglaskom, njemačka ulica, poljski grad: to nisu iznimni slučajevi, to je uobičajeni sadržaj tablice kupaca i oni prolaze kroz isti poziv kao i ASCII oznake koje ste hardkodirali, pod uvjetom da registrirani font doista sadrži te znakove (glyphe). Jedno ograničenje verzije dolazi s ugrađenim fontovima: dokument mora biti u verziji PDF 1.5 ili novijoj, pa ako vas neki drugi zahtjev veže za stariju verziju, to je stvar koja će tiho zakazati. Pismo s desna na lijevo, poput arapskog i hebrejskog, zahtijeva stvarno oblikovanje (shaping) umjesto jednostavnog traženja znakova, a to ima vlastiti cjevovod; pogledajte naš članak o oblikovanju teksta složenih pisama s HotPDF-om.

Kada niti jedan instalirani font ne može izraziti ono što trebate, primjerice MICR znakove na čeku ili vlastiti skup simbola, fontovi Type 3 rješavaju problem. Definirate svaki znak kao mali tok sadržaja putem RegisterType3Font i AddType3Glyph. To je specijalizirani dio API-ja i rijetko ćete ga koristiti, ali je mnogo čišći od razbacivanja stotina malih slika simbola po stranici.

Slike: srednji argumenti su širina i visina, a ne kut

Rukovanje slikama dijeli se na dva koraka, a njihovo držanje odvojenim cijeli je smisao. AddImage prima TBitmap ili TJPEGImage, ugrađuje ih jednom i vraća indeks. Slike u PNG formatu moraju se dekodirati u bitmapu prije toga. ShowImage zatim iscrtava taj indeks gdje god i koliko god često želite. Redoslijed argumenata u ShowImage je jedno mjesto na kojem vrijedi zastati i pažljivo pročitati:

var
  Png: TPngImage;
  Logo: TBitmap;
  LogoIdx: Integer;
begin
  Png := TPngImage.Create;
  Logo := TBitmap.Create;
  try
    Png.LoadFromFile('brand-logo.png');
    Logo.Assign(Png);                       // decode PNG to a bitmap
    LogoIdx := Pdf.AddImage(Logo, icFlate); // lossless for flat-color art
  finally
    Logo.Free;
    Png.Free;
  end;
  // (Index, X, Y, Width, Height, Angle): not (X1, Y1, X2, Y2)
  Pdf.CurrentPage.ShowImage(LogoIdx, 50, 700, 120, 40, 0);
end;

Dva broja nakon pozicije su širina i visina. To nisu koordinate suprotnog kuta, a zadnji argument je kut rotacije u stupnjevima. Ako pročitate potpis kao okvir X1/Y1/X2/Y2, logotip dimenzija 120x40 postavljen na (50, 700) protegnut će se od tamo do (120, 40), rastežući se preko većeg dijela stranice. Izlaz čini pogrešku očitom dok izvorni kod izgleda sasvim razumno, zbog čega se na tome može izgubiti cijelo popodne. Svojstvo KeepImageAspectRatio prema zadanim je postavkama True, pa okvir s pogrešnim omjerima ostavlja crne rubove (letterbox) umjesto izobličenja slike; postavite ga na False samo kada je doista želite rastegnuti.

Razdvajanje između registracije i postavljanja isplati se na dugim stazama. Budući da AddImage ugrađuje piksele jednom, a svaki ShowImage s tim indeksom upućuje natrag na isti ugrađeni objekt, mjesto gdje pozivate AddImage odreduje veličinu datoteke. Pozovite ga unutar petlje stranice za izvješće od 500 stranica i isti logotip će se ugraditi 500 puta. Pozovite ga jednom prije petlje, zadržite indeks i logotip će se pohraniti samo jednom. Mali rječnik s ključem putanje resursa dovoljan je da osigura da se svaka zasebna slika registrira točno jednom.

Odabir kodeka je drugi način utjecaja na veličinu. Fotografski sadržaj, skenirani prilozi i slično, pripadaju JPEG-u: proslijedite icJpeg u AddImage i smanjite JpegQuality na oko 85 jer to svojstvo počinje od 100, a razlika na 85 je nevidljiva na tiskanoj stranici. Jednostavne grafike poput logotipa, grafikona i crteža pripadaju formatu icFlate, gdje je kompresija bez gubitaka već kompaktna, a JPEG bi stvorio vidljive smetnje (ringing) oko oštrih rubova. Generiranje izvješća koje postavlja po jednu fotografiju pune kvalitete na svaku stranicu može narasti na gigabajte; isti sadržaj pri kvaliteti JPEG 85 iznosi oko desetinu te veličine, a čitatelj ne može uočiti razliku.

Linije, okviri i sjenčanje s osnovnim vektorskim elementima

Vodoravna linija ispod zaglavlja tablice i sivi okvir iza zbroja ne moraju biti slike. Nacrtajte ih kao vektore i oni će ostati oštri pri svakom zumiranju, ispisivat će se precizno i dodati gotovo ništa veličini datoteke. HotPDF slijedi isti model koji koriste sirovi tokovi PDF sadržaja: izgradite putanju, a zatim pozovite operator koji je iscrtava.

// Horizontal rule under the table header
Pdf.CurrentPage.SetLineWidth(0.75);
Pdf.CurrentPage.MoveTo(50, 660);
Pdf.CurrentPage.LineTo(545, 660);
Pdf.CurrentPage.Stroke;

// Shaded totals box: X, Y, width, height
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 120, 150, 40);
Pdf.CurrentPage.Fill;

Redoslijed nije neobvezan: postavite stanje bojanja, izgradite putanju, a zatim pozovite Stroke ili Fill. Putanja koju izgradite, ali je nikada ne obojite, ne pridonosi ničemu na stranici, što je gotovo uvijek odgovor kada se linija \"ne pojavljuje\". SetRGBFillColor prima jednu TColor vrijednost, pa se poznate VCL konstante poput clNavy i clBlack mogu izravno koristiti, a Rectangle koristi iste argumente širine i visine kao i postavljanje slike, umjesto dvaju kutova. Oprez kod tankih linija: sve ispod otprilike pola točke može izgledati elegantno na monitoru, a zatim nestati na uredskom pisaču od 600 dpi, pa je 0,75pt razuman minimum za svaku liniju koja mora preživjeti ispis.

Paginacija na stvarnim podacima, a ne na uzorcima

Jedan detalj koji treba ispravno postaviti prije definiranja rasporeda: stupci s brojevima trebali bi biti desno poravnati, a način da to učinite jest izmjeriti širinu renderirane vrijednosti i pozicionirati je unatrag od granice stupca, a ne dopunjavati tekstualni niz razmacima na početku. Nadopunjavanje razmacima poravnava se samo u fontovima fiksne širine (monospace), a nitko ne piše financijska izvješća u takvim fontovima. Prvo provucite vrijednosti kroz Delphi-jeve regionalno osjetljive rutine poput FormatFloat, tako da separator tisućica čiju širinu mjerite bude isti onaj koji će se doista prikazati u korisnikovim regionalnim postavkama.

Opasnost kod paginacije je u tome što je napišete za demo skup podataka, gdje deset kratkih redaka stane na jednu stranicu i petlja se nikada ne mora prekinuti. Produkcija vam donosi kupca čiji naziv tvrtke ima 140 znakova i izvadak s 4000 stavki, pa se sada petlja mora svaki put ispravno prekinuti. Predložak koji funkcionira je jedan Y kursor koji se pomiče prema dolje kako oduzimate visinu svakog retka, te provjera koja započinje novu stranicu u trenutku kada bi kursor prešao donju marginu. \"Prema dolje\" ovdje znači smanjenje Y vrijednosti, što je jedino mjesto gdje donje lijevo ishodište ostaje zbunjujuće. Držite sve to u jednoj rutini koja također ponovno postavlja SetFont i ponovno crta zaglavlje na novoj stranici, pa se bugovi s pogrešnim brojem stranica nikada neće ukorijeniti. Kada ista izvješća moraju zadovoljiti i arhivska pravila ili pravila pristupačnosti, odluke koje donesete upravo ovdje - koje fontove ugrađujete, je li izlaz označen, koje prostore boja koristite - upravo su one koje ti standardi kontroliraju; vodič za HotPDF PDF/A, PDF/X i PDF/UA vrijedi pročitati prije nego što se predložak definira.

Svaki ovdje prikazani poziv, pozicioniranje teksta, registracija fonta, ugradnja slike i crtanje putanje, dolaze u komponenti HotPDF Component za Delphi i C++Builder, čija referenca dokumentira cjeloviti izlazni API uz značajke obrazaca, šifriranja i potpisivanja.