De aanroep die tekst op een PDF-pagina plaatst, is eenvoudig. U geeft AddText een tekenreeks (string), een lettertype, een grootte en een positie, en de glyphs verschijnen. Wat het echter niet doet, is u vertellen hoe breed die tekenreeks zal zijn zodra deze is getekend, en het splitst een lange tekenreeks niet over meerdere regels. Een enkele aanroep schildert één tekstverloop op één positie. Als de tekst breder is dan de kolom waarin u hem wilde laten passen, loopt hij gewoon over de rand, en niets in de tekenaanroep waarschuwt u. Zodra u een paragraaf in plaats van een enkel label wilt, ontbreekt de breedte van een tekenreeks in het gekozen lettertype en de grootte, gemeten voordat u deze op de pagina vastlegt
Dit is het klassieke lay-outprobleem. Om een paragraaf in een kolom te laten teruglopen (word wrap), moet u woord voor woord weten hoeveel horizontale ruimte elke kandidaat-regel in beslag zal nemen, en u moet dit weten voordat u iets tekent. Tekstterugloop is een meetlus gewikkeld om een tekenaanroep, en een binding die alleen tekent, geeft u slechts de tweede helft. De ondersteuning voor tekstmeting in de PDFium component dicht die kloof met twee functies, MeasureText en MeasureTextWidth, die de gerenderde grootte van een tekenreeks rapporteren zonder een markering op een pagina aan te brengen
Waarom de meting een klasse-helper is, en geen nieuwe methode op TPdf
De meetondersteuning arriveert als een Delphi klasse-helper (class helper) voor TPdf, levend in zijn eigen unit, in plaats van als nieuwe methoden die in de TPdf klasse zijn vastgeschroefd. Een klasse-helper is een taalfunctie (language feature) waarmee u methoden aan een bestaand type kunt koppelen van buiten de declaratie. Zodra de unit binnen het bereik (in scope) is, worden de nieuwe methoden exact aangeroepen alsof ze tot de klasse behoren, dus een helpermethode leest als Pdf.MeasureTextWidth(...) zonder apart object om te construeren of door te geven
De reden om dit op deze manier te structureren, is scheiding. Het basis TPdf type blijft zoals het is, zonder toegevoegd veld en zonder aangepaste bestaande handtekening (signature), dus een project dat nooit lay-out nodig heeft, draagt nooit de meetcode mee. Een project dat het wel nodig heeft, voegt één unit toe aan een uses clausule en de methoden lichten op. Capaciteit wordt opt-in op de granulariteit van een enkele unit, wat de netste manier is om een type uit te breiden dat u niet bezit of niet wilt verstoren
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;
Meten zonder de pagina aan te raken
De meting moet vrij zijn van bijwerkingen (side effects). Het moet een breedte rapporteren zonder iets achter te laten, omdat u het vele malen aanroept terwijl u een lay-out bepaalt en de pagina er exact zo moet uitzien als wanneer u helemaal nooit had gemeten. De techniek die dit mogelijk maakt, is het bouwen van een tekstobject, de grootte ervan opvragen en het weggooien voordat het ooit aan een pagina wordt gekoppeld
De reeks bestaat uit vier PDFium-aanroepen. FPDFPageObj_NewTextObj creëert een tekstobject tegen het document, gegeven de naam en grootte van het lettertype. FPDFText_SetText stelt de tekenreeks in die het object draagt. FPDFPageObj_GetBounds leest het begrenzingskader (bounding box) van het object terug. FPDFPageObj_Destroy maakt het object vrij. Cruciaal is dat niets in die reeks de pagina-invoeg API aanroept. Het object wordt in isolatie gemaakt, opgevraagd en vernietigd, dus het document is ongewijzigd wanneer de functie terugkeert. Het is een wegwerpsensor waarvan de enige uitvoer de vier cijfers van het begrenzingskader zijn
Dit is de robuuste manier om het te doen, omdat PDFium geen handige opschuifbreedte (advance width) per glyph toont die u zelf zou kunnen optellen. De statistieken (metrics) van een glyph zijn afhankelijk van het lettertypeprogramma, de codering en van hoe PDFium het lettertype laadt, en er is geen openbare aanroep die u de opschuiving van elk teken in een tekenreeks overhandigt. Het begrenzingskader van een echt tekstobject wordt daarentegen berekend door dezelfde machinerie die de glyphs voor het tekenen zou opmaken, dus het weerspiegelt de werkelijke gerenderde grootte in plaats van een benadering. Het bouwen van één wegwerpobject en het aflezen van zijn grenzen is de meest betrouwbare meting die de bibliotheek kan geven
// 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;
Coördinaten en eenheden van het resultaat
Het begrenzingskader komt terug als vier randen: links, onder, rechts en boven, en de twee dimensies rollen eruit door aftrekken. Breedte is rechts min links en hoogte is boven min onder. Beide worden uitgedrukt in PDF-gebruikerseenheden, waarbij één eenheid een tweeënzeventigste van een inch is, dezelfde coördinatenruimte waarin u tekst op de pagina plaatst. In deze fase is er geen verborgen apparaateenheid (device unit) en geen pixel betrokken. Een breedte van 36 betekent een halve inch van de pagina, ongeacht de uiteindelijke renderresolutie
De verticale as loopt op de manier die PDF definieert, met Y die omhoog toeneemt, en daarom is hoogte boven min onder in plaats van het omgekeerde. Dat detail is van belang wanneer u een cursor in een kolom naar beneden verplaatst. U meet de hoogte van een regel, trekt deze vervolgens van de huidige basislijn af om de volgende te vinden, want naar beneden op de pagina verplaatsen betekent navigeren naar een kleinere Y. Als uw bestemming een scherm is in plaats van papier, converteert u de gebruikerseenheden naar apparaatpixels (device pixels) met de beeldschermresolutie: een waarde in gebruikerseenheden vermenigvuldigd met de DPI en gedeeld door 72 levert pixels op, zodat een kolombreedte die u in punten (points) hebt ingesteld kan worden vergeleken met een gemeten regel voordat u beslist waar de afbreking moet komen
Wat er gebeurt bij gedegenereerde invoer
De functies zijn geschreven om stilletjes te falen. Als er geen document is geopend, of als het tekstobject niet kan worden gemaakt, is het resultaat een nul-omvang in plaats van een opgeworpen uitzondering (exception). De breedte en hoogte worden bovenaan geïnitialiseerd op nul en worden pas overschreven zodra een begrenzingskader succesvol is teruggelezen. Een lege tekenreeks, een ontbrekend document, een lettertype dat de bibliotheek niet kan oplossen (resolve) naar een object, elk van deze retourneert nul in plaats van een fout te genereren
Die keuze houdt een meetlus eenvoudig, omdat een lus die over duizenden woorden loopt niet de plek is voor uitzonderingsafhandeling bij elke iteratie. De kosten hiervan zijn dat de aanroeper de controle draagt. Een breedte van nul is een schildwacht (sentinel), geen feit over de tekst, dus code die deelt door een gemeten breedte of een positieve waarde aanneemt, moet controleren op nul voordat deze erop kan vertrouwen. Behandel nul als "kan niet worden gemeten" en het contract is helder; negeer het en een gedegenereerde invoer wordt stilletjes een lay-out met een kolom van overlappende glyphs
Een gretige (greedy) tekstterugloop gebouwd op de meting
Met een breedtefunctie bij de hand is tekstterugloop (word wrap) een korte, gretige (greedy) lus. U splitst de paragraaf op in woorden, houdt een huidige regel bij en meet voor elk woord wat de regel zou worden als u dat woord eraan toevoegde. Zolang de testregel nog steeds in de kolombreedte past, blijft u toevoegen; als het overloopt, spoelt u (flush) de huidige regel door met AddText en begint u een nieuwe met het woord dat niet paste. Het verzamelen gebeurt volledig met MeasureTextWidth, en het enige dat ooit de pagina bereikt, is een regel waarvan u al hebt bevestigd dat deze past
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;
De lus meet de testregel in plaats van elk woord afzonderlijk te meten en deze op te tellen, omdat de breedte van een regel niet de som is van de breedtes van de losse woorden. Ruimtes tussen woorden spelen ook mee, en een gemeten volledige string legt dat direct vast. De gretige regel - voeg zoveel mogelijk woorden in als de kolom toelaat en verbreek bij het laatste passende woord - is dezelfde regel die de kloof vult tussen een ruwe AddText en een echte paragraaf. De tekenaanroep was nooit het moeilijke gedeelte. De meting die eraan vooraf moet gaan is dat wel, en dat is precies wat de helper biedt
Waar dit past
Meting is de laag tussen het genereren van content en het renderen ervan, dus het past op natuurlijke wijze bij de rest van een vanaf-nul documentworkflow. Als u pagina's samenstelt en tekst op de eerste plaats zet, ligt het fundament in het creëren van PDF-documenten vanaf nul met de PDFium component in Delphi, waar AddText en het opzetten van pagina's volledig aan bod komen. Wanneer het lettertype dat u meet evenveel uitmaakt als de tekenreeks, omdat de metrieken afhankelijk zijn van het gezicht, toont het analyseren van PDF-lettertype-eigenschappen met de PDFium component in Delphi hoe de bibliotheek de lettertype-informatie rapporteert die die begrenzingskaders aanstuurt. Beide bouwen voort op dezelfde binding, de PDFium Component voor Delphi en Lazarus, waar de meet-helper naast de document-, pagina- en tekst-API's wordt meegeleverd die elders op deze blog worden beschreven