Tehnički članak

Delphi PDF izveštaji sa HotPDF-om: TextOut, fontovi i slike

Generisanje izveštaja se svodi na postavljanje tri stvari na stranicu i njihovo usklađivanje: tekst na poznatim koordinatama, fontovi koji se na serveru renderuju isto kao i na vašem desktopu, i slike prilagođene veličini. Sve ostalo što biblioteka za izveštaje radi organizovano je oko ove tri stavke. HotPDF, losLab-ova biblioteka za generisanje PDF-a za Delphi i C++Builder, pruža vam svaku od njih kao direktan poziv na objektu stranice, a jedina stvarna prepreka je koordinatni sistem u pozadini, koji radi u suprotnom smeru od VCL kanvasa na koji ste navikli. Prvo rešite tu orijentaciju i ostatak posla oko rasporeda će prestati da vam stvara probleme

Pozicioniranje teksta i donji levi koordinatni početak

Skoro svačiji prvi izveštaj izađe naopako. Naslov završi blizu donje ivice, a svaki red ispod njega se penje ka vrhu. Ništa nije u kvaru. PDF korisnički prostor (user space), definisan u ISO 32000-1 §8.3, postavlja koordinatni početak u donji levi ugao sa Y osom koja raste nagore, što je slika u ogledalu GDI kanvasa gde Y raste nadole od gornjeg levog ugla. Pet minuta koje provedete prihvatajući to štedi raspored koji biste inače morali ponovo da pišete kada brojevi prestanu da imaju smisla

Glavni poziv objekta stranice je TextOut(X, Y, Angle, Text). X i Y određuju lokaciju teksta u tačkama (points) od donjeg levog ugla, a Angle ga rotira u stepenima, što je način na koji se crta dijagonalni pečat DRAFT ili COPY bez ikakve posebne podrške. Trik koji omogućava da intuicija stečena na VCL-u nastavi da radi jeste da izrazite Y kao visinu stranice umanjenu za rastojanje koje ž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 u ovom listingu su odgovorna za većinu grešaka koje se pojavljuju tek na drugoj stranici. AddPage preusmerava CurrentPage na stranicu koju je upravo kreirao, tako da referenca stranice koju ste ranije keširali više ne crta tamo gde očekujete. Izbor fonta je takođe po stranici, a ne po dokumentu. Ako preskočite SetFont nakon AddPage, prvi TextOut na novoj stranici se vraća na podrazumevanu vrednost sa kojom je stranica započela, a ne na podebljani font naslova koji ste postavili pre tri stranice. Sigurna navika je da tretirate „pokretanje nove stranice” i „ponovno uspostavljanje stanja teksta” kao jedan neodvojiv korak u petlji izveštaja

Fontovi koji postoje na serveru, a ne samo na vašem desktopu

Većina problema sa fontovima su zapravo problemi sa instalacijom (deployment) u drugom obliku. Vaša razvojna mašina ima instaliran korporativni font, tako da izveštaj izgleda ispravno na vašem ekranu i tako ga isporučujete. Produkcijski server pokreće zadatak pod servisnim nalogom koji nikada nije imao instaliran taj font, renderer tiho zamenjuje font nečim što može da pronađe, a prva stvar koju čujete je klijent koji pita zašto se memorandum promenio. Rešenje je da prestanete da verujete sistemskom direktorijumu fontova i učitate font iz datoteke koju vaš instalater postavlja na disk. HotPDF-ov poziv za registraciju Unicode-a uzima putanju i radi upravo to:

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

TextOut direktno prihvata WideString, što je važnije nego što na prvi pogled izgleda. Ime klijenta sa akcentom, nemačka ulica, poljski grad: to nisu izuzeci, to su normalni sadržaji tabele klijenata, i oni prolaze kroz isti poziv kao i ASCII oznake koje direktno kodirate, pod uslovom da registrovani font zaista sadrži te karaktere. Jedno ograničenje verzije prati ugrađene fontove: dokument mora biti PDF 1.5 ili noviji, pa ako vas neki nepovezani zahtev drži na starijoj verziji, to je stvar koja će tiho zakazati. Pisma koja se pišu zdesna nalevo, kao što su arapski i hebrejski, zahtevaju stvarno oblikovanje (shaping) teksta umesto jednostavnog pretraživanja karaktera, i to ima svoj poseban proces; pogledajte naš članak o oblikovanju teksta složenih pisama pomoću HotPDF-a

Kada nijedan instalirani font ne može da izrazi ono što vam treba, na primer MICR karakteri na čeku ili vlasnički skup simbola, Type 3 fontovi popunjavaju prazninu. Svaki karakter definišete kao mali tok sadržaja pomoću RegisterType3Font i AddType3Glyph. To je specijalizovan deo API-ja i retko ćete ga koristiti, ali je mnogo čistiji od razbacivanja stotina sitnih bitmapa simbola po stranici

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

Rad sa slikama se deli na dva koraka, i njihovo držanje odvojenim je suština. AddImage uzima TBitmap ili TJPEGImage, ugrađuje ga jednom i vraća indeks. PNG grafika mora biti dekodirana u bitmapu pre nego što stigne tamo. ShowImage zatim iscrtava taj indeks gde god i koliko god često želite. Redosled argumenata u ShowImage je stvar na koju vredi obratiti pažnju:

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 ugla, a poslednji argument je ugao rotacije u stepenima. Ako potpis funkcije shvatite kao X1/Y1/X2/Y2 okvir, logo dimenzija 120 sa 40 postavljen na (50, 700) će se umesto toga rastegnuti odatle do (120, 40), šireći se preko većeg dela stranice. Izlaz čini grešku očiglednom dok izvorni kod izgleda sasvim razumno, zbog čega na to možete izgubiti celo popodne. KeepImageAspectRatio je podrazumevano podešeno na True, tako da okvir sa pogrešnim proporcijama prilagođava sliku umesto da je izobliči; promenite ga na False samo kada zaista želite da rastegnete sliku

