Anropet som placerar text på en PDF-sida är okomplicerat. Du ger AddText en sträng, ett typsnitt, en storlek och en position, och glyferna visas. Vad det inte gör är att berätta hur bred strängen kommer att vara när den väl är ritad, och det bryter inte en lång sträng över flera rader. Ett enda anrop målar ett textstycke (run) på en position. Om textstycket är bredare än spalten du hade tänkt att det skulle passa in i, rinner det helt enkelt förbi kanten, och ingenting i ritanropet varnar dig. I samma ögonblick som du vill ha ett stycke snarare än en enda etikett, är den saknade pusselbiten bredden på en sträng i det valda typsnittet och storleken, mätt innan du bekräftar den mot sidan
Detta är det klassiska layoutproblemet. För att radbryta ett stycke till en spalt måste du veta, ord för ord, hur mycket horisontellt utrymme varje kandidatrad kommer att ta, och du måste veta det innan du ritar något. Radbrytning är en mätslinga omsluten kring ett ritanrop, och en bindning som enbart ritar ger dig den andra halvan. Stödet för textmätning i PDFium-komponenten täpper till den luckan med två funktioner, MeasureText och MeasureTextWidth, som rapporterar en strängs renderade storlek utan att göra ett märke på någon sida
Varför mätning är en klasshjälpare, inte en ny metod på TPdf
Stödet för mätning anländer som en Delphi-klasshjälpare (class helper) för TPdf, boende i en egen enhet, snarare än som nya metoder påbultade i TPdf-klassen. En klasshjälpare är en språkfunktion som låter dig fästa metoder på en existerande typ från utsidan av dess deklaration. När enheten väl är inom synhåll (in scope), anropas de nya metoderna exakt som om de tillhörde klassen, så en hjälparmetod läses som Pdf.MeasureTextWidth(...) utan att något separat objekt behöver konstrueras eller skickas runt
Anledningen till att lägga detta i lager är separation. Kärnan TPdf förblir som den är, utan att något fält läggs till och utan att någon befintlig signatur vidrörs, så ett projekt som aldrig behöver layout bär aldrig med sig koden för mätning. Ett projekt som behöver det lägger till en enhet i en uses-sats och metoderna tänds upp. Kapaciteten blir valfri (opt-in) med granulariteten hos en enda enhet, vilket är det renaste sättet att utöka en typ som du inte äger eller inte vill störa
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;
Att mäta utan att röra sidan
Mätningen måste vara fri från sidoeffekter. Den måste rapportera en bredd utan att lämna något efter sig, eftersom du anropar den många gånger medan du beslutar om en layout och sidan måste se exakt ut som om du aldrig hade mätt alls. Den teknik som gör detta möjligt är att bygga ett textobjekt, fråga det om dess storlek och kasta bort det innan det någonsin fästs på en sida
Sekvensen är fyra PDFium-anrop. FPDFPageObj_NewTextObj skapar ett textobjekt mot dokumentet, givet typsnittsnamnet och storleken. FPDFText_SetText sätter den sträng som objektet bär. FPDFPageObj_GetBounds läser tillbaka objektets avgränsningsram (bounding box). FPDFPageObj_Destroy frigör objektet. Kritiskt nog anropar inget i den sekvensen sidinfognings-API:n. Objektet skapas, förfrågas och förstörs isolerat, så dokumentet är oförändrat när funktionen returnerar. Det är en engångssond vars enda utdata är de fyra talen för dess avgränsningsram
Detta är det robusta sättet att göra det eftersom PDFium inte exponerar någon praktisk advance-bredd per glyf som du själv skulle kunna summera. Glyfmått beror på typsnittsprogrammet, på kodningen, och på hur PDFium läser in typsnittet (face), och det finns inget publikt anrop som ger dig bredden av varje enskilt tecken i en sträng. Avgränsningsramen för ett verkligt textobjekt, å andra sidan, beräknas av samma maskineri som skulle placera ut glyferna för ritning, så den återspeglar den faktiska renderade storleken i stället för en approximation. Att bygga ett engångsobjekt och läsa dess gränser är den mest pålitliga mätningen biblioteket kan ge
// 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 och enheter för resultatet
Avgränsningsramen kommer tillbaka som fyra kanter: vänster, botten, höger och topp, och de två dimensionerna fås genom subtraktion. Bredden är höger minus vänster och höjden är topp minus botten. Båda uttrycks i PDF-användarenheter (user units), där en enhet är en 72-dels tum, samma koordinatrymd där du positionerar text på sidan. Det finns ingen gömd enhetsenhet (device unit) och inga pixlar involverade i det här skedet. En bredd på 36 betyder en halv tum på sidan, oavsett den slutgiltiga renderingsupplösningen
Den vertikala axeln löper på det sätt PDF definierar den, med Y ökande uppåt, vilket är anledningen till att höjden är topp minus botten snarare än tvärtom. Den detaljen spelar roll när du flyttar fram en markör neråt i en spalt. Du mäter en rads höjd, sedan subtraherar du den från den aktuella baslinjen för att hitta nästa, eftersom att röra sig neråt på sidan betyder att röra sig mot ett mindre Y. Om ditt mål är en skärm snarare än papper, konverterar du användarenheter till enhetspixlar med visningsupplösningen: ett värde i användarenheter multiplicerat med DPI och dividerat med 72 ger pixlar, så en spaltbredd du sätter i punkter kan matchas mot ett uppmätt textstycke innan du bestämmer var radbrytningen ska hamna
Vad som händer vid ogiltig inmatning (degenerate input)
Funktionerna är skrivna för att misslyckas tyst. Om det inte finns något dokument öppet, eller om textobjektet inte kan skapas, blir resultatet noll i utsträckning snarare än att kasta ett undantag. Bredden och höjden initialiseras till noll i början och skrivs bara över när en avgränsningsram har lästs tillbaka med framgång. En tom sträng, ett saknat dokument, ett typsnitt som biblioteket inte kan lösa till ett objekt, var och en av dessa returnerar noll snarare än att kasta
Detta val håller en mätslinga enkel, eftersom en slinga som körs över tusentals ord inte är platsen för undantagshantering vid varje iteration. Kostnaden är att anroparen bär ansvaret för kontrollen. En nollbredd är ett sentinelvärde, inte ett faktum om texten, så kod som dividerar med en mätt bredd eller förutsätter ett positivt värde måste skydda sig mot noll innan den litar på det. Behandla noll som "kunde inte mäta" så är kontraktet tydligt; om du ignorerar det förvandlas en ogiltig inmatning tyst till en layout med en spalt av överlappande glyfer
En girig radbrytning byggd på mätningen
Med en breddfunktion till hands blir radbrytning en kort girig slinga (greedy loop). Du delar upp stycket i ord, upprätthåller en aktuell rad, och för varje ord mäter du vad raden skulle bli om du lade till det ordet. Så länge provraden fortfarande passar in i spaltbredden fortsätter du att lägga till; när den skulle flöda över, stänger (flush) du den aktuella raden med AddText och påbörjar en ny med ordet som inte fick plats. Ackumuleringen görs helt och hållet med MeasureTextWidth, och det enda som någonsin når sidan är en rad som du redan har bekräftat passar in
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;
Slingan mäter provraden i stället för att mäta varje ord och summera, eftersom bredden av en rad inte är summan av dess ords bredder. Mellanslagen mellan orden bidrar, och ett uppmätt textstycke fångar det direkt. Den giriga regeln: "passa in så många ord som spalten tillåter och bryt vid det sista som får plats", är samma regel som fyller tomrummet mellan ett naket AddText och ett riktigt stycke. Ritanropet var aldrig det svåra. Mätningen som måste föregå det är det, och det är exakt vad hjälparen tillhandahåller
Var detta passar in
Mätning är skiktet mellan att generera innehåll och att rendera det, så det paras naturligt med resten av ett arbetsflöde för dokument från grunden. Om du sammanställer sidor och placerar ut text från första början, finns grundarbetet i att skapa PDF-dokument från grunden med PDFium-komponenten i Delphi, där AddText och siduppsättning täcks till fullo. När typsnittet du mäter spelar lika stor roll som strängen, eftersom måtten beror på typsnittet (the face), visar hur du analyserar PDF-typsnittsegenskaper med PDFium-komponenten i Delphi hur biblioteket rapporterar typsnittsinformationen som driver dessa avgränsningsramar. Båda bygger på samma bindning, PDFium Component för Delphi och Lazarus, där mäthjälparen levereras vid sidan av de dokument-, sid- och text-API:er som beskrivs på den här bloggen