Techninis straipsnis

PDF teksto matavimas išdėstymui ir žodžių laužymui „Delphi“ aplinkoje

Iškvietimas, kuris įkelia tekstą į PDF puslapį, yra paprastas. Perduodate AddText eilutę, šriftą, dydį ir poziciją, ir glifai atsiranda. Ko jis nepadaro – tai nepasako, kokio pločio bus ši eilutė ją nupiešus, ir neskaldo ilgos eilutės į kelias eilutes. Vienas iškvietimas nupiešia vieną teksto seką vienoje pozicijoje. Jei ši seka yra platesnė už stulpelį, į kurį norėjote ją sutalpinti, ji tiesiog išeina už krašto, ir jokio įspėjimo piešimo iškvietime nėra. Tą akimirką, kai jums reikia pastraipos, o ne pavienės etiketės, trūkstama detalė yra pasirinkto šrifto ir dydžio eilutės plotis, išmatuotas prieš ją įkeliant į puslapį

Tai klasikinė išdėstymo problema. Norėdami sutalpinti pastraipą į stulpelį, turite žinoti, žodis po žodžio, kiek horizontalios vietos užims kiekviena galima eilutė, ir tai turite žinoti prieš ką nors piešiant. Žodžių laužymas yra matavimo ciklas, apgaubiantis piešimo iškvietimą, o sąsaja, kuri tik piešia, suteikia jums tik antrąją pusę. Teksto matavimo palaikymas PDFium komponente užpildo šią spragą dviem funkcijomis – MeasureText ir MeasureTextWidth, kurios praneša atvaizduotos eilutės dydį nepalikdamos jokios žymės puslapyje

Kodėl matavimas yra klasės pagalbininkas (class helper), o ne naujas metodas TPdf klasėje

Matavimo palaikymas pateikiamas kaip „Delphi“ klasės pagalbininkas (class helper) skirtas TPdf, esantis atskirame modulyje (unit), o ne kaip nauji metodai, įmontuoti į pačią TPdf klasę. Klasės pagalbininkas yra kalbos funkcija, leidžianti prijungti metodus prie esamo tipo iš už jo deklaracijos ribų. Kai modulis įtraukiamas į aprėptį, naujieji metodai iškviečiami lygiai taip pat, lyg jie priklausytų klasei, todėl pagalbinis metodas skaitomas kaip Pdf.MeasureTextWidth(...), ir nereikia konstruoti ar perduoti jokio atskiro objekto

Priežastis, dėl kurios tai daroma tokiu sluoksniu, yra atskyrimas. Pagrindinis TPdf tipas lieka toks, koks buvęs, be pridėtų laukų ir nepalietus jokių esamų parašų, todėl projektas, kuriam niekada nereikia išdėstymo, niekada neįtraukia matavimo kodo. Projektas, kuriam to reikia, prideda vieną modulį į uses sąlygą, ir metodai tampa prieinami. Funkcionalumas pasirenkamas pagal vieno modulio detalumą, o tai yra švariausias būdas išplėsti tipą, kuris jums nepriklauso arba kurio nenorite keisti

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;

Matavimas neliečiant puslapio

Matavimas turi būti be šalutinio poveikio. Jis turi pranešti plotį nepalikdamas jokių pėdsakų, nes jūs jį iškviesite daug kartų spręsdami dėl išdėstymo, o puslapis turi atrodyti lygiai taip, lyg niekada nebūtumėte matavę. Technika, padaranti tai įmanoma, yra teksto objekto sukūrimas, jo dydžio užklausimas ir jo išmetimas prieš tai, kai jis prijungiamas prie puslapio

Seka susideda iš keturių PDFium iškvietimų. FPDFPageObj_NewTextObj sukuria teksto objektą dokumente, pateikus šrifto pavadinimą ir dydį. FPDFText_SetText nustato eilutę, kurią tas objektas neša. FPDFPageObj_GetBounds nuskaito objekto ribojantįjį stačiakampį (bounding box). FPDFPageObj_Destroy atlaisvina objektą. Svarbiausia, kad šioje sekoje niekas neiškviečia puslapio įterpimo API. Objektas sukuriamas, užklausiamas ir sunaikinamas izoliuotai, todėl dokumentas lieka nepakitęs, kai funkcija grąžina rezultatą. Tai yra vienkartinis zondas, kurio vienintelė išvestis yra keturi jo ribojančiojo stačiakampio skaičiai

Tai patikimas būdas tai padaryti, nes PDFium nepateikia patogaus kiekvieno glifo žingsnio pločio, kurį galėtumėte susumuoti patys. Glifų metrika priklauso nuo šrifto programos, koduotės ir nuo to, kaip PDFium įkelia šriftą, ir nėra viešo iškvietimo, kuris pateiktų kiekvieno eilutės simbolio žingsnį. Kita vertus, tikro teksto objekto ribojantysis stačiakampis yra apskaičiuojamas to paties mechanizmo, kuris išdėstytų glifus piešimui, todėl jis atspindi faktinį atvaizduojamą dydį, o ne apytikslį įvertinimą. Sukurti vieną vienkartinį objektą ir nuskaityti jo ribas yra patikimiausias matavimas, kurį gali suteikti biblioteka

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

Rezultato koordinatės ir vienetai

