Articol tehnic

Măsurarea textului PDF pentru aspect și încadrarea cuvintelor în Delphi

Apelul care plasează textul pe o pagină PDF este simplu. Îi oferiți lui AddText un șir de caractere, un font, o dimensiune și o poziție, iar glifele apar. Ceea ce nu face este să vă spună cât de lat va fi acel șir odată ce va fi desenat și nu întrerupe un șir lung pe mai multe linii. Un singur apel desenează un rând de text într-o singură poziție. Dacă rândul este mai lat decât coloana în care ați vrut să se potrivească, trece pur și simplu de margine și nimic din apelul de desen 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 plasa pe pagină

Aceasta este problema clasică a aspectului. Pentru a încadra 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 acest lucru înainte de a desena ceva. Încadrarea cuvintelor este o buclă de măsurare înfășurată în jurul unui apel de desenare, iar o legătură care doar desenează vă oferă a doua jumătate. Suportul de măsurare a textului din componenta PDFium umple acel gol cu două funcții, MeasureText și MeasureTextWidth, care raportează extinderea redată a unui șir de caractere fără a pune vreun semn pe vreo pagină

De ce măsurarea este un ajutor de clasă, nu o nouă metodă pe TPdf

Suportul pentru măsurare vine ca un ajutor de clasă Delphi (class helper) pentru TPdf, trăind în propria sa unitate, mai degrabă decât ca metode noi fixate în clasa TPdf. Un ajutor de clasă este o caracteristică a limbajului care vă permite să atașați metode la un 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ă ajutătoare se citește ca Pdf.MeasureTextWidth(...) fără a avea niciun obiect separat de construit sau de transmis

Motivul pentru a-l stratifica în acest fel este separarea. Tipul de bază TPdf rămâne așa cum este, fără niciun câmp adăugat și fără a atinge nicio semnătură existentă, deci un proiect care nu are nevoie niciodată de aspect nu poartă niciodată codul de măsurare. Un proiect care are nevoie de el adaugă o unitate la o clauză uses și metodele se aprind. Capacitatea devine opțională la granularitatea unei singure unități, care este cea mai curată modalitate de a extinde un tip pe care nu îl dețineți sau nu doriți să îl deranjaț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 multe ori în timp ce decideți un aspect, iar pagina trebuie să arate exact așa cum ar fi arătat dacă nu ați fi măsurat niciodată nimic. Tehnica care face acest lucru posibil este construirea unui obiect text, solicitarea dimensiunii sale și aruncarea lui î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, având în vedere numele fontului și dimensiunea. FPDFText_SetText setează șirul de caractere pe care îl poartă acel obiect. FPDFPageObj_GetBounds citește înapoi caseta de încadrare a obiectului. FPDFPageObj_Destroy eliberează obiectul. În mod crucial, nimic din acea secvență nu apelează API-ul de inserare pe pagină. Obiectul este creat, interogat și distrus izolat, astfel încât documentul rămâne neschimbat când funcția revine. Este o sondă de unică folosință a cărei singură ieșire sunt cele patru numere ale casetei sale de încadrare

Aceasta este modalitatea robustă de a o face, deoarece PDFium nu expune o lățime convenabilă de avans per glifă pe care ați putea-o însuma singur. Metricile glifelor depind de programul fontului, de codificare și de modul în care PDFium încarcă fața, și nu există niciun apel public care să vă înmâneze avansul fiecărui caracter dintr-un șir. Caseta de încadrare a unui obiect text real, pe de altă parte, este calculată de aceeași mașinărie care ar așeza glifele pentru desen, deci reflectă amploarea redată reală mai degrabă decât o aproximare. Construirea unui obiect de unică folosință și citirea limitelor acestuia este cea mai fiabilă măsurătoare pe care v-o poate oferi biblioteca

