PDF stranica teksta izlaže znakove i okvire, nikad retke. PDFium Component gradi vizualni redak grupiranjem okvira znakova čija vertikalna središta padaju unutar pola visine sjemenskog znaka, pretražujući prema van iz kliknutog znaka dok se tolerancija ne premaši. Svaki put odabira u pregledniku poziva tog jednog pomoćnika, tako da se miš, tipkovnica i kod slažu
Simptom koji vas ovamo dovodi specifičan je i neugodan. Korisnik trostruko klikne odlomak u izvještaju s dva stupca i dobije pola stranice. Ili trostruko klikne stanicu tablice i odabir proguta cijeli redak plus broj stranice u podnožju. Preglednik nije pokvaren; postavlja pitanje na koje datoteka ne može odgovoriti. U PDF-u nema retka za odabrati, a svaka implementacija koja se pretvara drugačije nagađa. Ovaj se članak bavi činjenjem nagađanja namjernim i njegovim činjenjem konzistentnim. Ako vam zapravo treba izvlačenje teksta iz dokumenta, pogledajte izvlačenje teksta iz PDF dokumenata s PDFium-om; ako raspoređujete tekst i trebaju vam širine, pogledajte mjerenje teksta i prijelom riječi. Ovdje je predmet uži: odlučivanje gdje vizualni redak počinje i završava, i odabiranje upravo toga
Zašto PDF stranica teksta nema objekata redaka?
Zato što PDF sadržajni stream opisuje crtanje, ne strukturu. ISO 32000-1 §9.4 definira tekstualni objekt kao par BT / ET koji sadrži operatore pozicioniranja i prikazivanja. Operatori pozicioniranja iz §9.4.2 (Td, TD, Tm, T*) pomiču tekstualnu matricu po stranici, a operatori prikazivanja iz §9.4.3 (Tj, TJ, ', ") boje glifove gdje god ta matrica trenutno pokazuje. Ništa u tom modelu ne kaže "ovaj niz glifova je redak". Redak je ono što čovjek vidi nakon što je bojanje gotovo
Proizvođači ovo pogoršavaju na načine koje ne možete kontrolirati. Poravnat odlomak može se emitirati kao jedan niz TJ po retku, ili kao jedan Tj po riječi s eksplicitnim Tm prije svake, ili kao jedna operacija prikazivanja s prilagodbama kerninga koje nose razmak. Raspored s dva stupca može emitirati lijevi stupac odozgo prema dolje, a zatim desni stupac, ili ih može isprepliti ako je proizvođač prolazio kroz vlastiti interni popis objekata drugačijim redoslijedom. Slijed znakova koji PDFium predaje slijedi sadržajni stream, a sadržajni stream slijedi što god je aplikaciji koja generira odgovaralo. Tako su dvije funkcije koje zapravo dobivate FPDFText_CountChars, koja izvještava koliko znakova stranica sadrži, i FPDFText_GetCharBox, koja vraća granični okvir jednog znaka u prostoru stranice. To je cijeli sirovi rječnik. Sve iznad toga, riječi, retci, odlomci, stupci, je zaključivanje koje izvodite nad geometrijom
Zašto je otkrivanje CR i LF pogrešan test?
Zato što znakovi protiv kojih biste testirali nisu pouzdano prisutni, a kad su prisutni nisu pouzdano vaši. PDFium ubacuje sintetičke znakove u stranicu teksta kako bi izvučeni tekst bio čitljiv: razmak gdje su dva niza vizualno odvojena, CR ili LF gdje sljedeći niz počinje na novoj osnovnoj liniji. FPDFText_IsGenerated postoji upravo kako biste ih mogli razlikovati od znakova koji su izašli iz datoteke, a PDFium Component to izlaže kao svojstvo CharacterGenerated
Podijelite na tim znakovima i nasljedite svaku prosudbu koju je PDFium napravio dok ih je sintetizirao. Tvrdi prijelom retka unutar prelomljenog odlomka i mekani prijelom izgledaju identično nakon sinteze. Redak tablice koji je proizvođač emitirao stanicu po stanicu možda uopće nema prijelom između posljednje stanice i prve stanice sljedećeg retka, jer se osnovne linije slučajno dovoljno približe. U međuvremenu, naslov nakon kojeg slijedi tijelo teksta drugačije veličine može dobiti dva prijeloma gdje čovjek vidi jedan. Generirani znakovi su pogodnost renderiranja za izvlačenje cijele stranice; nisu model retka, i degradiraju upravo u dokumentima gdje je odabir najbitniji
Grupiranje okvira znakova po vertikalnom središtu
Pouzdan signal je geometrija. Uzmite znak koji je korisnik kliknuo kao sjeme, izračunajte vertikalno središte njegovog okvira, i idite prema van u oba smjera dok susjedni okviri drže svoja vertikalna središta unutar tolerancije. PDFium Component koristi pola visine sjemenskog okvira kao tu toleranciju, s podom od 0.5 jedinica stranice tako da degenerirani okviri, točka, tanak razmak, glif s gotovo nultom visinom okvira, ne sruše toleranciju na ništa i prekinu redak nakon jednog znaka
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;
Tri detalja u toj petlji zaslužuju svoje mjesto. Tolerancija se izvodi iz sjemena, a ne iz konstante, tako da naslov od 24pt dobiva širok pojas, a fusnota od 7pt dobiva uzak, i nijedan ne krade znakove od drugoga. Usporedba koristi vertikalna središta umjesto osnovnih linija ili vrhova okvira, što drži superskript, umetnuti niz drugačije veličine, ili mješovito-fontnu rečenicu na istom retku kao njeni susjedi. A neuspjeli FPDFText_GetCharBox prekida pretragu umjesto da bude preskočen, jer znak bez dohvatljive geometrije ne daje nikakav dokaz ni u jednom smjeru, a nastavljanje pored njega dopustilo bi obilasku da preskoči preko istinske granice na snazi znaka daljeg u nizu
Zašto svaki put odabira mora dijeliti jednog pomoćnika?
Zato što će se tri puta koda koji svaki implementira "redak" razići, i razići će se tiho. U PDFium Component, proširenje trostrukim klikom, Shift+Home, Shift+End, i javna metoda SelectLineAt svi razrješavaju svoje granice kroz isti poziv LineRangeAt. Trostruki klik postavlja sjeme iz sidra odabira; tipke shift postavljaju sjeme iz kursora odabira i pomiču samo taj kraj; SelectLineAt postavlja sjeme iz indeksa znaka koji dostavlja pozivatelj i predaje rezultat SelectTextRange, istom validatoru raspona koji koristi put miša. Duplicirajte logiku umjesto toga i neuspjeh nije rušenje, spor je zanos. Netko podesi toleranciju trostrukog klika kako bi popravio izvještaj s uskim proredom, i sad Shift+End stane jedan znak kraće od mjesta gdje trostruki klik staje na istom odlomku. Korisnik odabere redak mišem, proširi ga tipkovnicom, i gleda kako se odabir smanjuje. Jer SelectLineAt hrani obični pipeline odabira, programski odabir također ostaje neovisan o tome je li unos miša omogućen, i i dalje besplatno dobiva provjeru raspona, ponovno crtanje, i obavijest OnSelectionChange
// 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;
Primijetite argumente tolerancije na CharacterIndexAtPos. Hit testing ima vlastitu labavost, izraženu u jedinicama stranice, i to je odvojena briga od tolerancije retka. Klik koji sleti u prored između dva retka razrješava se na koji god znak je najbliži unutar tog okvira; pretraga retka zatim se pokreće od kojeg god se znaka to pokaže. Hranjenje presuviše velikodušne tolerancije pogotka u sjeme jedan je od lakših načina odabira retka na koji korisnik nije pokazivao
Dva prostora indeksa: indeks znaka i indeks teksta
Kad imate raspon, odolite porivu da ga koristite kao offset niza. FPDFText_GetText vraća tekst stranice kao UTF-16 međuspremnik, ali njegovi indeksi nisu isti prostor indeksa kao indeksi znakova koje koriste FPDFText_GetCharBox i FPDFText_CountChars. Generirani znakovi o kojima je bilo riječi ranije sjede u međuspremniku teksta dok zauzimaju utore znakova bez upotrebljive geometrije, a dva se numeriranja razilaze preko stranice. Mostovi su FPDFText_GetTextIndexFromCharIndex i FPDFText_GetCharIndexFromTextIndex, omotani u PDFium Component kao CharacterIndexToTextIndex i 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;
Smjer koji najviše grize je obrnuti. Pretraga implementirana nad izvučenim nizom daje vam indekse teksta, a njihovo izravno prosljeđivanje API-ju okvira ili odabira tiho adresira pogrešne znakove, s greškom koja raste što dalje idete niz stranicu. Pretvorite pomoću TextIndexToCharacterIndex prije nego išta geometrijsko dotakne broj. Parovi surogata dodaju drugi, neovisan problem offseta na vrh ovoga, pokriven u članku o emojijima, CJK-u i parovima surogata
Gdje se heuristika savija
Budite iskreni prema sebi o ograničenjima, jer su stvarna i dohvatljiva. Rotiran tekst je najjasniji slučaj: okvir znaka je osno poravnat pravokutnik u prostoru stranice, pa za tekst rotiran 90 stupnjeva okviri jednog vizualnog retka imaju vertikalna središta raspršena po stranici, a pretraga staje gotovo odmah. Ono što dobivate je kratak odabir umjesto pogrešnog, što je bolji način neuspjeha, ali je i dalje neuspjeh. Vertikalni načini pisanja ponašaju se isto iz istog razloga. Rasporedi s dva stupca rade kad su stupci vertikalno pomaknuti jedan od drugog, a lome se kad nisu. Ako oba stupca dijele mrežu osnovnih linija, znakovi iz desnog stupca sjede unutar tolerancije retka lijevog stupca, a pretraga će proći ravno preko jaza, jer u čistoj geometriji nema ničega ondje na čemu bi se zaustavila. Otkrivanje toga treba test horizontalnog jaza uz vertikalno grupiranje, a odabir praga jaza vlastita je prosudba o tome za koje dokumente ste voljni biti pogrešni. Mješovite veličine fontova su slučaj koji tolerancija relativna sjemenu dobro rješava: umetnuti raspon koda od 8pt unutar tijela teksta od 11pt drži svoje središte unutar pojasa, a naslov od 24pt na sljedećoj osnovnoj liniji ne uvlači redak tijela u sebe
Semantika odabira retka opisana ovdje isporučuje se u PDFium Component za Delphi i C++Builder, uz API hit testinga, raspona odabira i indeksa teksta korišten u primjerima; stranica proizvoda nosi punu referencu za model stranice teksta i odabira