Iškvietimas, kuris uždeda tekstą ant PDF puslapio, yra paprastas. Jūs duodate AddText eilutę (string), šriftą, dydį ir poziciją, ir atsiranda glifai (glyphs). Ko jis nepadaro, tai nepasako jums, kokio pločio bus ta eilutė, kai ji bus nupiešta, ir nesulaužo ilgos eilutės į kelias eilutes. Vienas iškvietimas nupiešia vieną teksto atkarpą (run of text) vienoje pozicijoje. Jei atkarpa yra platesnė nei stulpelis, į kurį norėjote ją sutalpinti, ji tiesiog išeina už krašto, ir niekas piešimo iškvietime jūsų neįspėja. Tą akimirką, kai norite pastraipos, o ne vienos etiketės (label), trūkstama detalė yra eilutės plotis pasirinktu šriftu ir dydžiu, išmatuotas prieš jums patvirtinant (commit) ją puslapyje
Tai klasikinė išdėstymo (layout) problema. Norėdami sulaužyti pastraipą į stulpelį, jūs turite žinoti, žodis po žodžio, kiek horizontalios vietos užims kiekviena kandidatinė eilutė, ir turite tai žinoti prieš bet ką piešdami. Žodžių laužymas (word wrap) yra matavimo ciklas, apvyniotas aplink piešimo iškvietimą, o susiejimas (binding), kuris tik piešia, suteikia jums antrąją pusę. Teksto matavimo palaikymas PDFium komponente užpildo šią spragą dviem funkcijomis – MeasureText ir MeasureTextWidth, kurios praneša atvaizduotos eilutės apimtį (rendered extent), nepadedant jokio ženklo jokiame puslapyje
Kodėl matavimas yra klasės pagalbininkas (class helper), o ne naujas metodas TPdf klasėje
Matavimo palaikymas pateikiamas kaip Delphi klasės pagalbininkas (class helper), skirtas TPdf, gyvenantis savo atskirame modulyje (unit), o ne kaip nauji metodai, prisukti prie TPdf klasės. Klasės pagalbininkas yra kalbos savybė, leidžianti prijungti metodus prie esamo tipo iš už jo deklaracijos ribų. Kai modulis yra matomumo srityje (in scope), nauji metodai iškviečiami lygiai taip pat, lyg jie priklausytų klasei, todėl pagalbinis metodas skaitomas kaip Pdf.MeasureTextWidth(...), ir nereikia konstruoti ar perduoti jokio atskiro objekto
Priežastis sluoksniuoti tai tokiu būdu yra atskyrimas (separation). Pagrindinis TPdf tipas lieka koks buvęs, nepridėjus jokio lauko ir nepalietus jokios esamos parašo (signature), todėl projektas, kuriam niekada nereikia išdėstymo (layout), niekada neneša matavimo kodo. Projektas, kuriam to reikia, prideda vieną modulį į uses sąlygą, ir metodai užsižiebia. Galimybė tampa pasirenkama (opt-in) vieno modulio lygmeniu (granularity), o tai yra švariausias būdas išplėsti tipą, kurio jūs nevaldote arba kurio nenorite trikdyti
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;
Matavimas neliečiant puslapio
Matavimas turi būti be šalutinių poveikių (side effects). Jis turi pranešti plotį nepalikdamas nieko po savęs, nes jūs iškviečiate jį daugybę kartų, kol sprendžiate dėl išdėstymo, ir puslapis turi atrodyti lygiai taip, kaip jis atrodytų, jei niekada nieko nebūtumėte matavę. Technika, padaranti tai įmanoma, yra sukurti teksto objektą, paklausti jo dydžio ir išmesti jį prieš tai, kai jis prijungiamas prie puslapio
Seka susideda iš keturių PDFium iškvietimų. FPDFPageObj_NewTextObj sukuria teksto objektą dokumente, nurodžius šrifto pavadinimą ir dydį. FPDFText_SetText nustato eilutę (string), kurią tas objektas neša. FPDFPageObj_GetBounds nuskaito objekto ribojantįjį stačiakampį (bounding box). FPDFPageObj_Destroy atlaisvina objektą. Svarbiausia, kad niekas šioje sekoje neiškviečia puslapio įterpimo API (page-insertion API). Objektas sukuriamas, apklausiamas ir sunaikinamas izoliuotai, todėl dokumentas lieka nepakitęs, kai funkcija grįžta. Tai yra išmetamas zondas (throwaway probe), kurio vienintelė išvestis (output) yra keturi jo ribojančiojo stačiakampio skaičiai
Tai yra tvirtas (robust) būdas tai padaryti, nes PDFium neatskleidžia patogaus kiekvieno glifo žingsnio pločio (advance width), kurį galėtumėte susumuoti patys. Glifų metrika priklauso nuo šrifto programos, nuo koduotės ir nuo to, kaip PDFium įkelia šriftą (loads the face), ir nėra jokio viešo iškvietimo, kuris duotų jums kiekvieno simbolio žingsnį eilutėje. Iš kitos pusės, tikro teksto objekto ribojantis stačiakampis yra apskaičiuojamas to paties mechanizmo, kuris išdėstytų glifus piešimui, todėl jis atspindi faktinę atvaizduojamą apimtį (actual rendered extent), o ne apytikslį įvertinimą (approximation). Sukurti vieną vienkartinį objektą ir nuskaityti jo ribas yra pats patikimiausias matavimas, kokį biblioteka gali pateikti
// 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;
Rezultato koordinatės ir vienetai
Ribojantis stačiakampis grįžta kaip keturios kraštinės: kairė, apačia, dešinė ir viršus, ir abu matmenys išgaunami atimant. Plotis yra dešinė atėmus kairę, o aukštis yra viršus atėmus apačią. Abu yra išreikšti PDF vartotojo vienetais (user units), kur vienas vienetas yra viena septyniasdešimt antroji colio (1/72) – ta pati koordinačių erdvė, kurioje jūs nustatote teksto padėtį puslapyje. Šiame etape nėra jokių paslėptų įrenginio vienetų (device unit) ir jokių pikselių. 36 plotis reiškia pusę colio puslapio, nesvarbu, kokia bus galutinė atvaizdavimo raiška (rendering resolution)
Vertikali ašis eina taip, kaip ją apibrėžia PDF – Y didėja į viršų, todėl aukštis yra viršus atėmus apačią, o ne atvirkščiai. Ši detalė svarbi, kai stumiate žymeklį (cursor) žemyn stulpeliu. Jūs išmatuojate eilutės aukštį, o tada atimate jį iš dabartinės bazinės linijos (baseline), kad rastumėte kitą, nes judėjimas puslapiu žemyn reiškia judėjimą link mažesnio Y. Jei jūsų paskirties vieta yra ekranas, o ne popierius, vartotojo vienetus paverčiate įrenginio pikseliais naudodami ekrano raišką: reikšmė vartotojo vienetais, padauginta iš DPI ir padalinta iš 72, duoda pikselius, todėl stulpelio plotį, kurį nustatėte taškais (points), galima palyginti su išmatuota atkarpa (measured run) prieš nusprendžiant, kur bus laužymas (break)
Kas nutinka esant išsigimusiai (degenerate) įvesčiai
Funkcijos parašytos taip, kad sugestų tyliai (fail quietly). Jei nėra atidaryto dokumento arba jei teksto objekto negalima sukurti, rezultatas yra nulinė apimtis (zero extent), o ne iššaukta išimtis (raised exception). Plotis ir aukštis pradžioje inicijuojami į nulį ir perrašomi tik tada, kai sėkmingai nuskaitomas ribojantis stačiakampis. Tuščia eilutė, trūkstamas dokumentas, šriftas, kurio biblioteka negali išspręsti į objektą – kiekvienas iš jų grąžina nulį, užuot išmetęs išimtį (throwing)
Šis pasirinkimas išlaiko matavimo ciklą paprastą, nes ciklas, kuris eina per tūkstančius žodžių, nėra ta vieta, kur reikėtų apdoroti išimtis (exception handling) kiekvienoje iteracijoje. Kaina yra ta, kad iškviestasis (caller) pats atlieka patikrinimą. Nulinis plotis yra kontrolinė reikšmė (sentinel), o ne faktas apie tekstą, todėl kodas, kuris dalija iš išmatuoto pločio arba daro prielaidą, kad tai yra teigiama reikšmė, turi apsisaugoti nuo nulio prieš pasitikėdamas juo. Traktuokite nulį kaip „nepavyko išmatuoti“ ir sutartis (contract) yra aiški; ignoruokite jį, ir išsigimusi įvestis (degenerate input) tyliai taps išdėstymu (layout) su stulpeliu persidengiančių glifų
Godus (greedy) žodžių laužymas, pastatytas ant matavimo
Turint pločio funkciją po ranka, žodžių laužymas (word wrap) yra trumpas godus ciklas (greedy loop). Jūs padalijate pastraipą į žodžius, išlaikote dabartinę eilutę ir kiekvienam žodžiui išmatuojate, kokia būtų eilutė, jei tą žodį pridėtumėte. Kol bandomoji eilutė (trial line) vis dar telpa į stulpelio plotį, jūs toliau pridedate; kai ji perpildytų (overflow) stulpelį, jūs ištuštinate (flush) dabartinę eilutę naudodami AddText ir pradedate naują su tuo žodžiu, kuris netilpo. Kaupimas (accumulation) atliekamas išimtinai su MeasureTextWidth, ir vienintelis dalykas, kuris kada nors pasiekia puslapį, yra eilutė, kurią jau patvirtinote, kad ji telpa
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;
Ciklas matuoja bandomąją eilutę, užuot matavęs kiekvieną žodį ir juos sumavęs, nes eilutės plotis nėra jos žodžių pločių suma. Tarpai tarp žodžių prisideda, ir išmatuota atkarpa tai užfiksuoja tiesiogiai. Godumo taisyklė (greedy rule) – sutalpinti tiek žodžių, kiek leidžia stulpelis, ir nutraukti ties paskutiniu, kuris telpa – yra ta pati taisyklė, kuri užpildo spragą tarp neapdoroto (raw) AddText ir tikros pastraipos. Piešimo iškvietimas niekada nebuvo sudėtingoji dalis. Matavimas, kuris turi eiti prieš jį, yra tas sunkumas, ir tai yra būtent tai, ką suteikia pagalbininkas (helper)
Kur tai tinka
Matavimas yra sluoksnis tarp turinio generavimo ir jo atvaizdavimo (rendering), todėl jis natūraliai dera su likusia dokumentų kūrimo nuo nulio darbo eiga (workflow). Jei iš pradžių surenkate puslapius ir dėliojate tekstą, pagrindai yra PDF dokumentų kūrimas nuo nulio naudojant PDFium komponentą Delphi aplinkoje, kur išsamiai aprašyti AddText ir puslapio sąranka (page setup). Kai šriftas, kurį matuojate, yra toks pat svarbus kaip ir pati eilutė (string), nes metrika priklauso nuo šrifto dizaino (face), PDF šriftų savybių analizė su PDFium komponentu Delphi aplinkoje rodo, kaip biblioteka praneša šrifto informaciją, kuri lemia tuos ribojančius stačiakampius (bounding boxes). Abu remiasi tuo pačiu susiejimu (binding), PDFium komponentu Delphi ir Lazarus, kuriame matavimo pagalbininkas pristatomas kartu su dokumentų, puslapių ir teksto API, aprašytais šiame tinklaraštyje