Technical Article

PDF ataskaitų kūrimas su „HotPDF“ „Delphi“ aplinkoje: „TextOut“, šriftai ir vaizdai

Ataskaitos generavimas iš esmės susideda iš trijų elementų išdėstymo puslapyje: teksto nurodytose koordinatėse, šriftų, kurie serveryje atvaizduojami taip pat kaip ir jūsų kompiuteryje, ir tinkamo dydžio vaizdų. Visa kita, ką atlieka ataskaitų biblioteka, yra sukurta aplink šiuos tris elementus. „HotPDF“ – „losLab“ sukurta PDF generavimo biblioteka, skirta „Delphi“ ir „C++Builder“ – suteikia galimybę valdyti kiekvieną iš jų tiesioginiais puslapio objekto iškvietimais. Jedintelis iššūkis yra koordinatų sistema, kuri veikia priešingai nei esate įpratę VCL drobėje (canvas). Pirmiausia išsiaiškinkite šią orientaciją ir tuomet kurti puslapio struktūrą bus kur kas lengviau.

Teksto išdėstymas ir pradžios taškas apatiniame kairiajame kampe

Beveik kiekvieno pirmoji sugeneruota ataskaita būna apversta aukštyn kojomis. Pavadinimas atsiranda netoli apatinio krašto, o kiekviena sekanti eilutė kyla į viršų. Tačiau viskas veikia teisingai. PDF naudotojo erdvėje (apibrėžtoje ISO 32000-1 §8.3) pradiniu tašku laikomas apatinis kairysis kampas, o Y koordinatė didėja į viršų. Tai yra veidrodinis GDI drobės atspindys, kur Y koordinatė didėja žemyn nuo viršutinio kairiojo kampo. Skirkite penkias minutes šiam principui suprasti ir tai padės išsaugoti puslapio struktūrą, kurią kitu atveju tektų perrašyti.

Pagrindinis puslapio objekto metodas yra TextOut(X, Y, Angle, Text). X ir Y nurodo teksto vietą taškais (points) nuo apatinio kairiojo kampo, o Angle pasuka jį laipsniais. Taip be jokio papildomo funkcionalumo galima nupiešti įstrižą antspaudą, pavyzdžiui, „DRAFT“ ar „COPY“. Kad galėtumėte vadovautis VCL įprasta logika, Y koordinatę išreikškite kaip puslapio aukštį, iš kurio atimamas norimas atstumas nuo viršaus:

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;

Šiame pavyzdyje parodytos dvi savybės, susijusios su būsenos keitimu, sukelia daugiausia klaidų, kurios išryškėja tik antrame puslapyje. Metodas AddPage nukreipia CurrentPage į naujai sukurtą puslapį, zodžiu, anksčiau išsaugota puslapio nuoroda nebeveikia ten, kur tikitės. Šrifto parinkimas taip pat atliekamas kiekvienam puslapiui atskirai, o ne visam dokumentui. Jei praleisite SetFont po AddPage iškvietimo, pirmasis TextOut naujame puslapyje naudos numatytąjį šriftą, o ne paryškintą antraštės šriftą, kurį nustatėte prieš tris puslapius. Geriausia praktika yra laikyti puslapio sukūrimą ir teksto būsenos atstatymą vienu nedalomu žingsniu ataskaitos kūrimo cikle.

Šriftai, kurie yra serveryje, o ne tik jūsų kompiuteryje

Dauguma problemų su šriftais iš tikrųjų yra susijusios su programos diegimu (deployment). Jūsų programuotojo kompiuteryje yra įdiegtas įmonės šriftas, todėl ataskaita atrodo tinkamai. Tačiau gamybinėje aplinkoje procesas vykdomas per tarnybinę paskyrą (service account), kurioje šis šriftas niekada nebuvo įdiegtas. Sistema tyliai pakeičia jį kitu rastu šriftu, o jūs apie tai sužinote tik tada, kai klientas paklausia, kodėl pasikeitė firminis blankas. Sprendimas yra nepasitikėti operacinės sistemos šriftų katalogu, o įkelti šriftą tiesiai iš failo, kurį jūsų diegimo programa įrašo į diską. Būtent tai atlieka „HotPDF“ Unicode registracijos funkcija, kuriai nurodomas kelias iki failo:

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

