Articol tehnic

Rapoarte PDF în Delphi cu HotPDF: TextOut, fonturi și imagini

Generarea unui raport se reduce la a plasa trei lucruri pe o pagină și a le face să cadă de acord asupra locului unde stau: text la coordonate cunoscute, fonturi care se randează la fel pe server ca pe desktopul dumneavoastră și imagini dimensionate să încapă. Tot restul lucrurilor pe care le face o bibliotecă de rapoarte se organizează în jurul acestor trei. HotPDF, biblioteca de generare PDF a losLab pentru Delphi și C++Builder, vă oferă fiecare dintre acestea ca un apel direct pe obiectul pagină, iar singura fricțiune reală este sistemul de coordonate de dedesubt, care merge în sens opus canvas-ului VCL cu care sunteți obișnuit. Rezolvați mai întâi acea orientare, iar restul lucrului de layout încetează să vă lupte împotrivă

Plasarea textului și originea din stânga jos

Primul raport al aproape oricui iese cu susul în jos. Titlul aterizează lângă marginea de jos, iar fiecare linie de sub el urcă spre partea de sus. Nimic nu funcționează defectuos. Spațiul utilizator PDF, definit în ISO 32000-1 §8.3, plasează originea în colțul din stânga jos, cu Y crescând în sus, ceea ce este imaginea în oglindă a canvas-ului GDI, unde Y crește în jos din colțul din stânga sus. Cinci minute petrecute făcând pace cu asta economisesc un layout pe care altfel l-ați rescrie odată ce numerele încetează să mai aibă sens

Diagramă HotPDF contrastând originea de coordonate VCL din stânga-sus cu originea PDF din stânga-jos, unde TextOut plasează un titlu la 50 de puncte de susul unei pagini Letter, la Y 792 minus 50
Spațiul de utilizator PDF oglindește canvas-ul VCL, astfel încât un titlu la 50pt de partea de sus a unei pagini Letter este TextOut(50, 792 - 50, 0, 'INVOICE'), iar aceeași conversie menține fiecare coordonată de raport intuitivă

Apelul central al obiectului pagină este TextOut(X, Y, Angle, Text). X și Y localizează textul în puncte, măsurate din colțul din stânga jos, iar Angle îl rotește în grade, ceea ce este modul în care se desenează o ștampilă diagonală DRAFT sau COPY, fără niciun suport special. Trucul care lasă intuiția antrenată pe VCL să funcționeze în continuare este să exprimați Y ca înălțimea paginii minus distanța dorită față de partea de sus:

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 de la partea de sus a paginii Letter
    Pdf.CurrentPage.SetFont('Arial', [], 10);
    Pdf.CurrentPage.TextOut(50, 792 - 70, 0, 'Date: 2026-06-11');
    Pdf.CurrentPage.TextOut(300, 400, 45, 'COPY');              // ștampilă rotită
    Pdf.AddPage;                                                // CurrentPage indică acum aici
    Pdf.CurrentPage.SetFont('Arial', [], 10);                   // starea fontului nu se transferă
    Pdf.CurrentPage.TextOut(50, 742, 0, 'Page 2 detail rows');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Cele două comportamente cu stare din acel listing sunt responsabile pentru majoritatea bug-urilor care apar doar pe pagina a doua. AddPage repoziționează CurrentPage către pagina tocmai creată, astfel încât o referință de pagină memorată anterior nu mai desenează unde vă așteptați. Selecția fontului este de asemenea per pagină, nu per document. Dacă săriți peste SetFont după un AddPage, primul TextOut de pe pagina proaspătă revine la orice implicit avea pagina la pornire, nu la fontul de titlu bold pe care l-ați setat cu trei pagini în urmă. Obiceiul sigur este să tratați "începe o pagină nouă" și "restabilește starea textului" ca pe un singur pas inseparabil în bucla de raport

Fonturi care există pe server, nu doar pe desktopul dumneavoastră

Cele mai multe probleme de font sunt de fapt probleme de implementare deghizate. Mașina dumneavoastră de dezvoltare are fontul corporativ instalat, așa că raportul arată bine pe ecranul dumneavoastră și e livrat. Gazda de producție rulează sarcina sub un cont de serviciu care nu a avut niciodată acel font instalat, randatorul substituie pe tăcute ceva ce poate găsi, iar primul semn că cineva află despre asta este un client care întreabă de ce s-a schimbat antetul. Ieșirea din impas este să nu mai aveți încredere în directorul de fonturi al sistemului de operare și să încărcați fontul dintr-un fișier pe care instalatorul dumneavoastră îl pune pe disc. Apelul de înregistrare Unicode al HotPDF primește o cale și face exact asta:

