Volání, které vloží text na stránku PDF, je přímočaré. Předáte AddText řetězec, písmo, velikost a pozici a znaky se objeví. Co však nedělá, je, že vám neřekne, jak široký ten řetězec po vykreslení bude, a nezalomí dlouhý řetězec přes několik řádků. Jedno volání vykreslí jeden běh textu na jedné pozici. Pokud je tento běh širší než sloupec, do kterého jste jej zamýšleli umístit, jednoduše přesáhne přes okraj a nic ve vykreslovacím volání vás nevaruje. V okamžiku, kdy chcete spíše odstavec než jeden štítek, chybějícím dílkem je šířka řetězce ve zvoleném písmu a velikosti, změřená dříve, než jej odešlete na stránku
Toto je klasický problém s rozvržením. Abyste zalomili odstavec do sloupce, musíte slovo od slova vědět, kolik horizontálního prostoru každý kandidátský řádek zabere, a musíte to vědět předtím, než cokoliv nakreslíte. Zalamování slov je smyčka měření obalená kolem volání kreslení a vazba, která pouze kreslí, vám dává tu druhou polovinu. Podpora pro měření textu v komponentě PDFium tuto mezeru zaplňuje pomocí dvou funkcí, MeasureText a MeasureTextWidth, které hlásí velikost řetězce po vykreslení, aniž by umístily jedinou značku na jakoukoliv stránku
Proč je měření pomocníkem třídy (class helper), a ne novou metodou v TPdf
Podpora měření přichází jako class helper v Delphi pro TPdf, který žije ve své vlastní jednotce, spíše než jako nové metody přimontované do třídy TPdf. Class helper je vlastnost jazyka, která vám umožňuje připojit metody k existujícímu typu zvenčí jeho deklarace. Jakmile je jednotka v rozsahu platnosti, nové metody se volají přesně tak, jako by patřily do třídy, takže metoda z pomocníka se čte jako Pdf.MeasureTextWidth(...), aniž by se musel konstruovat nebo předávat jakýkoliv samostatný objekt
Důvodem pro takové vrstvení je oddělení. Jádrový typ TPdf zůstává tak, jak je, není přidáno žádné pole a není dotčena žádná stávající signatura, takže projekt, který nikdy nepotřebuje rozvržení (layout), nikdy nenese kód pro měření. Projekt, který to potřebuje, přidá jednu jednotku do sekce uses a metody se aktivují. Tato schopnost je volitelná s granularitou jedné jednotky, což je nejčistší způsob, jak rozšířit typ, který nevlastníte nebo jej nechcete narušovat
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ěření bez dotyku stránky
Měření musí být bez vedlejších účinků. Musí nahlásit šířku, aniž by po sobě cokoliv zanechalo, protože ho voláte mnohokrát při rozhodování o rozvržení a stránka musí vypadat přesně tak, jak by vypadala, kdybyste nikdy nic neměřili. Technika, která to umožňuje, spočívá ve vytvoření textového objektu, dotazování na jeho velikost a jeho zahození ještě předtím, než by byl kdy připojen ke stránce
Tato sekvence je představována čtyřmi voláními PDFium. FPDFPageObj_NewTextObj vytvoří textový objekt vůči dokumentu s daným názvem a velikostí písma. FPDFText_SetText nastaví řetězec, který objekt nese. FPDFPageObj_GetBounds přečte zpět ohraničující obdélník (bounding box) objektu. FPDFPageObj_Destroy objekt uvolní. A to je klíčové: nic v této sekvenci nevolá API pro vkládání na stránku. Objekt je vytvořen, dotazován a zničen v izolaci, takže dokument se při návratu z funkce nemění. Jedná se o jednorázovou sondu na zahození, jejímž jediným výstupem jsou čtyři čísla jejího ohraničujícího obdélníku
Toto je ten robustní způsob, jak to udělat, protože PDFium nezpřístupňuje vhodnou šířku posunu (advance width) pro jednotlivé znaky, kterou byste si mohli sami sečíst. Metriky znaků závisí na programu písma, na kódování a na tom, jak PDFium načte font, a neexistuje žádné veřejné volání, které by vám předalo posun každého znaku v řetězci. Ohraničující obdélník skutečného textového objektu je naopak počítán stejným mechanismem, který by rozvrhl znaky pro kreslení, takže odráží skutečný rozsah po vykreslení namísto aproximace. Vytvoření jednoho objektu na jedno použití a přečtení jeho mezí je nejspolehlivější měření, které knihovna může poskytnout
// 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;
Souřadnice a jednotky výsledku
Ohraničující obdélník se vrací jako čtyři hrany: levá, dolní, pravá a horní, a tyto dva rozměry z toho vyplynou odčítáním. Šířka je pravá minus levá a výška je horní minus dolní. Obojí je vyjádřeno v uživatelských jednotkách PDF (PDF user units), kde jedna jednotka představuje jednu dvaasedmdesátinu palce, což je stejný prostor souřadnic, ve kterém umisťujete text na stránku. V této fázi není zapojena žádná skrytá jednotka zařízení ani žádný pixel. Šířka 36 znamená půl palce stránky bez ohledu na výsledné rozlišení vykreslování
Svislá osa probíhá způsobem, jakým ji definuje PDF, přičemž Y roste směrem nahoru, a to je důvod, proč je výška horní minus dolní spíše než naopak. Na tomto detailu záleží, když posunujete kurzor dolů ve sloupci. Změříte výšku řádku, a poté ji odečtete od aktuální účaří, abyste našli ten další, protože pohyb po stránce dolů znamená pohyb k menšímu Y. Pokud je vaším cílem obrazovka spíše než papír, převádíte uživatelské jednotky na pixely zařízení s rozlišením zobrazení: hodnota v uživatelských jednotkách vynásobená DPI a vydělená 72 dává pixely, takže šířka sloupce, kterou nastavíte v bodech, může být porovnána se změřeným během textu, než se rozhodnete, kam vložíte zalomení
Co se děje při degenerativním vstupu
Funkce jsou napsány tak, aby selhaly tiše. Pokud není otevřen žádný dokument, nebo pokud nelze textový objekt vytvořit, výsledkem je nulový rozsah spíše než vyvolání výjimky. Šířka a výška jsou inicializovány na nulu hned nahoře a jsou přepsány pouze tehdy, jakmile byl úspěšně načten zpět ohraničující obdélník. Prázdný řetězec, chybějící dokument, písmo, které knihovna nedokáže přeložit na objekt, z nichž každý vrací spíše nulu než vyhazování výjimek
Tato volba udržuje smyčku měření jednoduchou, protože smyčka, která běží přes tisíce slov, není místem pro zpracování výjimek v každé iteraci. Cenou za to je, že kontrolu na sobě nese volající. Nulová šířka je strážníkem, nikoliv faktem o textu, takže kód, který dělí naměřenou šířkou nebo předpokládá kladnou hodnotu, se musí chránit proti nule dříve, než jí začne důvěřovat. Berte nulu jako "nebylo možné změřit" a kontrakt je jasný; ignorujte to, a z degenerativního vstupu se potichu stane rozvržení se sloupcem překrývajících se znaků
Hltavé zalamování slov postavené na měření
S funkcí pro zjišťování šířky v ruce je zalamování slov krátkou hltavou (greedy) smyčkou. Rozdělíte odstavec na slova, udržujete si aktuální řádek a u každého slova měříte, jaký by řádek byl, pokud byste k němu to slovo připojili. Dokud se zkušební řádek stále vejde do šířky sloupce, přidáváte dál; když by řádek přetekl, spláchnete aktuální řádek přes AddText a začnete nový se slovem, které se už nevešlo. Akumulace probíhá výhradně pomocí MeasureTextWidth a jedinou věcí, která se vůbec kdy dostane na stránku, je řádek, u kterého už máte potvrzeno, že se do ní vejde
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;
Smyčka měří zkušební řádek spíše než aby měřila každé slovo a prováděla sčítání, protože šířka řádku se nerovná součtu šířek jeho slov. Přispívají i mezery mezi slovy a změřený běh to zachycuje přímo. Pravidlo hltavosti – umísti co nejvíce slov, kolik jich sloupec dovoluje, a zalom řádek po posledním, které se ještě vejde – je stejné pravidlo, které vyplňuje mezeru mezi surovým AddText a skutečným odstavcem. To volání na nakreslení vlastně nikdy nebylo tou těžkou částí. Měření, které mu musí předcházet, tou těžkou částí je, a to je přesně to, co pomocník poskytuje
Kam to zapadá
Měření je vrstvou mezi generováním obsahu a jeho vykreslením, takže se přirozeně spáruje se zbytkem workflow tvorby dokumentu od nuly. Pokud se zabýváte kompletací stránek a umístěním textu už na prvním místě, pak jsou základy popsány ve vytváření dokumentů PDF od nuly pomocí komponenty PDFium v Delphi, kde jsou volání AddText a nastavení stránky pokryty v plném rozsahu. Když u písma, které měříte, záleží na stejné míře jako u řetězce – protože metriky závisí na fontu – článek analýza vlastností písem PDF pomocí komponenty PDFium v Delphi ukazuje, jak knihovna vykazuje informace o písmu, ze kterých se řídí ony ohraničující obdélníky. Obojí staví na stejné vazbě, PDFium Component pro Delphi a Lazarus, kde je pomocník na měření dodáván společně s API pro dokument, stránku a text popsanými napříč tímto blogem