Technický článok

Meranie textu PDF pre rozloženie a zalamovanie slov v Delphi

Volanie, ktoré umiestni text na stránku PDF, je priamočiare. Funkcia AddText dostane reťazec, písmo, veľkosť a pozíciu, a glify sa objavia. Čo neurobí, je povedať vám, ako širok bude daný reťazec po nakreslení, a ani nerozdělí dlhý reťazec do viacerých riadkov. Jediné volanie nakreslí jeden úsek textu na jednu pozíciu. Ak je úsek širší ako stĺpec, do ktorého ste ho chceli umiestniť, jednoducho presahuje okraj a volanie kreslenia vás na to neupozorní. V momente, keď chcete odsek namiesto jedného popisku, chýbajúcim prvkom je šírka reťazca vo zvolenom písme a veľkosti, zmeraná pred jeho zápisom na stránku

Toto je klasický problém rozloženia. Aby ste zalomili odsek do stĺpca, musíte vedieť, slovo po slove, koľko horizontálneho priestoru každý kandidátsky riadok zaberie, a to pred akýmkoľvek kreslením. Zalamovanie slov je meracia slučka obalená okolo volania kreslenia, a binding, ktorý iba kreslí, vám dáva len druhú polovicu. Podpora merania textu v PDFium component uzatvára túto medzeru dvoma funkciami, MeasureText a MeasureTextWidth, ktoré hlásia vykreslený rozsah reťazca bez zanechania akejkoľvek stopy na stránke

Prečo je meranie class helper, nie nová metóda na TPdf

Podpora merania prichádza ako Delphi class helper pre TPdf, žijúci vo vlastnej jednotke, namiesto nových metód prilepených priamo do triedy TPdf. Class helper je jazykový prvok, ktorý vám umožňuje pripojiť metódy k existujúcemu typu zvonku jeho deklarácie. Keď je jednotka v scope, nové metódy sa volajú presne tak, akoby patrili triede, takže metóda helpera sa číta ako Pdf.MeasureTextWidth(...) bez samostatného objektu na vytvorenie alebo odovzdanie

Dôvodom vrstvenia týmto spôsobom je oddelenie. Základný typ TPdf zostáva ako je, bez pridania poľa a bez zmeny existujúcich signatúr, takže projekt, ktorý nikdy nepotrebuje rozloženie, nikdy nenasie kód merania. Projekt, ktorý ho potrebuje, pridá jednu jednotku do klauzuly uses a metódy sa aktivujú. Funkcia sa stáva voliteľnou na úrovni granularity jedinej jednotky, čo je najčistejší spôsob rozšírenia typu, ktorý nevlastníte alebo nechcete rušiť

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;

Meranie bez dotyku stránky

Meranie musí byť bez vedľajších účinkov. Musí hlásiť šírku bez zanechania čohokoľvek za sebou, pretože ho voláte mnohokrát pri rozhodovaní o rozložení a stránka musí vyzerať presne tak, ako by vyzerala, keby ste nikdy nemeria. Technika, ktorá to umožňuje, je vytvoriť textový objekt, opýtať sa ho na jeho veľkosť a zahodiť ho pred tým, ako je niekedy pripojený k stránke

Sekvencia pozostáva zo štyroch volaní PDFium. FPDFPageObj_NewTextObj vytvorí textový objekt pre dokument s daným názvom písma a veľkosťou. FPDFText_SetText nastaví reťazec, ktorý objekt nesie. FPDFPageObj_GetBounds načíta ohraničujúci obdĺžnik objektu. FPDFPageObj_Destroy objekt uvoľní. Kľúčové je, že nič v tejto sekvencii nevolá API na vkladanie do stránky. Objekt je vytvorený, dopytovaný a zničený v izolácii, takže dokument zostáva nezmenený po návrate funkcie. Je to jednorazová sonda, ktorej jediným výstupom sú štyri čísla jej ohraničujúceho obdĺžnika

Toto je robustný spôsob, ako to urobiť, pretože PDFium nevystavuje pohodlnú šírku postupu na glyf, ktorú by ste mohli sami sčítať. Metriky glyfov závisia od programu písma, od kódovania a od toho, ako PDFium načíta tvár, a neexistuje verejné volanie, ktoré by vám odovzdalo postup každého znaku v reťazci. Ohraničujúci obdĺžnik skutočného textového objektu, na druhej strane, je vypočítaný rovnakým mechanizmom, ktorý by rozložil glyfy pre kreslenie, takže odráža skutočný vykreslený rozsah namiesto aproximácie. Vytvorenie jedného jednorazového objektu a čítanie jeho hraníc je najspoľahlivejšie meranie, ktoré knižnica môže poskytnúť

// 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;

Súradnice a jednotky výsledku