TextOut tiesiogiai priima WideString tipo reikšmę, o tai yra svarbiau, nei gali pasirodyti iš pirmo žvilgsnio. Kliento vardas su diakritiniais ženklais, vokiškas gatvės pavadinimas ar lenkiškas miestas nėra išskirtiniai atvejai, tai yra įprasti klientų lentelės duomenys. Jie apdorojami tuo pačiu metodu kaip ir jūsų įrašytos ASCII žymės, jei tik užregistruotas šriftas turi reikiamus simbolius (glyphs). Kartu su įterptais šriftais atsiranda vienas versijos apribojimas: dokumentas turi atitikti PDF 1.5 ar vėlesnę versiją. Jei dėl kokių nors priežasčių turite naudoti senesnę versiją, šis funkcionalumas neveiks. Raštui iš dešinės į kairę, pavyzdžiui, arabų ar hebrajų kalboms, reikia tikrojo teksto formavimo (shaping), o ne paprasto simbolių atvaizdavimo. Tam naudojami kiti sprendimai, kurie aprašyti straipsnyje apie sudėtingo rašto teksto formavimą su „HotPDF“.

Kai joks įdiegtas šriftas negali atvaizduoti to, ko jums reikia (pavyzdžiui, MICR simbolių ant čekių ar specifinių ženklų rinkinio), šią spragą užpildo „Type 3“ šriftai. Kiekvieną simbolį galite apibrėžti kaip nedidelį turinio srautą naudodami RegisterType3Font and AddType3Glyph. Tai yra specifinė API dalis, kurią naudosite retai, tačiau tai yra daug tvarkingesnis sprendimas nei daugybės mažų simbolių paveikslėlių (bitmaps) dėliojimas puslapyje.

Vaizdai: viduriniai argumentai yra plotis ir aukštis, o ne kampo koordinatės

Vaizdo apdorojimas yra padalintas į du žingsnius, ir labai svarbu juos skirti. Metodas AddImage priima TBitmap arba TJPEGImage objektą, vieną kartą jį įterpia ir grąžina indeksą. PNG paveikslėliai prieš tai turi būti dekoduoti į bitmap formatą. Tada su ShowImage galite piešti tą vaizdą nurodytame indekse tiek kartų, kiek reikia. Todėl verta atidžiai peržiūrėti ShowImage parametrų tvarką:

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;

Du skaičiai po pozicijos nurodo plotį ir aukštį. Tai nėra priešingo kampo koordinatės. Paskutinis argumentas yra pasukimo kampas laipsniais. Jei parametrus suprasite kaip X1/Y1/X2/Y2 sritį, tuomet 120x40 dydžio logotipas, padėtas taške (50, 700), išsitemps iki taško (120, 40) ir užims didžiąją puslapio dalį. Gautas rezultatas iškart išduos klaidą, nors pats kodas gali atrodyti logiškas, todėl tokios klaidy paieška gali atimti nemažai laiko. Nustatymas KeepImageAspectRatio pagal numatytuosius nustatymus yra True, todėl parinkus netinkamas proporcijas vaizdas bus tiesiog apkirptas, o ne deformuotas. Pakeiskite šią reikšmę į False tik tada, kai tikrai norite ištempti vaizdą.

Skirtumas tarp vaizdo užregistravimo ir jo atvaizdavimo ypač išryškėja generuojant didelius dokumentus. Kadangi AddImage įterpia pikselius tik vieną kartą, o kiekvienas ShowImage su tuo pačiu indeksu nurodo tą patį objektą, nuo to, kur iškviečiate AddImage, priklauso galutinis failo dydis. Jei iškviesite jį puslapių cikle kurdami 500 puslapių dokumentą, tas pats logotipas bus įterptas 500 kartų. Iškvieskite jį vieną kartą prieš ciklą, išsaugokite indeksą ir logotipas bus įrašytas tik kartą. Paprastas žodynas (dictionary), susietas su vaizdo failo keliu, leidžia užtikrinti, kad kiekvienas vaizdas būtų užregistruotas tik vieną kartą.

Kitas būdas valdyti failo dydį yra kodeko parinkimas. Fotografinio pobūdžio turiniui, skenuotiems priedams ir panašiems vaizdams naudokite JPEG formatą: perduokite icJpeg į AddImage ir sumažinkite JpegQuality iki maždaug 85 (ši savybė prasideda nuo 100, tačiau kokybės skirtumas esant 85 yra nepastebimas atspausdintame puslapyje). Vektorinio stiliaus grafikai, pavyzdžiui, logotipams ar diagramoms, tinka icFlate su suspaudimu be praradimų. Čia JPEG formatas gali sukurti matomus artefaktus aplink griežtus kraštus. Dokumentų paketas, kuriame į kiekvieną puslapį įterpiama maksimalios kokybės nuotrauka, gali išaugti iki kelių gigabaitų, o naudojant JPEG 85 kokybę failas bus dešimt kartų mažesnis ir skaitytojas nepastebės jokio skirtumo.

