Kaldet, der sætter tekst på en PDF-side, er ligetil (straightforward). Du giver AddText en streng, en skrifttype, en størrelse og en position, og glyfferne vises. Hvad det ikke gør, er at fortælle dig, hvor bred den streng vil være, når den først er tegnet, og det bryder ikke en lang streng over flere linjer. Et enkelt kald maler én tekstkørsel (run of text) på én position. Hvis kørslen er bredere end den kolonne, du mente den skulle passe i, løber den simpelthen forbi kanten, og intet i tegningskaldet advarer dig. Det øjeblik du ønsker et afsnit snarere end en enkelt etiket (label), er den manglende brik bredden af en streng i den valgte skrifttype og størrelse, målt før du overgiver (commit) den til siden
Dette er det klassiske layout-problem. For at ombryde (wrap) et afsnit til en kolonne skal du vide, ord for ord, hvor meget vandret plads hver kandidatlinje vil tage, og du skal vide det, før du tegner noget. Ordombrydning (Word wrap) er en målingsløkke (measurement loop) svøbt omkring et tegningskald, og en binding, der kun tegner, giver dig den anden halvdel. Tekstmålingsunderstøttelsen i PDFium-komponenten lukker det hul med to funktioner, MeasureText og MeasureTextWidth, der rapporterer den gengivne udstrækning (rendered extent) af en streng uden at sætte et mærke på nogen side
Hvorfor måling er en class helper, ikke en ny metode på TPdf
Målingsunderstøttelsen ankommer som en Delphi class helper til TPdf, der lever i sin egen enhed (unit), i stedet for som nye metoder boltet ind i TPdf-klassen. En class helper er en sprogfunktion, der lader dig vedhæfte (attach) metoder til en eksisterende type udefra dens deklaration. Når enheden er i scope (i virkefelt), kaldes de nye metoder nøjagtigt som om de tilhørte klassen, så en hjælpemetode læses som Pdf.MeasureTextWidth(...) uden noget separat objekt at konstruere eller sende rundt
Grunden til at lagdele det på denne måde er adskillelse (separation). Kerne-TPdf-typen forbliver som den er, uden tilføjet felt og ingen eksisterende signatur berørt, så et projekt, der aldrig har brug for layout, bærer aldrig målingskoden. Et projekt, der har brug for den, tilføjer én enhed til en uses-sætning (clause), og metoderne lyser op (light up). Kapacitet bliver tilvalg (opt-in) ved granulariteten af en enkelt enhed, hvilket er den reneste måde at udvide en type, du ikke ejer eller ikke ønsker at forstyrre
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 uden at røre siden
Målingen skal være fri for bivirkninger. Den skal rapportere en bredde uden at efterlade noget, fordi du kalder den mange gange, mens du beslutter et layout, og siden skal se præcis ud, som den ville have gjort, hvis du slet aldrig havde målt. Teknikken, der gør dette muligt, er at bygge et tekstobjekt, bede det om dets størrelse, og smide det væk, før det nogensinde er knyttet (attached) til en side
Sekvensen er fire PDFium-kald. FPDFPageObj_NewTextObj opretter et tekstobjekt mod dokumentet, givet skrifttypens navn og størrelse. FPDFText_SetText indstiller den streng, som objektet bærer. FPDFPageObj_GetBounds læser objektets bounding box tilbage. FPDFPageObj_Destroy frigør objektet. Helt centralt (Crucially) kalder intet i den sekvens sideindsættelses-API'en (the page-insertion API). Objektet oprettes, forespørges og ødelægges isoleret, så dokumentet er uændret, når funktionen vender tilbage. Det er en engangs-sonde (throwaway probe), hvis eneste output er de fire tal for dens bounding box
Dette er den robuste måde at gøre det på, fordi PDFium ikke udstiller (expose) en praktisk pr.-glyf fremføringsbredde (advance width), som du selv kunne summere. Glyf-metrikker (Glyph metrics) afhænger af skrifttypeprogrammet (the font program), af kodningen (the encoding) og af, hvordan PDFium indlæser ansigtet (the face), og der er intet offentligt kald, der giver dig fremføringen af hvert tegn i en streng. Bounding boxen for et rigtigt tekstobjekt beregnes derimod af det samme maskineri, der ville lægge glyfferne ud til tegning, så den afspejler den faktiske gengivne udstrækning (rendered extent) frem for en tilnærmelse (approximation). At bygge ét engangsobjekt (disposable object) og læse dets grænser (bounds) er den mest pålidelige måling, biblioteket kan give
// 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 enheder for resultatet
Bounding boxen kommer tilbage som fire kanter, venstre, bund, højre og top, og de to dimensioner falder ud ved subtraktion. Bredde er højre minus venstre, og højde er top minus bund. Begge udtrykkes i PDF-brugerenheder (PDF user units), hvor en enhed er en tooghalvfjerdsindstyvende af en tomme, det samme koordinatrum, hvori du placerer tekst på siden. Der er ingen skjult enhedsenhed (device unit) og ingen pixel involveret på dette tidspunkt. En bredde på 36 betyder en halv tomme side, uanset den endelige renderings-opløsning (rendering resolution)
Den lodrette akse (The vertical axis) forløber (runs), som PDF definerer den, med Y stigende opad, hvilket er grunden til, at højden er top minus bund snarere end det omvendte. Den detalje betyder noget, når du fremfører en markør ned ad en kolonne. Du måler en linjes højde, og trækker den derefter fra den aktuelle grundlinje for at finde den næste, fordi at bevæge sig ned ad siden betyder at bevæge sig mod mindre Y. Hvis din destination er en skærm snarere end papir, konverterer du brugerenheder til enhedspixels med skærmopløsningen: en værdi i brugerenheder ganget med DPI'en og divideret med 72 giver pixels, så en kolonnebredde, du indstiller i punkter, kan matches mod en målt kørsel (measured run), før du beslutter, hvor bruddet går
Hvad der sker ved degenereret input
Funktionerne er skrevet til at fejle stille (fail quietly). Hvis der ikke er noget dokument åbent, eller hvis tekstobjektet ikke kan oprettes, er resultatet et nulomfang (zero extent) snarere end en rejst undtagelse (raised exception). Bredden og højden initialiseres til nul i toppen og overskrives kun, når en bounding box er læst tilbage med succes. En tom streng, et manglende dokument, en skrifttype biblioteket ikke kan løse (resolve) til et objekt, hver af disse returnerer nul snarere end at kaste (throwing)
Det valg holder en målingsløkke simpel, fordi en løkke, der kører over tusindvis af ord, ikke er stedet for undtagelseshåndtering (exception handling) ved hver iteration. Omkostningen er, at kalderen (the caller) bærer tjekket. En nulbredde (zero width) er en vagt (sentinel), ikke en kendsgerning om teksten, så kode, der dividerer med en målt bredde eller forudsætter en positiv værdi, skal vogte sig mod nul (guard against zero), før den stoler på den. Behandl nul som "kunne ikke måle", og kontrakten er klar; ignorer det, og et degenereret input bliver stille og roligt til et layout med en kolonne af overlappende glyffer
En grådig ordombrydning bygget på målingen
Med en breddefunktion i hånden er ordombrydning en kort grådig (greedy) løkke. Du deler afsnittet op i ord, beholder en aktuel linje, og for hvert ord måler du, hvad linjen ville være, hvis du tilføjede (appended) det ord. Mens prøvelinjen (the trial line) stadig passer til kolonnebredden, fortsætter du med at tilføje; når den ville flyde over (overflow), flusher (flush) du den aktuelle linje med AddText og starter en ny med det ord, der ikke passede. Akkumuleringen gøres udelukkende med MeasureTextWidth, og det eneste, der nogensinde når siden, er en linje, du allerede har bekræftet 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];
// 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;
Løkken måler prøvelinjen snarere end at måle hvert ord og summere, fordi bredden af en linje ikke er summen af bredderne af dens ord. Mellemrum mellem ord bidrager, og en målt kørsel (measured run) fanger det direkte. Den grådige regel (The greedy rule), at få plads til så mange ord som kolonnen tillader og bryde ved det sidste, der passer, er den samme regel, der udfylder kløften mellem en rå AddText og et rigtigt afsnit. Tegningskaldet var aldrig den svære del. Målingen, der skal gå forud for det, er, og det er præcis, hvad hjælperen (the helper) leverer
Hvor dette passer ind
Måling er laget mellem at generere indhold og gengive (rendering) det, så det parres naturligt med resten af et fra-bunden-dokument-workflow (from-scratch document workflow). Hvis du samler (assembling) sider og placerer tekst i første omgang, ligger forarbejdet i opbygning af PDF-dokumenter fra bunden med PDFium-komponenten i Delphi, hvor AddText og sideopsætning (page setup) er fuldt dækket. Når skrifttypen, du måler, betyder lige så meget som strengen, fordi metrikker afhænger af ansigtet (the face), viser analyse af PDF-skrifttypeegenskaber med PDFium-komponenten i Delphi, hvordan biblioteket rapporterer skrifttypeinformationen, der driver de bounding boxes. Begge bygger på den samme binding, PDFium-komponenten til Delphi og Lazarus, hvor målehjælperen (the measurement helper) leveres sammen med dokument-, side- og tekst-API'erne beskrevet på tværs af denne blog