PDF teksto puslapis atveria simbolius ir dėžes, niekada — eilutes. PDFium Component sukuria vizualią eilutę, sugrupuodama simbolių dėžes, kurių vertikalūs centrai patenka į pusę sėklos simbolio aukščio, skenuodama į išorę nuo paspausto simbolio, kol tolerancija viršijama. Kiekvienas pasirinkimo kelias žiūryklėje iškviečia tą vieną pagalbininką, todėl pelė, klaviatūra ir kodas sutaria
Simptomas, atvedantis ieškoti šio sprendimo, — konkretus ir nemalonus. Vartotojas triskart spusteli pastraipą dviejų stulpelių ataskaitoje ir gauna pusę puslapio. Arba jie triskart spusteli lentelės ląstelę, ir pasirinkimas praryja visą eilutę plius puslapio numerį poraštėje. Žiūryklė nesugedusi; ji užduoda klausimą, į kurį failas negali atsakyti. PDF nėra jokios eilutės, kurią pasirinkti, ir bet kokia realizacija, apsimetanti kitaip, spėlioja. Šis straipsnis — apie tai, kaip padaryti spėjimą sąmoningą ir nuoseklų. Jei jums iš tikrųjų reikia teksto ištraukimo iš dokumento, žr. teksto ištraukimą iš PDF dokumentų su PDFium; jei kuriate teksto išdėstymą ir reikia plotmenų, žr. teksto matavimą ir žodžių laužymą. Čia tema siauresnė: nuspręsti, kur vizuali eilutė prasideda ir baigiasi, ir pasirinkti tiksliai tai
Kodėl PDF teksto puslapis neturi eilutės objektų?
Todėl, kad PDF turinio srautas aprašo piešimą, ne struktūrą. ISO 32000-1 §9.4 apibrėžia teksto objektą kaip BT / ET porą, laikančią pozicionavimo ir rodymo operatorius. §9.4.2 pozicionavimo operatoriai (Td, TD, Tm, T*) juda teksto matrica per puslapį, o §9.4.3 rodymo operatoriai (Tj, TJ, ', ") nudažo ženklus ten, kur ta matrica šiuo metu rodo. Niekas tame modelyje nesako „šis ženklų ruožas — eilutė“. Eilutė — tai, ką žmogus mato, kai piešimas baigtas
Generatoriai tai pablogina būdais, kurių negalite kontroliuoti. Lygiuota pastraipa gali būti išleista kaip vienas TJ masyvas kiekvienai eilutei, arba kaip vienas Tj kiekvienam žodžiui su aiškiu Tm prieš kiekvieną, arba kaip vienas rodymo veiksmas su rankiojimo koregavimais, nešančiais tarpus. Dviejų stulpelių išdėstymas gali išleisti kairį stulpelį nuo viršaus iki apačios ir tada dešinį stulpelį, arba gali juos supinti, jei generatorius ėjo per savo pačios vidinį objektų sąrašą kitokia tvarka. Simbolių seka, kurią PDFium jums paduoda, seka turinio srautą, o turinio srautas seka viską, ką generuojanti programa nusprendė daryti. Todėl dvi funkcijos, kurias iš tikrųjų gaunate, — FPDFText_CountChars, kuri praneša, kiek simbolių puslapis laiko, ir FPDFText_GetCharBox, kuri grąžina vieno simbolio ribojantį stačiakampį puslapio erdvėje. Tai — visas žalias žodynas. Viskas virš to — žodžiai, eilutės, pastraipos, stulpeliai — yra išvada, kurią jūs atliekate ant geometrijos
Kodėl CR ir LF aptikimas — neteisingas testas?
Todėl, kad simboliai, prieš kuriuos tikrintumėte, nepatikimai esami, o kai jie esami, jie nepatikimai jūsų. PDFium įterpia sintetinius simbolius į teksto puslapį, kad ištrauktas tekstas būtų skaitomas: tarpą, kur dvi eilutės vizualiai atskirtos, CR ar LF, kur kita eilutė prasideda nauja bazine linija. FPDFText_IsGenerated egzistuoja kaip tik tam, kad galėtumėte juos atskirti nuo simbolių, kilusių iš failo, o PDFium Component tai atveria kaip CharacterGenerated savybę
Padalykite pagal tuos simbolius, ir jūs paveldite kiekvieną sprendimą, kurį PDFium priėmė, juos sintezuodama. Griežtas eilutės lūžis suvyniotoje pastraipoje ir minkštas suvyniojimas atrodo identiškai po sintezės. Lentelės eilutė, kurią generatorius išleido ląstelė po ląstelės, gali negauti jokio lūžio tarp paskutinės ląstelės ir kitos eilutės pirmos ląstelės, nes bazinės linijos atsitiktinai pakankamai artimos. Tuo tarpu antraštė, po kurios eina pagrindinis tekstas kitu dydžiu, gali gauti du lūžius ten, kur žmogus mato vieną. Sugeneruoti simboliai — atvaizdavimo patogumas visam puslapio ištraukimui; jie — ne eilutės modelis, ir jie degraduoja kaip tik tuose dokumentuose, kur pasirinkimas svarbiausias
Simbolių dėžių grupavimas pagal vertikalų centrą
Patikimas signalas — geometrija. Paimkite simbolį, kurį vartotojas paspaudė, kaip sėklą, apskaičiuokite jo dėžės vertikalų centrą ir eikite į išorę abiem kryptimis, kol kaimyninės dėžės laiko savo vertikalius centrus tolerancijos ribose. PDFium Component naudoja pusę sėklos dėžės aukščio kaip šią toleranciją, su 0.5 puslapio vienetų grindimis, kad degeneruotos dėžės — taškas, plonas tarpas, ženklas su beveik nuliniu aukščiu dėže — nesutrauktų tolerancijos į nieką ir nenukirstų eilutės po vieno simbolio
function TPdfView.LineRangeAt(TxtPage: FPDF_TEXTPAGE; CharIndex: Integer;
out StartIndex, Count: Integer): Boolean;
var
Lo, Hi, Total: Integer;
SeedBox, Box: TPdfRectangle;
SeedYMid, BoxYMid, HalfH: Double;
begin
Result := False;
StartIndex := -1;
Count := 0;
Total := FPDFText_CountChars(TxtPage);
if (CharIndex < 0) or (CharIndex >= Total) then
Exit;
if FPDFText_GetCharBox(TxtPage, CharIndex, SeedBox.Left, SeedBox.Right,
SeedBox.Bottom, SeedBox.Top) = 0 then
Exit;
SeedYMid := (SeedBox.Top + SeedBox.Bottom) / 2;
HalfH := Abs(SeedBox.Top - SeedBox.Bottom) / 2;
if HalfH < 0.5 then // floor for degenerate boxes
HalfH := 0.5;
Lo := CharIndex;
Hi := CharIndex;
while Lo > 0 do
begin
if FPDFText_GetCharBox(TxtPage, Lo - 1, Box.Left, Box.Right,
Box.Bottom, Box.Top) = 0 then
Break;
BoxYMid := (Box.Top + Box.Bottom) / 2;
if Abs(BoxYMid - SeedYMid) > HalfH then
Break;
Dec(Lo);
end;
while Hi < Total - 1 do
begin
if FPDFText_GetCharBox(TxtPage, Hi + 1, Box.Left, Box.Right,
Box.Bottom, Box.Top) = 0 then
Break;
BoxYMid := (Box.Top + Box.Bottom) / 2;
if Abs(BoxYMid - SeedYMid) > HalfH then
Break;
Inc(Hi);
end;
StartIndex := Lo;
Count := Hi - Lo + 1;
Result := True;
end;
Trys detalės tame cikle pelnytai užima savo vietą. Tolerancija kildinama iš sėklos, ne iš konstantos, todėl 24pt antraštė gauna platų diapazoną, o 7pt išnašos tekstas — siaurą, ir nė vienas nepavagia simbolių iš kito. Palyginimas naudoja vertikalius centrus, ne bazines linijas ar dėžių viršūnes, kas laiko viršutinį indeksą, atskiro dydžio eilutę ar mišraus šrifto sakinį toje pačioje eilutėje kaip ir jo kaimynus. O nepavykęs FPDFText_GetCharBox nutraukia skenavimą, o ne praleidžiamas, nes simbolis be gaunamos geometrijos neduoda jokio įrodymo bet kuria kryptimi, o tęsimas po jo leistų ėjimui peršokti tikrą ribą, remiantis tolesniu simboliu
Kodėl kiekvienas pasirinkimo kelias turi dalintis vienu pagalbininku?
Todėl, kad trys kodo keliai, kurie kiekvienas realizuoja „eilutę“, nukryps, ir jie nukryps tyliai. PDFium Component, triguba spauda, Shift+Home, Shift+End ir viešasis SelectLineAt metodas — visi išsprendžia savo ribas per tą patį LineRangeAt iškvietimą. Triguba spauda ją sėkina iš pasirinkimo inkaro; shift klavišai ją sėkina iš pasirinkimo žymeklio ir juda tik tuo galu; SelectLineAt ją sėkina iš iškviečiančiojo pateikto simbolio indekso ir perduoda rezultatą SelectTextRange, tam pačiam intervalo tikrintuvui, kurį naudoja pelės kelias. Dubliuokite logiką vietoj to, ir gedimas — ne avarija, tai — lėtas dreifas. Kažkas suderina trigubos spaudos toleranciją, kad pataisytų ataskaitą su tanka linijų aukščiu, ir dabar Shift+End sustoja vienu simboliu anksčiau, nei triguba spauda sustoja toje pačioje pastraipoje. Vartotojas pasirenka eilutę su pele, praplečia ją klaviatūra ir stebi, kaip pasirinkimas sumažėja. Kadangi SelectLineAt maitina įprastą pasirinkimo vamzdyną, programinis pasirinkimas taip pat lieka nepriklausomas nuo to, ar pelės įvestis įjungta, ir vis tiek gauna intervalo patvirtinimą, perpiešimą ir OnSelectionChange pranešimą nemokamai
// Select the visual line under a client-space point, then read it back
procedure TForm1.SelectLineUnderCursor(X, Y: Integer);
var
CharIndex: Integer;
begin
CharIndex := PdfView1.CharacterIndexAtPos(X, Y, 6.0, 6.0);
if CharIndex < 0 then
Exit;
if PdfView1.SelectLineAt(PdfView1.CurrentPage, CharIndex) then
Memo1.Lines.Add(PdfView1.SelectedText);
end;
Atkreipkite dėmesį į tolerancijos argumentus CharacterIndexAtPos. Pataikymo testavimas turi savo laisvę, išreikštą puslapio vienetais, ir tai — atskiras rūpestis nuo eilutės tolerancijos. Spustelėjimas, patenkantis į tarpą tarp dviejų eilučių, išsprendžiamas į artimiausią simbolį tame diapazone; eilutės skenavimas tada vyksta nuo bet kurio simbolio, kuris pasirodė esąs tas. Per dosnios pataikymo tolerancijos padavimas į sėklą — vienas iš lengvesnių būdų pasirinkti eilutę, į kurią vartotojas nerodė
Dvi indeksų erdvės: simbolio indeksas ir teksto indeksas
Kai turite intervalą, atsispirkite pagundai jį naudoti kaip eilutės poslinkį. FPDFText_GetText grąžina puslapio tekstą kaip UTF-16 buferį, bet jo indeksai — ne ta pati indeksų erdvė kaip simbolių indeksai, naudojami FPDFText_GetCharBox ir FPDFText_CountChars. Anksčiau aptarti sugeneruoti simboliai sėdi teksto buferyje, užimdami simbolių lizdus be jokios naudingos geometrijos, ir dvi numeracijos nukrypsta viena nuo kitos per puslapį. Tiltai — FPDFText_GetTextIndexFromCharIndex ir FPDFText_GetCharIndexFromTextIndex, apgaubti PDFium Component kaip CharacterIndexToTextIndex ir TextIndexToCharacterIndex
var
TextStart, TextEnd: Integer;
begin
// char-index range from LineRangeAt -> offsets into the page text buffer
TextStart := Pdf.CharacterIndexToTextIndex(StartIndex);
TextEnd := Pdf.CharacterIndexToTextIndex(StartIndex + Count - 1);
if (TextStart >= 0) and (TextEnd >= TextStart) then
Caption := Pdf.Text(TextStart, TextEnd - TextStart + 1);
end;
Kryptis, kuri kanda skaudžiausiai, — atvirkštinė. Paieška, realizuota per ištrauktą eilutę, duoda jums teksto indeksus, o jų tiesioginis perdavimas dėžės ar pasirinkimo API tyliai adresuoja neteisingus simbolius, su klaida, augančia tolyn žemyn puslapiu. Konvertuokite su TextIndexToCharacterIndex, prieš bet kam geometriniam liečiant skaičių. Surogatinės poros prideda antrą, nepriklausomą poslinkio problemą virš šios, kuri aprašyta straipsnyje apie emoji, CJK ir surogatines poras
Kur euristika lūžta
Būkite sąžiningi su savimi dėl ribų, nes jos — realios ir pasiekiamos. Pasuktas tekstas — aiškiausias atvejis: simbolio dėžė — ašiai lygiuotas stačiakampis puslapio erdvėje, todėl 90 laipsnių pasuktam tekstui vienos vizualios eilutės dėžės turi vertikalius centrus, išsklaidytus per visą puslapį, ir skenavimas sustoja beveik iš karto. Tai, ką gaunate, — trumpas pasirinkimas, o ne neteisingas, kas — geresnis gedimo tipas, bet vis dar gedimas. Vertikalūs rašymo režimai elgiasi taip pat dėl tos pačios priežasties. Dviejų stulpelių išdėstymai veikia, kai stulpeliai vertikaliai pastumti vienas nuo kito, ir lūžta, kai jie nepastumti. Jei abu stulpeliai dalinasi bazinės linijos tinklu, dešiniojo stulpelio simboliai atsiduria tolerancijos ribose nuo kairio stulpelio eilutės, ir skenavimas eis tiesiai per tarpą, nes gryna geometrija ten neturi kur sustoti. Aptikimui reikia horizontalaus tarpo testo virš vertikalaus grupavimo, ir tarpo ribos pasirinkimas — savas sprendimas dėl to, kuriuose dokumentuose norite klysti. Mišrūs šrifto dydžiai — atvejis, kurį sėklos santykinė tolerancija tvarko gerai: 8pt kodo intarpas 11pt pagrindiniame tekste laiko savo centrą juostoje, o 24pt antraštė kitoje bazinėje linijoje netraukia pagrindinės eilutės į save
Čia aprašyta eilutės pasirinkimo semantika dalyvauja PDFium Component Delphi ir C++Builder platformoms, kartu su pataikymo testavimo, pasirinkimo intervalo ir teksto indekso API, naudojamomis pavyzdžiuose; produkto puslapyje pateikta pilna teksto puslapio ir pasirinkimo modelio dokumentacija