Kallet som plasserer tekst på en PDF-side er rett frem. Du gir AddText en streng, en skrifttype, en størrelse og en posisjon, og tegnene (glyphs) vises. Det den ikke gjør, er å fortelle deg hvor bred den strengen vil være når den er tegnet, og den bryter ikke en lang streng over flere linjer. Et enkelt kall maler ett tekstavsnitt (run of text) på én posisjon. Hvis avsnittet er bredere enn kolonnen du mente den skulle passe inn i, renner den rett og slett forbi kanten, og ingenting i tegnekallet advarer deg. I det øyeblikket du ønsker et avsnitt i stedet for en enkel etikett, er den manglende brikken bredden til en streng i den valgte skrifttypen og størrelsen, målt før du binder deg til å plassere den på siden.
Dette er det klassiske layoutproblemet. For å bryte et avsnitt inn i en kolonne, må du vite, ord for ord, hvor mye horisontal plass hver kandidatlinje vil ta, og du må vite det før du tegner noe som helst. Orddeling (word wrap) er en målesløyfe pakket rundt et tegnekall, og en binding som bare tegner gir deg den siste halvdelen. Tekstmålingsstøtten i PDFium-komponenten lukker det gapet med to funksjoner, MeasureText og MeasureTextWidth, som rapporterer den gjengitte utstrekningen til en streng uten å sette et merke på noen side.
Hvorfor måling er en klassehjelper (class helper), ikke en ny metode på TPdf
Målestøtten kommer som en Delphi-klassehjelper (class helper) for TPdf, og ligger i sin egen enhet, i stedet for som nye metoder boltet fast i TPdf-klassen. En klassehjelper er en språkfunksjon som lar deg knytte metoder til en eksisterende type utenfor deklarasjonen. Når enheten er i omfang (in scope), kalles de nye metodene akkurat som om de tilhørte klassen, så en hjelpermetode leses som Pdf.MeasureTextWidth(...) uten noe separat objekt å konstruere eller sende rundt.
Grunner til å bygge det opp på denne måten er separasjon. Kjerne-TPdf-typen forblir som den er, uten tilføyde felt og uten berørte eksisterende signaturer. Et prosjekt som aldri trenger layout, bærer aldri målekoden med seg. Et prosjekt som trenger det, legger én enhet til en uses-klausul, og metodene lyser opp. Kapasitet blir tilvalg (opt-in) med granulariteten til en enkelt enhet, som er den reneste måten å utvide en type du ikke eier eller ikke ønsker å forstyrre på.
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åling uten å berøre siden
Målingen må være fri for bivirkninger. Den må rapportere en bredde uten å etterlate seg noe, fordi du kaller den mange ganger mens du bestemmer et layout, og siden må se nøyaktig ut som den ville gjort hvis du aldri hadde målt i det hele tatt. Teknikken som gjør dette mulig, er å bygge et tekstobjekt, be det om størrelsen, og kaste det bort før det noen gang er festet til en side.
Sekvensen er fire PDFium-kall. FPDFPageObj_NewTextObj oppretter et tekstobjekt mot dokumentet, gitt skrifttypenavnet og -størrelsen. FPDFText_SetText setter strengen som objektet bærer. FPDFPageObj_GetBounds leser tilbake objektets avgrensningsboks (bounding box). FPDFPageObj_Destroy frigjør objektet. Avgjørende er at ingenting i den sekvensen kaller API-et for sideinnsetting. Objektet blir opprettet, spurt ut og ødelagt isolert, så dokumentet forblir uendret når funksjonen returnerer. Det er en kaste-sonde hvis eneste utdata er de fire tallene i dens avgrensningsboks.
Dette er den robuste måten å gjøre det på, fordi PDFium ikke eksponerer en praktisk fremføringsbredde (advance width) per tegn som du selv kunne summere. Tegnmetrikk (glyph metrics) avhenger av fontprogrammet, av kodingen, og av hvordan PDFium laster inn ansiktet (face), og det er ikke noe offentlig kall som gir deg fremføringen til hvert tegn i en streng. Avgrensningsboksen til et ekte tekstobjekt, derimot, er beregnet av det samme maskineriet som ville lagt tegnene ut for tegning, så det reflekterer det faktiske gjengitte omfanget i stedet for en tilnærming. Å bygge ett engangsobjekt og lese av grensene er den mest pålitelige målingen biblioteket kan gi.
// 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;
Koordinater og enheter for resultatet
Avgrensningsboksen kommer tilbake som fire kanter: venstre, bunn, høyre og topp, og de to dimensjonene faller ut ved subtraksjon. Bredden er høyre minus venstre, og høyden er toppen minus bunnen. Begge uttrykkes i PDF-brukerenheter (user units), der én enhet er 1/72 tomme, det samme koordinatsystemet hvor du plasserer tekst på siden. Det er ingen skjult enhetsenhet og ingen piksler involvert på dette stadiet. En bredde på 36 betyr en halv tomme på siden, uavhengig av den endelige gjengivelsesoppløsningen.
Den vertikale aksen går slik PDF definerer den, der Y øker oppover, noe som er grunnen til at høyden er toppen minus bunnen og ikke omvendt. Den detaljen har betydning når du flytter en peker nedover en kolonne. Du måler høyden på en linje, og trekker den deretter fra gjeldende grunnlinje for å finne den neste, for det å flytte seg nedover siden betyr å bevege seg mot mindre Y-verdier. Hvis målet ditt er en skjerm snarere enn papir, konverterer du brukerenheter til enhetspiksler (device pixels) ved hjelp av skjermoppløsningen: en verdi i brukerenheter multiplisert med PPT (DPI) og delt på 72 gir piksler, slik at en kolonnebredde du angir i punkter (points) kan samsvare med en målt sekvens før du bestemmer hvor bruddet går.
Hva skjer med degenererte inndata
Funksjonene er skrevet for å feile stille. Hvis det ikke er noe dokument åpent, eller hvis tekstobjektet ikke kan opprettes, er resultatet et null-omfang snarere enn et hevet unntak (raised exception). Bredden og høyden blir initialisert til null på toppen og overskrives bare når en avgrensningsboks har blitt lest tilbake uten feil. En tom streng, et manglende dokument, en font biblioteket ikke kan løse opp til et objekt – hver av disse returnerer null i stedet for å kaste et unntak.
Dette valget holder en målesløyfe enkel, fordi en løkke som kjører over tusenvis av ord, ikke er stedet for unntakshåndtering på hver eneste iterasjon. Kostnaden er at den som kaller (the caller) må stå for sjekken. En null-bredde er en vaktpost (sentinel), ikke et faktum om teksten, så kode som deler på en målt bredde eller antar en positiv verdi, må gardere seg mot null før man stoler på den. Behandle null som "kunne ikke måle" og kontrakten er tydelig; ignorerer du den vil en degenerert inndataverdi (degenerate input) stille bli til en layout med en kolonne av overlappende tegn (glyphs).
En grådig orddeling bygd på målingen
Med en breddefunksjon for hånden, er orddeling en kort grådig løkke. Du deler avsnittet opp i ord, beholder en nåværende linje, og for hvert ord måler du hva linjen ville vært hvis du la til det ordet. Mens prøvelinjen fremdeles passer inn i kolonnebredden, fortsetter du å legge til; når den flyter over (overflow), flusher (flush) du den gjeldende linjen med AddText og starter en ny en med det ordet som ikke passet. Akkumuleringen utføres utelukkende med MeasureTextWidth, og det eneste som noen gang når siden er en linje du allerede har bekreftet at passer.
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;
Løkken måler prøvelinjen (trial line) i stedet for å måle hvert enkelt ord og summere, fordi bredden på en linje ikke er summen av ordenes bredder. Mellomrommene mellom ordene bidrar, og en målt gjennomgang fanger opp dette direkte. Den grådige regelen, få plass til så mange ord som kolonnen tillater og bryt ved det siste som passer, er den samme regelen som fyller gapet mellom en rå AddText og et skikkelig avsnitt. Tegnekallet var aldri den vanskelige delen. Målingen som må foregå i forkant er det, og det er nøyaktig hva hjelperen (the helper) leverer.
Hvor dette passer inn
Måling er laget mellom innholdsgenerering og gjengivelse, og passer derfor naturlig sammen med resten av en fra-grunnen-av (from-scratch) dokumentarbeidsflyt. Bygger du sider og plasserer tekst for første gang, finnes grunnarbeidet i oppretting av PDF-dokumenter fra bunnen av med PDFium-komponenten i Delphi, hvor AddText og sideoppsett (page setup) er dekket i sin helhet. Når fonten du måler har like stor betydning som strengen – ettersom metrikk (metrics) avhenger av skriftbildet – viser analyse av PDF-fontegenskaper med PDFium-komponenten i Delphi hvordan biblioteket rapporterer den fontinformasjonen som driver avgrensningsboksene. Begge bygger på samme binding, PDFium Component for Delphi og Lazarus, der målehjelperen (the measurement helper) leveres sammen med API-ene for dokument, side og tekst, som beskrives andre steder på denne bloggen.