Linijos, stačiakampiai ir šešėliavimas su vektorinėmis formomis

Horizontali linija po lentelės antrašte ir pilkas fonas po sumos laukeliu neprivalo būti paveikslėliai. Nupieškite juos kaip vektorius ir jie išliks ryškūs bet kokiame mastelyje bei praktiškai nepadidins failo dydžio. „HotPDF“ naudoja tą patį modelį kaip ir standartiniai PDF turinio srautai: sukuriama kreivė (path), o tada iškviečiamas jos piešimo operatorius.

// 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;

Ši tvarka yra privaloma: nustatykite spalvą ir linijos storį, sukurkite kreivę, o tada iškvieskite Stroke arba Fill. Jei sukursite kreivę, bet jos nenupiešite, puslapyje nieko neatsiras, o tai dažniausiai ir būna priežastis, kodėl nematote nubrėžtos linijos. Metodas SetRGBFillColor priima TColor reikšmę, todėl galite naudoti įprastas VCL konstantes, tokias kaip clNavy ar clBlack. Metodas Rectangle naudoja pločio ir aukščio argumentus, o ne dviejų kampų koordinates. Būkite atsargūs su plonomis linijomis: plonesnės nei pusės taško linijos ekrane gali atrodyti gražiai, tačiau pranykti spausdinant 600 dpi biuro spausdintuvu, todėl 0.75pt yra rekomenduojama minimali riba.

Puslapiavimas pagal realius, o ne pavyzdinius duomenis

Viena svarbi detalė prieš pradedant kurti puslapio struktūrą: skaitiniai stulpeliai turėtų būti lygiuojami pagal dešinįjį kraštą. Tai reikėtų daryti išmatuojant kiekvienos reikšmės plotį ir atimant jį iš stulpelio ribos koordinačių, o ne užpildant eilutę tarpais iš kairės. Tarpų naudojimas veikia tik su lygiapločiais (monospaced) šriftais, tačiau finansinėse ataskaitose tokie šriftai praktiškai nenaudojami. Pirmiausia apdorokite skaičius su „Delphi“ lokalės funkcijomis, tokiomis kaip FormatFloat, kad tūkstančių skirtukas, kurio plotį matuojate, sutaptų su tuo, kurį matys klientas savo aplinkoje.

Kuriant puslapiavimą kyla pavojus, kad parašysite kodą tik pavyzdiniams duomenims, kur dešimt eilučių telpa į vieną puslapį. Tačiau gamybinėje aplinkoje galite gauti klientą, kurio įmonės pavadinimas yra 140 simbolių ilgio, o pati ataskaita turi 4000 eilučių, todėl puslapiai privalo būti skaidomi teisingai. Tinkamas sprendimas yra naudoti vieną Y žymeklį (cursor), kuris juda žemyn atimant kiekvienos eilutės aukštį, ir atlikti patikrą, kuri sukuria naują puslapį, kai žymeklis kerta apatinę paraštę. Judėjimas žemyn čia reiškia Y koordinatės mažėjimą, o tai yra vienintelė vieta, kur apatinio kairiojo kampo logika išlieka neįprasta. Visą šį funkcionalumą rekomenduojama realizuoti viename metode, kuris taip pat iš naujo priskiria SetFont ir nupiešia puslapio antraštę naujame puslapyje. Kai tos pačios ataskaitos turi atitikti archyvavimo ar prieinamumo (accessibility) taisykles, būtent šie pasirinkimai (kokius šriftus įterpiate, ar išvestis yra pažymėta, kokias spalvų erdves naudojate) lemia atitiktį standartams, todėl prieš pradedant kurti šabloną verta perskaityti PDF/A, PDF/X ir PDF/UA kūrimo su „HotPDF“ vadovą.

Visi čia parodyti metodai (teksto pozicionavimas, šriftų registravimas, vaizdų įterpimas ir vektorinis piešimas) yra dalis HotPDF Component, skirto „Delphi“ ir „C++Builder“, kurio dokumentacijoje aprašyta visa išvesties API bei formų kūrimo, šifravimo ir pasirašymo funkcijos.