Podela između registracije i postavljanja se isplati kod dugih serija. Pošto AddImage ugrađuje piksele jednom, a svaki ShowImage sa tim indeksom upućuje na isti ugrađeni objekat, mesto gde pozivate AddImage određuje veličinu datoteke. Ako ga pozovete unutar petlje stranice za izveštaj od 500 stranica, isti logo će biti ugrađen 500 puta. Pozovite ga jednom pre petlje, sačuvajte indeks i logo će biti sačuvan samo jednom. Mali rečnik koji se indeksira putanjom resursa dovoljan je da se osigura da se svaka jedinstvena slika registruje tačno jednom

Izbor kodeka je drugi faktor koji utiče na veličinu. Fotografski sadržaj, skenirani prilozi i slično, pripadaju JPEG-u: prosledite icJpeg u AddImage i spustite JpegQuality na oko 85, pošto to svojstvo počinje od 100, a razlika na 85 je nevidljiva na odštampanoj stranici. Jednobojna grafika, kao što su logotipi, grafikoni i linijski crteži, pripada icFlate-u, gde je kompresija bez gubitaka već kompaktna, a JPEG bi stvorio vidljive smetnje oko oštrih ivica. Izveštaj koji postavlja jednu fotografiju punog kvaliteta na svaku stranicu može narasti na gigabajte; isti sadržaj sa JPEG kvalitetom 85 biće oko deset puta manji, a nijedan čitalac neće primetiti razliku

Linije, okviri i senčenje pomoću vektorskih primitiva

Horizontalna linija ispod zaglavlja tabele i sivi okvir iza ukupnog iznosa ne moraju biti slike. Nacrtajte ih kao vektore i ostaće oštri pri svakom zumiranju, štampaće se jasno i skoro nimalo neće povećati datoteku. HotPDF prati isti model koji koriste sirovi PDF tokovi sadržaja: napravite 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;

Redosled nije opcionalan: podesite stanje bojenja, konstruišite putanju, a zatim pozovite Stroke or Fill. Putanja koju napravite ali je nikada ne obojite ne doprinosi ništa stranici, što je skoro uvek odgovor kada se linija „ne pojavljuje”. SetRGBFillColor uzima jednu TColor vrednost, tako da se poznate VCL konstante poput clNavy i clBlack mogu direktno koristiti, a Rectangle koristi iste argumente širine i visine kao i pozicioniranje slike, umesto dva ugla. Jedno upozorenje o tankim linijama: sve što je ispod otprilike pola tačke može izgledati elegantno na monitoru, a zatim nestati na kancelarijskom štampaču od 600 dpi, tako da je 0.75pt razuman minimum za svaku liniju koja mora da preživi štampanje

Paginacija na stvarnim podacima, a ne na uzorcima

Jedan detalj koji treba ispraviti pre nego što se raspored definiše: numeričke kolone treba da budu poravnate po desnoj ivici, a način da se to uradi je da izmerite renderovanu širinu svake vrednosti i pozicionirate je unazad od granice kolone, a ne da dopunjavate string razmacima na početku. Dopunjavanje razmacima se poravnava samo u monospace fontovima, a niko ne pravi finansijske izveštaje u monospace fontu. Prvo propustite vrednosti kroz Delphi-jeve funkcije koje prepoznaju regionalna podešavanja, kao što je FormatFloat, kako bi separator hiljada čiju širinu merite bio isti onaj koji će klijentova regionalna podešavanja zaista prikazati

Opasnost sa paginacijom je u tome što je pišete prema demo skupu podataka, gde deset kratkih redova staje na jednu stranicu i petlja nikada ne mora da se prekine. Produkcija vam donosi klijenta čiji naziv kompanije ima 140 karaktera i izveštaj sa 4.000 stavki, i tada petlja mora ispravno da se prekine svaki put. Šablon koji funkcioniše je jedan Y kursor koji se pomera nadole kako oduzimate visinu svakog reda, i provera koja započinje novu stranicu u trenutku kada bi kursor prešao donju marginu. Pomeranje nadole ovde znači smanjenje vrednosti Y, što je jedino mesto gde donji levi koordinatni početak ostaje kontraintuitivan. Držite sve to u jednoj rutini koja takođe ponovo postavlja SetFont i ponovo crta zaglavlje na novoj stranici, i greške sa pogrešnim brojem stranica se nikada neće pojaviti. Kada isti izveštaji moraju da zadovolje i pravila o arhiviranju ili pristupačnosti, odluke koje donesete upravo ovde (koje fontove ugrađujete, da li je izlaz tagovan, koje prostore boja koristite) jesu one koje ti standardi kontrolišu; HotPDF PDF/A, PDF/X i PDF/UA vodič vredi pročitati pre nego što se šablon učvrsti

Svaki poziv prikazan ovde – pozicioniranje teksta, registracija fonta, ugradnja slika i crtanje putanja – nalazi se u HotPDF komponenti za Delphi i C++Builder, čija referenca dokumentuje kompletan izlazni API zajedno sa funkcijama za forme, enkripciju i potpisivanje sa kojima dolazi