Diagramă a unei probleme de implementare de fonturi PDF Delphi: serverul de producție substituie silențios un font lipsă, în timp ce RegisterUnicodeTTF încarcă TTF-ul dintr-un fișier implementat și îl încorporează în PDF
Contarea pe directorul de fonturi al OS se rupe când contul de serviciu de producție duce lipsă de font, în timp ce încărcarea TTF dintr-un fișier livrat încorporează glifele, iar fiecare gazdă randează identic
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 acceptă direct un WideString, ceea ce contează mai mult decât pare la prima vedere. Un nume de client cu accent, o stradă germană, un oraș polonez: acestea nu sunt cazuri limită, sunt conținutul normal al unui tabel de clienți, iar ele trec prin același apel ca și etichetele ASCII pe care le codificați dur, atâta timp cât fontul înregistrat conține de fapt glifele. O constrângere de versiune vine la pachet cu fonturile înglobate: documentul trebuie să fie PDF 1.5 sau ulterior, așa că dacă o cerință fără legătură vă fixează la o versiune mai veche, acesta este lucrul care se va strica pe tăcute. Scrierile de la dreapta la stânga, precum araba și ebraica, au nevoie de shaping real, nu de o căutare directă de glife, iar aceasta are propriul flux; vedeți articolul nostru despre shaping-ul de text pentru scrieri complexe cu HotPDF

Atunci când niciun font instalat nu poate exprima ceea ce aveți nevoie, gândiți-vă la caracterele MICR de pe un cec sau la un set de simboluri proprietar, fonturile Type 3 umplu golul. Definiți fiecare glif ca un mic flux de conținut prin RegisterType3Font și AddType3Glyph. Este un colț specializat al API-ului și rareori veți apela la el, dar este mult mai curat decât să împrăștiați sute de bitmap-uri minuscule de simboluri pe o pagină

Imagini: argumentele din mijloc sunt lățime și înălțime, nu un colț

Gestionarea imaginilor se împarte în doi pași, iar păstrarea lor separată este tot rostul. AddImage primește un TBitmap sau TJPEGImage, îl înglobează o singură dată și returnează un index. Materialul grafic PNG trebuie decodat într-un bitmap înainte de a ajunge acolo. ShowImage desenează apoi acel index oriunde și de câte ori doriți. Ordinea argumentelor la ShowImage este singurul loc unde merită să încetiniți și să citiți:

Diagramă a conductei de imagini HotPDF, în care AddImage încorporează bitmap-ul o dată și returnează un indice, ShowImage îl plasează după lățime și înălțime, iar ordinea argumentelor nu este o pereche de colțuri
AddImage încorporează pixelii o dată, iar fiecare apel ShowImage refolosește acel index, iar argumentele din mijloc sunt o lățime și o înălțime, nu coordonatele unui colț opus
var
  Png: TPngImage;
  Logo: TBitmap;
  LogoIdx: Integer;
begin
  Png := TPngImage.Create;
  Logo := TBitmap.Create;
  try
    Png.LoadFromFile('brand-logo.png');
    Logo.Assign(Png);                       // decodează PNG-ul într-un bitmap
    LogoIdx := Pdf.AddImage(Logo, icFlate); // fără pierderi, pentru grafică cu culori plate
  finally
    Logo.Free;
    Png.Free;
  end;
  // (Index, X, Y, Lățime, Înălțime, Unghi): nu (X1, Y1, X2, Y2)
  Pdf.CurrentPage.ShowImage(LogoIdx, 50, 700, 120, 40, 0);
end;

Cele două numere de după poziție sunt o lățime și o înălțime. Nu sunt coordonatele colțului opus, iar argumentul final este un unghi de rotație în grade. Citiți semnătura ca pe o casetă X1/Y1/X2/Y2 și un logo de 120 pe 40 plasat la (50, 700) se întinde în schimb de acolo până la (120, 40), întinzându-se pe cea mai mare parte a paginii. Rezultatul face greșeala evidentă, în timp ce codul sursă arată complet rezonabil, ceea ce este exact ce face să se piardă o după-amiază. KeepImageAspectRatio este implicit True, așa că o casetă cu proporții greșite încadrează imaginea cu benzi, în loc să o distorsioneze; comutați-l pe False doar atunci când chiar intenționați să întindeți imaginea

Separarea între înregistrare și plasare se răsplătește pe rulări lungi. Pentru că AddImage înglobează pixelii o singură dată, iar fiecare ShowImage cu acel index indică înapoi spre același obiect înglobat, locul unde apelați AddImage decide dimensiunea fișierului. Apelați-l în interiorul buclei de pagini pentru un extras de cont de 500 de pagini, iar același logo se înglobează de 500 de ori. Apelați-l o singură dată înainte de buclă, păstrați indexul, iar logo-ul este stocat o singură dată. Un mic dicționar indexat după calea resursei este suficient ca să vă asigurați că fiecare imagine distinctă este înregistrată exact o dată

