Kallet som setter tekst på en PDF-side, er enkelt. Du gir AddText en streng, en font, en størrelse og en posisjon, og glyffene dukker opp. Det den ikke gjør, er å fortelle deg hvor bred den strengen blir når den først er tegnet, og den bryter ikke en lang streng over flere linjer. Ett enkelt kall maler én tekstkjøring på én posisjon. Er kjøringen bredere enn spalten du hadde tenkt den skulle passe i, løper den ganske enkelt forbi kanten, og ingenting i tegnekallet advarer deg. I det øyeblikket du vil ha et avsnitt i stedet for en enkelt etikett, er den manglende brikken bredden på en streng i den valgte fonten og størrelsen, målt før du forplikter den til siden
Dette er det klassiske oppsettsproblemet. For å bryte et avsnitt inn i en spalte må du vite, ord for ord, hvor mye vannrett plass hver kandidatlinje vil ta, og du må vite det før du tegner noe som helst. Orddeling er en måleløkke lagt rundt et tegnekall, og en binding som bare tegner, gir deg den andre halvparten. Tekstmålingsstøtten i PDFium-komponenten lukker det gapet med to funksjoner, MeasureText og MeasureTextWidth, som melder den gjengitte utstrekningen til en streng uten å sette et merke på noen side
Hvorfor måling er en class helper og ikke en ny metode på TPdf
Målestøtten kommer som en Delphi class helper for TPdf, som bor i sin egen unit, i stedet for som nye metoder boltet inn i TPdf-klassen. En class helper er en språkfunksjon som lar deg feste metoder på en eksisterende type utenfra dens egen erklæring. Så snart uniten er i omfang, kalles de nye metodene nøyaktig som om de hørte til klassen, så en hjelpemetode leses som Pdf.MeasureTextWidth(...) uten noe eget objekt å konstruere eller sende rundt
Grunnen til å lagdele det slik er atskillelse. Kjernetypen TPdf forblir som den er, uten et eneste felt lagt til og uten at noen eksisterende signatur er rørt, så et prosjekt som aldri trenger oppsett, bærer aldri målekoden. Et prosjekt som trenger den, legger én unit til en uses-klausul, og metodene tennes. Kapasitet blir noe du velger til, på granulariteten til én enkelt unit, som er den reneste måten å utvide en type du ikke eier eller ikke vil forstyrre
uses
PDFium, FPdfView, FPdfEdit,
FPdfMeasure; // hjelpe-uniten; gjør MeasureText tilgjengelig på TPdf
// Med uniten i omfang leses metodene som medlemmer av TPdf:
var
W, H: Double;
begin
Pdf.MeasureText('Subtotal', 'Helvetica', 11, W, H);
// W og H er nå den gjengitte bredden og høyden i PDF-brukerenheter
end;
Å måle uten å røre siden
Målingen må være fri for bivirkninger. Den må melde en bredde uten å legge igjen noe, fordi du kaller den mange ganger mens du avgjør et oppsett, og siden må se nøyaktig ut som den ville gjort om du aldri hadde målt i det hele tatt. Teknikken som gjør dette mulig, er å bygge et tekstobjekt, spørre det om størrelsen, og kaste det før det noen gang festes til en side
Sekvensen er fire PDFium-kall. FPDFPageObj_NewTextObj oppretter et tekstobjekt mot dokumentet, gitt fontnavnet og størrelsen. FPDFText_SetText setter strengen det objektet bærer. FPDFPageObj_GetBounds leser tilbake objektets avgrensningsboks. FPDFPageObj_Destroy frigjør objektet. Avgjørende er det at ingenting i den sekvensen kaller API-et for sideinnsetting. Objektet opprettes, spørres og ødelegges isolert, så dokumentet er uendret når funksjonen returnerer. Det er en engangssonde hvis eneste utdata er de fire tallene i avgrensningsboksen
Dette er den robuste måten å gjøre det på, fordi PDFium ikke eksponerer en bekvem framrykkbredde per glyff som du kunne summert selv. Glyffmetrikk avhenger av fontprogrammet, av kodingen og av hvordan PDFium laster inn skriftsnittet, og det finnes ikke noe offentlig kall som gir deg framrykket til hvert tegn i en streng. Avgrensningsboksen til et ekte tekstobjekt beregnes derimot av det samme maskineriet som ville satt opp glyffene for tegning, så den gjenspeiler den faktiske gjengitte utstrekningen snarere enn en tilnærming. Å bygge ett engangsobjekt og lese avgrensningen dets er den mest pålitelige målingen biblioteket kan gi
// Formen på MeasureText, uttrykt mot de verifiserte PDFium-kallene.
// Et tekstobjekt bygges, måles og ødelegges; ingen side er involvert.
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); // sonden forkastes, siden urørt
end;
end;
Koordinater og enheter i resultatet
Avgrensningsboksen kommer tilbake som fire kanter, venstre, bunn, høyre og topp, og de to dimensjonene faller ut av subtraksjon. Bredden er høyre minus venstre, og høyden er topp minus bunn. Begge uttrykkes i PDF-brukerenheter, der én enhet er en syttitodel av en tomme, det samme koordinatrommet du plasserer tekst i på siden. Det finnes ingen skjult enhetsenhet og ingen piksel involvert på dette stadiet. En bredde på 36 betyr en halv tomme side, uansett hvilken gjengivelsesoppløsning som til slutt brukes
Den loddrette aksen går slik PDF definerer den, med Y økende oppover, som er grunnen til at høyden er topp minus bunn og ikke omvendt. Den detaljen betyr noe når du flytter en markør nedover en spalte. Du måler en linjes høyde og trekker den så fra gjeldende grunnlinje for å finne den neste, fordi å bevege seg ned på siden betyr å bevege seg mot mindre Y. Er målet ditt en skjerm i stedet for papir, konverterer du brukerenheter til enhetspiksler med visningsoppløsningen: en verdi i brukerenheter multiplisert med DPI-en og delt på 72 gir piksler, så en spaltebredde du setter i punkter, kan holdes opp mot en målt kjøring før du bestemmer hvor bruddet skal gå
Hva som skjer med degenerert inndata
Funksjonene er skrevet for å feile stille. Finnes det ikke noe åpent dokument, eller lar tekstobjektet seg ikke opprette, blir resultatet en nullutstrekning i stedet for et reist unntak. Bredden og høyden initialiseres til null øverst og overskrives bare når en avgrensningsboks er lest tilbake med hell. 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
Det valget holder en måleløkke enkel, fordi en løkke som går over tusenvis av ord, ikke er stedet for unntakshåndtering ved hver iterasjon. Kostnaden er at kalleren bærer kontrollen. En nullbredde er en markørverdi, 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 den stoler på den. Behandle null som «kunne ikke måles», så er kontrakten klar; overse den, og en degenerert inndata blir stille til et oppsett med en spalte av overlappende glyffer
En grådig orddeling bygget på målingen
Med en breddefunksjon i hånden er orddeling en kort grådig løkke. Du deler avsnittet i ord, holder på en gjeldende linje, og for hvert ord måler du hva linjen ville blitt om du føyde til det ordet. Så lenge prøvelinjen fortsatt passer spaltebredden, fortsetter du å legge til; når den ville flyte over, tømmer du den gjeldende linjen med AddText og starter en ny med ordet som ikke fikk plass. Oppsamlingen gjøres utelukkende med MeasureTextWidth, og det eneste som noen gang når siden, er en linje du allerede har bekreftet passer
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];
// Mål kandidatlinjen før du tegner noe som helst.
if (Line <> '') and (Pdf.MeasureTextWidth(Trial, Font, FontSize) > ColumnWidth) then
begin
Pdf.AddText(Line, Font, FontSize, X, Y); // tøm linjen som passet
Y := Y - LineHeight; // Y minker nedover
Line := Words[I]; // ordet som fløt over starter neste linje
end
else
Line := Trial;
end;
if Line <> '' then
Pdf.AddText(Line, Font, FontSize, X, Y); // tøm den siste linjen
end;
Løkken måler prøvelinjen i stedet for å måle hvert ord og summere, fordi bredden på en linje ikke er summen av bredden på ordene i den. Mellomrommene mellom ordene bidrar, og en målt kjøring fanger det direkte. Den grådige regelen, få plass til så mange ord som spalten tillater og bryt ved det siste som passer, er den samme regelen som fyller gapet mellom et rått AddText og et virkelig avsnitt. Tegnekallet var aldri den vanskelige delen. Målingen som må komme før det, er det, og det er nøyaktig det hjelperen gir
Hvor dette passer inn
Måling er laget mellom det å generere innhold og det å gjengi det, så det passer naturlig sammen med resten av en arbeidsflyt for dokumenter bygget fra bunnen av. Setter du sammen sider og plasserer tekst i utgangspunktet, ligger grunnarbeidet i å lage PDF-dokumenter fra bunnen av med PDFium-komponenten i Delphi, der AddText og sideoppsett dekkes i sin helhet. Når fonten du måler betyr like mye som strengen, fordi metrikken avhenger av skriftsnittet, viser å analysere PDF-fontegenskaper med PDFium-komponenten i Delphi hvordan biblioteket melder fontinformasjonen som driver de avgrensningsboksene. Begge bygger på den samme bindingen, PDFium Component for Delphi og Lazarus, der målehjelperen følger med sammen med dokument-, side- og tekst-API-ene som er beskrevet gjennom denne bloggen