Tehnični članak

Merjenje PDF besedila za postavitev in prelom besed v Delphiju

Klic, ki postavi besedilo na PDF stran, je preprost. Metodi AddText podate niz, pisavo, velikost in položaj, in glifi se pojavijo. Kar pa ne stori, je, da bi vam povedala, kako širok bo ta niz, ko bo narisan, in ne prelomi dolgega niza v več vrstic. En sam klic nariše en potek besedila na eni poziciji. Če je potek širši od stolpca, v katerega naj bi se prilegal, preprosto steče čez rob, in nič v risalnem klicu vas na to ne opozori. V trenutku, ko želite odstavek namesto ene same oznake, je manjkajoči del širina niza v izbrani pisavi in velikosti, izmerjena preden ga postavite na stran

To je klasičen problem postavitve. Da bi prelomili odstavek v stolpec, morate vedeti, besedo za besedo, koliko vodoravnega prostora bo zavzela vsaka kandidatna vrstica, in to morate vedeti, preden karkoli narišete. Prelom besed je merilna zanka, ovita okoli risalnega klica, vezava, ki samo riše, pa vam da le drugo polovico. Podpora za merjenje besedila v komponenti PDFium to vrzel zapre z dvema funkcijama, MeasureText in MeasureTextWidth, ki poročata o izrisanem obsegu niza, ne da bi pri tem na katero koli stran narisali karkoli

Zakaj je merjenje razredni pomočnik in ne nova metoda na TPdf

Podpora za merjenje pride kot Delphi razredni pomočnik (class helper) za TPdf, ki živi v svoji lastni enoti, namesto kot nove metode, dodane v razred TPdf. Razredni pomočnik je jezikovna značilnost, ki vam omogoča, da metode pripnete na obstoječ tip od zunaj njegove deklaracije. Ko je enota v dosegu, se nove metode kličejo natanko tako, kot da bi bile del razreda, zato se metoda pomočnika bere kot Pdf.MeasureTextWidth(...) brez ločenega objekta, ki bi ga bilo treba zgraditi ali prenašati naokoli

Razlog za tako plastenje je ločevanje. Osnovni tip TPdf ostane tak, kot je, brez dodanega polja in brez dotika obstoječega podpisa, tako da projekt, ki nikoli ne potrebuje postavitve, nikoli ne nosi kode za merjenje. Projekt, ki jo potrebuje, doda eno enoto v stavek uses in metode zaživijo. Zmožnost postane prostovoljna na ravni ene same enote, kar je najčistejši način za razširitev tipa, ki ga ne posedujete ali ga nočete motiti

uses
  PDFium, FPdfView, FPdfEdit,
  FPdfMeasure;   // enota pomočnika; prinese MeasureText v doseg na TPdf

// Z enoto v dosegu se metode berejo kot člani TPdf:
var
  W, H: Double;
begin
  Pdf.MeasureText('Subtotal', 'Helvetica', 11, W, H);
  // W in H sta zdaj izrisana širina in višina v PDF uporabniških enotah
end;

Merjenje brez dotikanja strani

Merjenje mora biti brez stranskih učinkov. Poročati mora o širini, ne da bi kaj pustilo za sabo, saj ga kličete mnogokrat med določanjem postavitve, stran pa mora na koncu izgledati natanko tako, kot bi izgledala, če sploh ne bi nič merili. Tehnika, ki to omogoča, je, da zgradite tekstovni objekt, ga vprašate po velikosti in ga zavržete, še preden je sploh pritrjen na stran

Zaporedje je štiri klici PDFium. FPDFPageObj_NewTextObj ustvari tekstovni objekt na dokumentu, glede na dano ime pisave in velikost. FPDFText_SetText določi niz, ki ga ta objekt nosi. FPDFPageObj_GetBounds prebere nazaj mejno okence objekta. FPDFPageObj_Destroy objekt sprosti. Ključno je, da nič v tem zaporedju ne pokliče API-ja za vstavljanje na stran. Objekt je ustvarjen, poizvedovan in uničen v izolaciji, tako da je dokument nespremenjen, ko funkcija vrne rezultat. Gre za sondo za enkratno uporabo, katere edini izhod so štiri številke njenega mejnega okenca

To je zanesljiv način, ker PDFium ne izpostavi priročne širine predujma za posamezen glif, ki bi jo lahko sešteli sami. Metrike glifov so odvisne od programa pisave, kodiranja in od tega, kako PDFium naloži obraz pisave, javnega klica, ki bi vam vrnil predujem vsakega znaka v nizu, pa ni. Mejno okence resničnega tekstovnega objekta pa na drugi strani izračuna isti mehanizem, ki bi glife razporedil za risanje, zato odraža dejanski izrisani obseg in ne približka. Gradnja enega enkratnega objekta in branje njegovih mej je najzanesljivejša meritev, ki jo knjižnica lahko da

Diagram štirih klicev PDFium za MeasureText v Delphi, ki sondira zavržljiv besedilni objekt, ne da bi se dotaknil strani
MeasureText zgradi enkraten besedilni objekt, prebere njegov omejujoč okvir in ga uniči, tako da merjenje pusti dokument PDF nedotaknjen
// Oblika MeasureText, izražena glede na preverjene klice PDFium.
// Tekstovni objekt se zgradi, izmeri in uniči; nobena stran ni vpletena.
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);   // sonda zavržena, stran nedotaknjena
  end;
end;

Koordinate in enote rezultata