Alegerea codecului este cealaltă pârghie de dimensiune. Conținutul fotografic, atașamentele scanate și altele asemenea, aparțin JPEG-ului: transmiteți icJpeg către AddImage și coborâți JpegQuality la aproximativ 85, pentru că proprietatea pornește de la 100, iar diferența la 85 este invizibilă pe o pagină tipărită. Materialul grafic cu culori plate, precum logo-uri, grafice și desene liniare, aparține icFlate, unde compresia fără pierderi este deja compactă, iar JPEG ar mânji un ringing vizibil în jurul marginilor dure. O rulare de extrase de cont care împinge o fotografie de calitate maximă pe fiecare pagină poate umfla fișierul până la gigaocteți; același conținut la JPEG 85 ajunge la aproximativ o zecime din dimensiune, și niciun cititor nu-și dă seama

Linii, casete și umbrire cu primitive de traseu

Linia orizontală de sub un antet de tabel și caseta gri din spatele unei cifre de total nu trebuie să fie imagini. Desenați-le ca vectori și rămân clare la orice zoom, se tipăresc net și adaugă aproape nimic la fișier. HotPDF urmează același model pe care îl folosesc fluxurile de conținut PDF brute: construiește un traseu, apoi apelează un operator care îl pictează

// Linie orizontală sub antetul tabelului
Pdf.CurrentPage.SetLineWidth(0.75);
Pdf.CurrentPage.MoveTo(50, 660);
Pdf.CurrentPage.LineTo(545, 660);
Pdf.CurrentPage.Stroke;

// Casetă de total umbrită: X, Y, lățime, înălțime
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 120, 150, 40);
Pdf.CurrentPage.Fill;

Ordinea nu este opțională: setați starea de pictare, construiți traseul, apoi apelați Stroke sau Fill. Un traseu pe care îl construiți dar nu îl pictați niciodată nu contribuie cu nimic la pagină, ceea ce este aproape întotdeauna răspunsul atunci când o linie "nu apare". SetRGBFillColor primește un singur TColor, așa că familiarele constante VCL precum clNavy și clBlack se potrivesc direct, iar Rectangle folosește aceleași argumente de lățime și înălțime ca plasarea imaginilor, nu două colțuri. O avertizare despre liniile subțiri: orice sub aproximativ jumătate de punct poate arăta elegant pe un monitor și apoi poate dispărea pe o imprimantă de birou de 600 dpi, așa că 0,75pt este un prag rezonabil pentru orice linie care trebuie să supraviețuiască tipăririi

Paginarea cu date reale, nu cu date eșantion

Un detaliu de rezolvat corect înainte ca layout-ul să se fixeze: coloanele numerice ar trebui aliniate pe marginea lor dreaptă, iar modul de a face asta este să măsurați lățimea randată a fiecărei valori și să o poziționați înapoi de la limita coloanei, nu să umpleți șirul cu spații la început. Umplerea cu spații se aliniază doar într-un font monospațiat, și nimeni nu redactează un raport financiar într-un font monospațiat. Treceți mai întâi valorile prin rutinele sensibile la localizare ale Delphi, precum FormatFloat, astfel încât separatorul de mii a cărui lățime o măsurați să fie același pe care localizarea clientului îl va afișa efectiv

Pericolul cu paginarea este că o scrieți față de setul de date demo, unde zece rânduri scurte încap pe o pagină, iar bucla nu trebuie niciodată să se întrerupă. Producția vă dă un client a cărui denumire de companie are 140 de caractere și un extras de cont cu 4.000 de linii, iar acum bucla trebuie să se întrerupă corect de fiecare dată. Modelul care rezistă este un singur cursor Y care se mișcă în jos pe măsură ce scădeți înălțimea fiecărui rând, și o verificare care începe o pagină nouă în momentul în care cursorul ar traversa marginea de jos. În jos înseamnă aici Y descrescător, care este singurul loc în care originea din stânga jos rămâne contraintuitivă. Păstrați tot acest lucru într-o singură rutină care de asemenea reemite SetFont și redesenează antetul curent pe noua pagină, iar bug-urile de tip off-by-one-page nu mai prind niciodată contur. Când aceleași rapoarte trebuie să respecte și reguli de arhivare sau accesibilitate, alegerile pe care le faceți chiar aici, ce fonturi înglobați, dacă ieșirea este taguită, ce spații de culoare folosiți, sunt exact cele pe care acele standarde le supraveghează; ghidul HotPDF pentru PDF/A, PDF/X și PDF/UA merită citit înainte ca șablonul să se fixeze

Fiecare apel prezentat aici, poziționarea textului, înregistrarea fonturilor, înglobarea imaginilor și desenarea de traseu, vine inclus în HotPDF Delphi Component pentru Delphi și C++Builder, a cărui referință documentează întregul API de ieșire, alături de funcțiile de formulare, criptare și semnare cu care coexistă