Ohraničujúci obdĺžnik sa vracia ako štyri hrany — ľavá, spodná, pravá a horná — a dve dimenzie sa získajú odčítaním. Šírka je pravá mínus ľavá a výška je horná mínus spodná. Obe sú vyjadrené v používateľských jednotkách PDF, kde jedna jednotka je jedna sedemdesiatdvina palca, rovnaký súradnicový priestor, v ktorom umiestňujete text na stránku. V tejto fáze nie sú zahrnuté žiadne skryté jednotky zariadenia ani pixely. Šírka 36 znamená polovicu palca na stránke, bez ohľadu na konečné rozlíšenie vykreslenia

Vertikálna os beží tak, ako to PDF definuje, s Y narastajúcim nahor, čo je dôvod, prečo je výška horná mínus spodná a nie naopak. Tento detail je dôležitý, keď posúvate kurzor dole po stĺpci. Zmeriate výšku riadku, potom ju odčítate od aktuálnej základnej línie, aby ste našli ďalšiu, pretože pohyb dole po stránke znamená pohyb k menšiemu Y. Ak je vaším cieľom obrazovka namiesto papiera, prevádzate používateľské jednotky na pixely zariadenia pomocou rozlíšenia displeja: hodnota v používateľských jednotkách vynásobená DPI a vydelená 72 dáva pixely, takže šírku stĺpca nastavenú v bodoch môžete porovnať s nameraným úsekom pred rozhodnutím o umiestnení zlomenia

Čo sa stane pri degenerovanom vstupe

Funkcie sú napísané tak, aby zlyhali ticho. Ak nie je otvorený žiadny dokument alebo ak textový objekt nemôže byť vytvorený, výsledkom je nulový rozsah namiesto vyvolanej výnimky. Šírka a výška sú inicializované na nulu na začiatku a prepísané len vtedy, keď bol ohraničujúci obdĺžnik úspešne načítaný. Prázdny reťazec, chýbajúci dokument, písmo, ktoré knižnica nedokáže preložiť na objekt — každé z toho vracia nulu namiesto vyvolania výnimky

Tento výber udržiava meraciu slučku jednoduchou, pretože slučka, ktorá prechádza tisíckami slov, nie je miestom pre obsluhu výnimiek pri každej iterácii. Cena je, že volajúci nesie kontrolu. Nulová šírka je strážny znak, nie fakt o texte, takže kód, ktorý delí nameranú šírku alebo predpokladá kladnú hodnotu, musí chrániť pred nulou pred dôverou v ňu. Zaobchádzajte s nulou ako s „nedalo sa zmerať" a zmluva je jasná; ignorujte ju a degenerovaný vstup sa ticho stane rozložením so stĺpcom prekrývajúcich sa glyfov

Chamtivé zalamovanie slov postavené na meraní

S funkciou šírky v ruke je zalamovanie slov krátka chamtivá slučka. Rozdelíte odsek na slová, udržiavate aktuálny riadok a pre každé slovo zmeriate, aký by bol riadok, keby ste to slovo pridali. Kým sa skúšobný riadok zmestí do šírky stĺpca, pridávate ďalej; keď by pretiekol, vyprázdnite aktuálny riadok pomocou AddText a začnite nový so slovom, ktoré sa nezmestilo. Akumulácia sa robí výhradne pomocou MeasureTextWidth, a jediná vec, ktorá sa niekedy dostane na stránku, je riadok, ktorý ste už potvrdili, že sa zmestí

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;

Slučka meria skúšobný riadok, a nie každé slovo zvlášť a nespočítava, pretože šírka riadku nie je súčtom šírok jeho slov. Medzery medzi slovami prispievajú a nameraný úsek to zachytáva priamo. Chamtivé pravidlo — zmestiť čo najviac slov, koľko stĺpec dovolí, a zalomiť pri poslednom, ktoré sa zmestí — je rovnaké pravidlo, ktoré vypĺňa medzeru medzi surovou funkciou AddText a skutočným odsekom. Volanie kreslenia nikdy nebolo tou ťažkou časťou. Meranie, ktoré mu musí predchádzať, áno, a to je presne to, čo helper poskytuje

Kde toto zapadá

Meranie je vrstva medzi generovaním obsahu a jeho vykresľovaním, takže sa prirodzene páruje so zvyškom pracovného postupu tvorby dokumentu od začiatku. Ak zostavujete stránky a umiestňujete text, základy nájdete v článku tvorba PDF dokumentov od začiatku s PDFium componentom v Delphi, kde sú AddText a nastavenie stránky podrobne popísané. Keď písmo, ktoré meriate, má rovnaký význam ako reťazec, pretože metriky závisia od tváre, analýza vlastností písma PDF s PDFium componentom v Delphi ukazuje, ako knižnica hlási informácie o písme, ktoré poháňajú tieto ohraničujúce obdĺžniky. Oba stavajú na rovnakom bindingu — PDFium Component pre Delphi a Lazarus — kde helper merania sa dodáva spolu s API pre dokument, stránku a text popísanými na tomto blogu