// 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 încadrare revine sub formă de 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 de utilizator PDF, unde o unitate este o a șaptezeci și doua parte dintr-un inch, același spațiu de coordonate în care poziționați textul pe pagină. Nu există nicio unitate ascunsă a dispozitivului și niciun pixel implicat în acest stadiu. O lățime de 36 înseamnă jumătate de inch de pagină, indiferent de rezoluția finală a randării

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, nu invers. Acest detaliu contează atunci când avansați un cursor în jos pe o coloană. Măsurați înălțimea unei linii, apoi o scădeți din linia de bază curentă pentru a o găsi pe următoarea, deoarece deplasarea în jos pe pagină înseamnă deplasarea spre un Y mai mic. Dacă destinația dvs. este un ecran, nu hârtie, convertiți unitățile de utilizator în pixeli ai dispozitivului cu rezoluția afișajului: o valoare în unități de utilizator înmulțită cu DPI și împărțită la 72 dă pixeli, astfel încât o lățime de coloană pe care o setați în puncte poate fi corelată cu un parcurs măsurat înainte de a decide unde are loc întreruperea

Ce se întâmplă la o intrare degenerată

Funcțiile sunt scrise pentru a eșua în tăcere. Dacă nu este deschis niciun document sau dacă obiectul text nu poate fi creat, rezultatul este o întindere zero, nu o excepție ridicată. Lățimea și înălțimea sunt inițializate la zero în partea de sus și sunt suprascrise numai după ce o casetă de încadrare a fost citită înapoi cu succes. Un șir de caractere 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

Această alegere menține simplă o buclă de măsurare, deoarece o buclă care rulează pe mii de cuvinte nu este locul pentru gestionarea excepțiilor la fiecare iterație. Costul este că apelantul suportă verificarea. O lățime zero este o santinelă, nu un fapt despre text, așa că codul care împarte la o lățime măsurată sau presupune o valoare pozitivă trebuie să se protejeze împotriva lui zero înainte de a se încrede în ea. Tratați zero ca „nu s-a putut măsura” și contractul este clar; ignorați-l și o intrare degenerată devine în tăcere un aspect cu o coloană de glife care se suprapun

O încadrare lacomă a cuvintelor construită pe măsurare

Cu o funcție de lățime în mână, încadrarea cuvintelor este o buclă scurtă și lacomă. Împărțiți paragraful în cuvinte, păstrați o linie curentă, iar pentru fiecare cuvânt măsurați care ar fi linia dacă ați adăuga acel cuvânt. Atâta timp cât linia de probă încă se potrivește cu lățimea coloanei, continuați să adăugați; când s-ar depăși, goliți linia curentă cu AddText și începeți una nouă cu cuvântul care nu s-a potrivit. Acumularea se face în întregime cu MeasureTextWidth, iar singurul lucru care ajunge vreodată pe 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<string>;
  Line, Trial: WideString;
  I: Integer;
  Y: Double;
begin
  Words := string(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(Line, Font, FontSize, X, Y);   // 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(Line, Font, FontSize, X, Y);      // flush the final line
end;

Bucla măsoară linia de probă mai degrabă decât măsurarea fiecărui cuvânt și însumarea sa, deoarece lățimea unei linii nu este suma lățimilor cuvintelor sale. Spațiile dintre cuvinte contribuie, iar un rând măsurat captează direct acest lucru. Regula lacomă, potriviți câte cuvinte permite coloana și rupeți-o la ultimul care se potrivește, este aceeași regulă care umple golul dintre un apel crud AddText și un paragraf real. Apelul de desenare nu a fost niciodată partea grea. Măsurarea care trebuie să-l preceadă este, și exact asta oferă funcția de ajutor

Unde se potrivește acest lucru

Măsurarea este stratul dintre generarea conținutului și redarea acestuia, deci se asociază în mod natural cu restul fluxului de lucru al unui document construit de la zero. Dacă asamblați pagini și plasați textul în primul rând, munca de bază se află în crearea de documente PDF de la zero cu componenta PDFium în Delphi, unde AddText și configurarea paginii sunt acoperite integral. Atunci când fontul pe care îl măsurați contează la fel de mult ca șirul de caractere, deoarece metricile depind de față, analizarea proprietăților fontului PDF cu componenta PDFium în Delphi arată modul în care biblioteca raportează informațiile despre font care conduc acele casete de încadrare. Ambele se bazează pe aceeași legătură, Componenta PDFium pentru Delphi și Lazarus, unde instrumentul ajutător de măsurare este livrat alături de API-urile pentru documente, pagini și text descrise pe acest blog