Apelul care pune text pe o pagină PDF este simplu. Dați lui AddText un șir, un font, o dimensiune și o poziție, iar glifele apar. Ceea ce nu face este să vă spună cât de larg va fi acel șir odată desenat, și nu împarte un șir lung pe mai multe linii. Un singur apel pictează o serie de text la o singură poziție. Dacă seria este mai lată decât coloana în care doriți să se încadreze, depășește pur și simplu marginea, iar nimic în apelul de desenare nu vă avertizează. În momentul în care doriți un paragraf în loc de o singură etichetă, piesa lipsă este lățimea unui șir în fontul și dimensiunea aleasă, măsurată înainte de a o comite pe pagină
Aceasta este problema clasică de layout. Pentru a împacheta un paragraf într-o coloană trebuie să știți, cuvânt cu cuvânt, cât spațiu orizontal va ocupa fiecare linie candidată, și trebuie să știți asta înaintea desenării oricărui lucru. Împachetarea cuvintelor este o buclă de măsurare înfășurată în jurul unui apel de desenare, iar un binding care numai desenează vă oferă a doua jumătate. Suportul de măsurare a textului din componenta PDFium acoperă acel gol cu două funcții, MeasureText și MeasureTextWidth, care raportează extinderea randată a unui șir fără a pune nicio urmă pe vreo pagină
De ce măsurarea este un class helper, nu o metodă nouă pe TPdf
Suportul de măsurare sosește ca un class helper Delphi pentru TPdf, locuind în propria unitate, nu ca metode noi fixate în clasa TPdf. Un class helper este o funcție de limbaj care vă permite să atașați metode unui tip existent din afara declarației sale. Odată ce unitatea este în domeniu, noile metode sunt apelate exact ca și cum ar aparține clasei, deci o metodă helper se citește ca Pdf.MeasureTextWidth(...) fără niciun obiect separat de construit sau transmis
Motivul pentru a-l stratifica în acest fel este separarea. Tipul de bază TPdf rămâne cum este, fără niciun câmp adăugat și nicio semnătură existentă atinsă, deci un proiect care nu are niciodată nevoie de layout nu poartă niciodată codul de măsurare. Un proiect care are nevoie adaugă o unitate la o clauză uses și metodele se aprind. Capacitatea devine opt-in la granularitatea unei singure unități, care este cel mai curat mod de a extinde un tip pe care nu îl dețineți sau nu doriți să îl perturbați
uses
PDFium, FPdfView, FPdfEdit,
FPdfMeasure; // the helper unit; brings MeasureText into scope on TPdf
// With the unit in scope the methods read as members of TPdf:
var
W, H: Double;
begin
Pdf.MeasureText('Subtotal', 'Helvetica', 11, W, H);
// W and H are now the rendered width and height in PDF user units
end;
Măsurarea fără a atinge pagina
Măsurarea trebuie să fie lipsită de efecte secundare. Trebuie să raporteze o lățime fără a lăsa nimic în urmă, deoarece o apelați de mai multe ori în timp ce decideți un layout, iar pagina trebuie să arate exact cum ar fi arătat dacă nu ați fi măsurat niciodată. Tehnica care face acest lucru posibil este să construiți un obiect text, să îi cereți dimensiunea sa și să-l aruncați înainte de a fi vreodată atașat la o pagină
Secvența este de patru apeluri PDFium. FPDFPageObj_NewTextObj creează un obiect text față de document, dat fiind numele fontului și dimensiunea. FPDFText_SetText setează șirul pe care îl poartă acel obiect. FPDFPageObj_GetBounds citește înapoi caseta de delimitare a obiectului. FPDFPageObj_Destroy eliberează obiectul. Crucial, nimic în acea secvență nu apelează API-ul de inserare a paginii. Obiectul este creat, interogat și distrus în izolare, deci documentul este neschimbat când funcția returnează. Este o sondă de unică folosință al cărei unic output sunt cele patru numere ale casetei sale de delimitare
Aceasta este modalitatea robustă de a face aceasta, deoarece PDFium nu expune o lățime de avans per glifă convenabilă pe care ați putea-o suma singur. Metricile glifelor depind de programul de fonturi, de codificare și de modul în care PDFium încarcă fața fontului, și nu există niciun apel public care să vă înmâneze avansul fiecărui caracter dintr-un șir. Caseta de delimitare a unui obiect text real, pe de altă parte, este calculată de aceleași mecanisme care ar aranja glifele pentru desenare, deci reflectă extinderea reală randată, nu o aproximare. Construirea unui obiect de unică folosință și citirea limitelor sale este cea mai fiabilă măsurare pe care biblioteca o poate oferi
// The shape of MeasureText, expressed against the verified PDFium calls.
// A text object is built, measured, and destroyed; no page is involved.
procedure TPdfMeasureHelper.MeasureText(const Text, Font: WString;
FontSize: Single; out Width, Height: Double);
var
TextObject: FPDF_PAGEOBJECT;
L, B, R, T: Single;
begin
Width := 0;
Height := 0;
if Self.Document = nil then
Exit;
TextObject := FPDFPageObj_NewTextObj(Self.Document,
FPDF_BYTESTRING(AnsiString(Font)), FontSize);
if TextObject = nil then
Exit;
try
if FPDFText_SetText(TextObject, FPDF_WIDESTRING(WideString(Text))) = 0 then
Exit;
if FPDFPageObj_GetBounds(TextObject, L, B, R, T) <> 0 then
begin
Width := R - L;
Height := T - B;
end;
finally
FPDFPageObj_Destroy(TextObject); // probe discarded, page untouched
end;
end;
Coordonatele și unitățile rezultatului
Caseta de delimitare revine ca patru margini, stânga, jos, dreapta și sus, iar cele două dimensiuni rezultă prin scădere. Lățimea este dreapta minus stânga, iar înălțimea este sus minus jos. Ambele sunt exprimate în unități utilizator PDF, unde o unitate este o șaptezeci și doua dintr-un inch, același spațiu de coordonate în care poziționați textul pe pagină. Nu există nicio unitate ascunsă de dispozitiv și niciun pixel implicat în această etapă. O lățime de 36 înseamnă jumătate de inch de pagină, indiferent de rezoluția de randare eventuală
Axa verticală rulează în modul în care PDF o definește, cu Y crescând în sus, motiv pentru care înălțimea este sus minus jos și nu invers. Acel detaliu contează când avansați un cursor în jos pe o coloană. Măsurați înălțimea unei linii, apoi scădeți-o din linia de bază curentă pentru a găsi pe cea următoare, deoarece deplasarea în jos pe pagină înseamnă deplasarea spre Y mai mic. Dacă destinația dvs. este un ecran mai degrabă decât hârtie, convertiți unitățile utilizator în pixeli de dispozitiv cu rezoluția de afișare: o valoare în unități utilizator înmulțită cu DPI și împărțită la 72 dă pixeli, deci o lățime de coloană setată în puncte poate fi comparată cu o serie măsurată înainte de a decide unde are loc ruptura
Ce se întâmplă la intrări degenerate
Funcțiile sunt scrise să eșueze silențios. Dacă nu există niciun document deschis, sau dacă obiectul text nu poate fi creat, rezultatul este o extindere zero în loc de o excepție ridicată. Lățimea și înălțimea sunt inițializate la zero în partea de sus și suprascrise numai odată ce o casetă de delimitare a fost citită înapoi cu succes. Un șir gol, un document lipsă, un font pe care biblioteca nu îl poate rezolva într-un obiect, fiecare dintre acestea returnează zero în loc să arunce o excepție
Acea alegere menține o buclă de măsurare simplă, deoarece o buclă care rulează peste mii de cuvinte nu este locul pentru gestionarea excepțiilor la fiecare iterație. Costul este că apelantul poartă verificarea. O lățime zero este un sentinel, nu un fapt despre text, deci codul care împarte la o lățime măsurată sau presupune o valoare pozitivă trebuie să se protejeze împotriva lui zero înainte de a-l considera de încredere. Tratați zero ca „nu s-a putut măsura" și contractul este clar; ignorați-l și o intrare degenerată devine silențios un layout cu o coloană de glife suprapuse
O împachetare lacomă a cuvintelor construită pe măsurare
Cu o funcție de lățime la îndemână, împachetarea cuvintelor este o buclă lacomă scurtă. Împărțiți paragraful în cuvinte, mențineți o linie curentă, iar pentru fiecare cuvânt măsurați cum ar fi linia dacă ați adăuga acel cuvânt. Cât timp linia de încercare se încadrează încă în lățimea coloanei, continuați să adăugați; când ar depăși, goliți linia curentă cu AddText și porniți una nouă cu cuvântul care nu s-a potrivit. Acumularea se face în întregime cu MeasureTextWidth, iar singurul lucru care ajunge vreodată la pagină este o linie pe care ați confirmat deja că se potrivește
procedure WrapParagraph(Pdf: TPdf; const Para, Font: WString;
FontSize: Single; X, TopY, ColumnWidth, LineHeight: Double);
var
Words: TArray<WideString>;
Line, Trial: WideString;
I: Integer;
Y: Double;
begin
Words := WideString(Para).Split([' ']);
Line := '';
Y := TopY;
for I := 0 to High(Words) do
begin
if Line = '' then
Trial := Words[I]
else
Trial := Line + ' ' + Words[I];
// Measure the candidate line before drawing anything.
if (Line <> '') and (Pdf.MeasureTextWidth(Trial, Font, FontSize) > ColumnWidth) then
begin
Pdf.AddText(X, Y, Font, FontSize, Line); // flush the line that fit
Y := Y - LineHeight; // Y decreases going down
Line := Words[I]; // overflowing word starts next line
end
else
Line := Trial;
end;
if Line <> '' then
Pdf.AddText(X, Y, Font, FontSize, Line); // flush the final line
end;
Bucla măsoară linia de încercare mai degrabă decât să măsoare fiecare cuvânt și să le sumeze, deoarece lățimea unei linii nu este suma lățimilor cuvintelor sale. Spațiile dintre cuvinte contribuie, iar o serie măsurată captează asta direct. Regula lacomă, potriviți cât mai multe cuvinte permite coloana și faceți ruptura la ultimul care se potrivește, este aceeași regulă care umple golul dintre un AddText brut și un paragraf real. Apelul de desenare nu a fost niciodată partea dificilă. Măsurarea care trebuie să o preceadă este, și exact asta oferă helper-ul
Unde se încadrează aceasta
Măsurarea este stratul dintre generarea conținutului și randarea lui, deci se potrivește în mod natural cu restul unui flux de lucru de document creat de la zero. Dacă asamblați pagini și plasați text în primul rând, fundamentul este în crearea documentelor PDF de la zero cu componenta PDFium în Delphi, unde AddText și configurarea paginii sunt acoperite complet. Când fontul pe care îl măsurați contează la fel de mult ca șirul, deoarece metricile depind de față, analiza proprietăților fontului PDF cu componenta PDFium în Delphi arată cum raportează biblioteca informațiile despre font care conduc acele casete de delimitare. Ambele se construiesc pe același binding, componenta PDFium Component pentru Delphi și Lazarus, unde helper-ul de măsurare este livrat alături de API-urile de document, pagină și text descrise pe tot acest blog