Ribojantysis stačiakampis grįžta kaip keturi kraštai: kairysis, apatinis, dešinysis ir viršutinis, o du matmenys gaunami atimant. Plotis yra dešinė minus kairė, o aukštis – viršus minus apačia. Abu yra išreikšti PDF vartotojo vienetais, kur vienas vienetas yra viena septyniasdešimt antroji colio dalis – toje pačioje koordinačių erdvėje, kurioje pozicionuojate tekstą puslapyje. Šiame etape nėra jokių paslėptų įrenginio vienetų ir pikselių. 36 pločio reikšmė reiškia pusę colio puslapio, nesvarbu, kokia bus galutinė atvaizdavimo raiška

Vertikali ašis eina taip, kaip apibrėžia PDF: Y didėja į viršų, todėl aukštis yra viršus minus apačia, o ne atvirkščiai. Ši detalė svarbi, kai perkeliate žymeklį žemyn stulpeliu. Išmatuojate eilutės aukštį, tada atimate jį iš dabartinės bazinės linijos, kad rastumėte kitą, nes judėjimas žemyn puslapiu reiškia judėjimą link mažesnio Y. Jei jūsų tikslas yra ekranas, o ne popierius, vartotojo vienetus paverčiate įrenginio pikseliais naudodami ekrano raišką: reikšmė vartotojo vienetais, padauginta iš DPI ir padalinta iš 72, duoda pikselius, todėl stulpelio plotis, nustatytas taškais, gali būti palygintas su išmatuota seka, prieš nusprendžiant, kur bus laužymas

Kas nutinka esant išsigimusiai (degeneratyviai) įvesčiai

Funkcijos parašytos taip, kad tyliai grąžintų klaidą. Jei nėra atidaryto dokumento arba teksto objekto sukurti nepavyksta, rezultatas yra nulinis dydis, o ne iškelta išimtis. Plotis ir aukštis yra inicijuojami nuliu pačioje pradžioje ir perrašomi tik tada, kai sėkmingai nuskaitomas ribojantysis stačiakampis. Tuščia eilutė, trūkstamas dokumentas, šriftas, kurio biblioteka negali paversti objektu – kiekvienas iš šių atvejų grąžina nulį, o ne iškelia išimtį

Šis pasirinkimas leidžia išlaikyti matavimo ciklą paprastą, nes ciklas, kuris apdoroja tūkstančius žodžių, nėra vieta išimčių apdorojimui kiekvienoje iteracijoje. Kaina yra ta, kad patikrą turi atlikti iškviečiantysis kodas. Nulinis plotis yra signalizatorius (sentinel), o ne faktas apie tekstą, todėl kodas, kuris dalija iš išmatuoto pločio arba daro prielaidą apie teigiamą reikšmę, turi apsisaugoti nuo nulio prieš pasikliaudamas juo. Traktuokite nulį kaip „nepavyko išmatuoti“, ir kontraktas bus aiškus; ignoruokite tai, ir išsigimusi įvestis tyliai taps išdėstymu su persidengiančių glifų stulpeliu

Godus (greedy) žodžių laužymas, sukurtas remiantis matavimu

Turint pločio funkciją, žodžių laužymas yra trumpas godus ciklas. Padalijate pastraipą į žodžius, palaikote dabartinę eilutę ir kiekvienam žodžiui išmatuojate, kokia būtų eilutė, jei tą žodį pridėtumėte. Kol bandomoji eilutė vis dar telpa į stulpelio plotį, tol pridedate toliau; kai ji perpildytų stulpelį, atvaizduojate dabartinę eilutę su AddText ir pradedate naują su netilpusiu žodžiu. Kaupimas visiškai atliekamas naudojant MeasureTextWidth, ir vienintelis dalykas, kuris pasiekia puslapį, yra ta eilutė, dėl kurios jau patvirtinote, kad ji telpa

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;

Ciklas matuoja bandomąją eilutę, o ne matuoja kiekvieną žodį atskirai ir juos sumuoja, nes eilutės plotis nėra jos žodžių pločių suma. Tarpai tarp žodžių turi įtakos, ir išmatuota seka tai tiesiogiai užfiksuoja. Godžioji taisyklė – sutalpinti tiek žodžių, kiek leidžia stulpelis, ir perkelti po paskutiniojo tilpusiojo – yra ta pati taisyklė, kuri užpildo spragą tarp pliko AddText ir tikros pastraipos. Piešimo iškvietimas niekada nebuvo sudėtingoji dalis. Matavimas, kuris turi eiti prieš jį, yra toks, ir būtent tai suteikia pagalbininkas

Kur tai pritaikoma

Matavimas yra sluoksnis tarp turinio generavimo ir jo atvaizdavimo, todėl jis natūraliai dera su likusia dokumentų kūrimo nuo nulio darbo eiga. Jei nuo pat pradžių surenkate puslapius ir dedate tekstą, pagrindai pateikti straipsnyje PDF dokumentų kūrimas nuo nulio su PDFium komponentu „Delphi“ aplinkoje, kur išsamiai aptariamas AddText ir puslapio sąranka. Kai šriftas, kurį matuojate, yra toks pat svarbus kaip ir pati eilutė, nes metrika priklauso nuo šrifto, straipsnis PDF šrifto savybių analizė su PDFium komponentu „Delphi“ aplinkoje parodo, kaip biblioteka pateikia šrifto informaciją, kuri nulemia tuos ribojančiuosius stačiakampius. Abu yra pagrįsti ta pačia sąsaja – PDFium komponentu, skirtu „Delphi“ ir „Lazarus“, kur matavimo pagalbininkas pateikiamas kartu su dokumentų, puslapių ir teksto API, aprašytais visame šiame tinklaraštyje