Mejno okence se vrne kot štirje robovi: levi, spodnji, desni in zgornji, obe dimenziji pa dobimo z odštevanjem. Širina je desna minus leva, višina pa zgornja minus spodnja. Obe sta izraženi v PDF uporabniških enotah, kjer je ena enota ena dvainsedemdesetinka palca, isti koordinatni prostor, v katerem besedilo postavljate na stran. V tej fazi ni nobene skrite enote naprave in nobenega piksla. Širina 36 pomeni pol palca strani, ne glede na končno ločljivost izrisa

Navpična os poteka tako, kot jo določa PDF, z Y, ki narašča navzgor, zato je višina zgornja minus spodnja in ne obratno. Ta podrobnost je pomembna, ko kazalko pomikate navzdol po stolpcu. Izmerite višino vrstice, nato jo odštejete od trenutne osnovne črte, da najdete naslednjo, ker pomik navzdol po strani pomeni pomik proti manjšemu Y. Če je vaš cilj zaslon namesto papirja, uporabniške enote pretvorite v slikovne pike naprave z ločljivostjo zaslona: vrednost v uporabniških enotah, pomnožena z DPI in deljena s 72, da slikovne pike, tako da lahko širino stolpca, določeno v točkah, primerjate z izmerjenim potekom, preden se odločite, kje bo prelom

Kaj se zgodi pri izrojenem vhodu

Funkciji sta napisani tako, da tiho odpovesta. Če ni odprtega dokumenta ali če tekstovnega objekta ni mogoče ustvariti, je rezultat ničelni obseg namesto sprožene izjeme. Širina in višina sta na začetku inicializirani na nič in prepisani šele, ko je mejno okence uspešno prebrano nazaj. Prazen niz, manjkajoč dokument, pisava, ki je knjižnica ne more razrešiti v objekt — vsak od teh primerov vrne nič namesto da bi sprožil izjemo

Ta izbira ohranja merilno zanko preprosto, ker zanka, ki teče čez tisoče besed, ni mesto za obravnavo izjem pri vsaki iteraciji. Cena je, da preverjanje nosi klicatelj. Ničelna širina je varovalna vrednost, ne dejstvo o besedilu, zato mora koda, ki deli z izmerjeno širino ali predpostavlja pozitivno vrednost, pred zaupanjem preveriti, ali je vrednost nič. Če ničlo obravnavate kot "ni bilo mogoče izmeriti", je pogodba jasna; če to prezrete, izrojen vhod tiho postane postavitev s stolpcem prekrivajočih se glifov

Požrešen prelom besed, zgrajen na merjenju

S funkcijo za širino v rokah je prelom besed kratka požrešna zanka. Odstavek razdelite na besede, hranite trenutno vrstico in za vsako besedo izmerite, kakšna bi bila vrstica, če bi to besedo dodali. Dokler poskusna vrstica še ustreza širini stolpca, jo naprej dodajate; ko bi prišlo do prekoračitve, trenutno vrstico izpišete s AddText in začnete novo z besedo, ki ni ustrezala. Seštevanje poteka izključno z MeasureTextWidth, na stran pa kadar koli pride le vrstica, za katero ste že potrdili, da ustreza

Diagram požrešne zanke prelivanja besed v Delphi, ki izmeri poskusne vrstice z MeasureTextWidth in prelomi pri zadnji prilagajoči besedi
Požrešna zanka ovijanja izmeri vsako preskusno vrstico proti širini stolpca in sprazni le vrstice, ki so potrjeno prave
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];
    // Izmerite kandidatno vrstico, preden karkoli narišete.
    if (Line <> '') and (Pdf.MeasureTextWidth(Trial, Font, FontSize) > ColumnWidth) then
    begin
      Pdf.AddText(Line, Font, FontSize, X, Y);   // izpiši vrstico, ki je ustrezala
      Y    := Y - LineHeight;                    // Y se zmanjšuje navzdol
      Line := Words[I];                          // beseda, ki ni ustrezala, začne naslednjo vrstico
    end
    else
      Line := Trial;
  end;
  if Line <> '' then
    Pdf.AddText(Line, Font, FontSize, X, Y);      // izpiši zadnjo vrstico
end;

Zanka meri poskusno vrstico namesto da bi merila vsako besedo posebej in jih seštevala, ker širina vrstice ni vsota širin njenih besed. Presledki med besedami prispevajo svoje, izmerjeni potek pa to zajame neposredno. Požrešno pravilo, spravite vanjo toliko besed, kot jih stolpec dovoljuje, in prelomite pri zadnji, ki še ustreza, je isto pravilo, ki zapolni vrzel med golim AddText in pravim odstavkom. Risalni klic nikoli ni bil težji del. Merjenje, ki mora priti pred njim, pa je, in prav to zagotavlja pomočnik

Kam se to umešča

Merjenje je plast med ustvarjanjem vsebine in njenim izrisom, zato se naravno poveže z ostalim potekom dela pri gradnji dokumenta od začetka. Če sestavljate strani in postavljate besedilo na novo, so temelji v članku ustvarjanje PDF dokumentov od začetka s komponento PDFium v Delphiju, kjer sta AddText in nastavitev strani obravnavana v celoti. Kadar je pisava, ki jo merite, prav tako pomembna kot niz sam, ker so metrike odvisne od obraza pisave, analiziranje lastnosti PDF pisav s komponento PDFium v Delphiju pokaže, kako knjižnica poroča o podatkih o pisavi, ki poganjajo ta mejna okenca. Oba gradita na isti vezavi, PDFium Component za Delphi in Lazarus, kjer je pomočnik za merjenje dobavljen skupaj z API-ji za dokument, stran in besedilo, opisanimi po tem blogu