De aanroep die tekst op een PDF-pagina plaatst, is eenvoudig. U geeft AddText een tekenreeks, een lettertype, een grootte en een positie, en de tekens verschijnen. Wat het echter niet doet, is vertellen hoe breed die tekenreeks zal zijn als deze eenmaal is getekend, en het breekt een lange tekenreeks niet over meerdere regels af. Een enkele aanroep tekent één run van tekst op één positie. Als de run breder is dan de kolom waarin u het wilde laten passen, loopt het gewoon over de rand, en niets in de tekenaanroep waarschuwt u. Zodra u een alinea wilt in plaats van een enkel label, is het ontbrekende stukje de breedte van een tekenreeks in het gekozen lettertype en formaat, gemeten voordat u het aan de pagina toevoegt
Dit is het klassieke lay-outprobleem. Om een alinea in een kolom te laten teruglopen, moet u, woord voor woord, weten hoeveel horizontale ruimte elke kandidaatregel zal innemen, en dat moet u weten voordat u iets tekent. Woordterugloop is een meetlus verpakt rond een tekenaanroep, en een binding die alleen tekent, geeft u de tweede helft. De ondersteuning voor tekstmeting in de PDFium-component dicht die kloof met twee functies, MeasureText en MeasureTextWidth, die de gerenderde omvang van een tekenreeks rapporteren zonder een markering op een pagina achter te laten
Waarom meting een klasse-helper is, geen nieuwe methode op TPdf
De ondersteuning voor meting wordt geleverd als een Delphi klasse-helper voor TPdf, die in zijn eigen unit leeft, in plaats van als nieuwe methoden die in de TPdf klasse zijn vastgeschroefd. Een klasse-helper is een taalkenmerk waarmee u methoden kunt koppelen aan een bestaand type, van buiten de declaratie. Zodra de unit in scope is, worden de nieuwe methoden exact aangeroepen alsof ze tot de klasse behoren, dus een helpermethode leest als Pdf.MeasureTextWidth(...) zonder afzonderlijk object om te construeren of door te geven
De reden om het op deze manier in lagen op te bouwen is scheiding. Het kern TPdf type blijft zoals het is, zonder toegevoegd veld en zonder een bestaande handtekening aan te raken, 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. De functionaliteit 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 neveneffecten. Het moet een breedte rapporteren zonder iets achter te laten, omdat u het vele malen aanroept terwijl u beslist over een lay-out en de pagina er exact zo moet uitzien als wanneer u nooit had gemeten. De techniek die dit mogelijk maakt is door een tekstobject te bouwen, naar de grootte ervan te vragen en het weg te gooien voordat het ooit aan een pagina wordt gekoppeld
De reeks bestaat uit vier PDFium-aanroepen. FPDFPageObj_NewTextObj creëert een tekstobject voor het document, op basis van de naam en grootte van het lettertype. FPDFText_SetText stelt de tekenreeks in die dat object draagt. FPDFPageObj_GetBounds leest het begrenzingsvak van het object terug. FPDFPageObj_Destroy maakt het object vrij. Cruciaal hierbij is dat niets in die reeks de pagina-invoeg-API aanroept. Het object wordt in isolatie aangemaakt, opgevraagd en vernietigd, zodat het document ongewijzigd blijft wanneer de functie terugkeert. Het is een wegwerpsonde waarvan de enige uitvoer de vier getallen van het begrenzingsvak is
Dit is de robuuste manier om het te doen, omdat PDFium geen handige breedte-voortgang per glyph blootlegt die u zelf zou kunnen optellen. Glyph-statistieken hangen af van het lettertypeprogramma, de codering en hoe PDFium het lettertype laadt, en er is geen openbare aanroep die u de voortgang van elk teken in een tekenreeks overhandigt. Het begrenzingsvak van een echt tekstobject daarentegen wordt berekend door dezelfde mechanismen die de glyphs zouden opmaken om te tekenen, dus het weerspiegelt de daadwerkelijk gerenderde omvang in plaats van een benadering. Het bouwen van één wegwerpobject en het aflezen van de 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 begrenzingsvak komt terug als vier randen: links, onder, rechts en boven, en de twee dimensies vallen eruit door aftrekking. De breedte is rechts min links en de 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 positioneert. Er is geen verborgen apparaat-eenheid en er is in dit stadium geen pixel bij betrokken. Een breedte van 36 betekent een halve inch van de pagina, wat de uiteindelijke renderresolutie ook is
De verticale as loopt zoals PDF deze definieert, waarbij Y naar boven toeneemt, wat de reden is dat de hoogte boven min onder is in plaats van andersom. Dat detail is van belang wanneer u een cursor naar beneden door een kolom schuift. U meet de hoogte van een regel, trekt die vervolgens af van de huidige basislijn om de volgende te vinden, omdat naar beneden over de pagina bewegen betekent dat de Y kleiner wordt. Als uw bestemming een scherm is in plaats van papier, zet u gebruikerseenheden om in apparaatpixels met de schermresolutie: een waarde in gebruikerseenheden vermenigvuldigd met de DPI en gedeeld door 72 geeft pixels, dus een kolombreedte die u in punten instelt, kan worden vergeleken met een gemeten run voordat u beslist waar de afbreking komt
Wat er gebeurt bij gedegenereerde invoer
De functies zijn geschreven om stilletjes te falen. Als er geen document open is, of als het tekstobject niet kan worden aangemaakt, is het resultaat een nulomvang in plaats van een opgeworpen uitzondering. De breedte en hoogte worden bovenaan geïnitialiseerd op nul en pas overschreven zodra een begrenzingsvak succesvol is teruggelezen. Een lege tekenreeks, een ontbrekend document, een lettertype dat de bibliotheek niet in een object kan omzetten, elk van deze geeft nul terug in plaats van een fout op te werpen
Die keuze houdt een meetlus eenvoudig, omdat een lus die over duizenden woorden loopt, niet de plaats is voor het afhandelen van uitzonderingen bij elke iteratie. De kosten zijn dat de aanroeper de controle draagt. Een nulbreedte is een sentinel, geen feit over de tekst, dus code die door een gemeten breedte deelt of een positieve waarde veronderstelt, moet zich indekken tegen nul voordat deze wordt vertrouwd. Behandel nul als "kon niet meten" en het contract is duidelijk; negeer het en een gedegenereerde invoer wordt stilletjes een lay-out met een kolom overlappende glyphs
Een gretige woordterugloop gebouwd op de meting
Met een breedtefunctie in de hand is woordterugloop een korte, gretige lus. U splitst de alinea in woorden, houdt een huidige regel bij en voor elk woord meet u wat de regel zou zijn als u dat woord zou toevoegen. Zolang de testregel nog steeds binnen de kolombreedte past, blijft u toevoegen; als de regel overstroomt, schrijft u de huidige regel weg met AddText en start u een nieuwe regel 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<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;
De lus meet de testregel in plaats van elk woord te meten en de som te nemen, omdat de breedte van een regel niet de som is van de breedtes van de woorden. Spaties tussen woorden dragen bij, en een gemeten run legt dat direct vast. De gretige regel, pas zoveel woorden aan als de kolom toelaat en breek af bij het laatste woord dat past, is dezelfde regel die het gat opvult tussen een onbewerkte AddText en een echte alinea. De tekenaanroep was nooit het moeilijke deel. De meting die daaraan vooraf moet gaan wel, en dat is precies wat de helper biedt
Waar dit past
Meting is de laag tussen het genereren van inhoud en het renderen ervan, en past dus natuurlijk bij de rest van een from-scratch documentworkflow. Als u pagina's samenstelt en tekst plaatst, ligt de basis in PDF-documenten vanaf nul aanmaken met de PDFium-component in Delphi, waar AddText en pagina-instelling volledig aan bod komen. Wanneer het lettertype dat u meet net zo belangrijk is als de tekenreeks, omdat metrieken afhankelijk zijn van het lettertype, toont het analyseren van PDF-lettertype-eigenschappen met de PDFium-component in Delphi hoe de bibliotheek de lettertype-informatie rapporteert die die begrenzingsvakken aanstuurt. Beide bouwen voort op dezelfde binding, de PDFium-component voor Delphi en Lazarus, waar de meethelper wordt geleverd naast de document-, pagina- en tekst-API's die op deze blog worden beschreven