PDF tekstualna stranica izlaže znakove i okvire, nikad redove. PDFium Component gradi vizuelan red grupisanjem okvira znakova čiji vertikalni centri padaju unutar polovine visine sejanog znaka, skenirajući spolja od kliknutog znaka dok se tolerancija ne premaši. Svaka putanja selekcije u pregledaču poziva tog jednog pomoćnika, tako da se miš, tastatura i kod slažu
Simptom koji vas ovde dovodi je specifičan i neprijatan. Korisnik triput klikne pasus u izveštaju sa dve kolone i dobije pola stranice. Ili triput klikne ćeliju tabele i selekcija proguta ceo red plus broj stranice u fusnoti. Pregledač nije pokvaren; postavlja pitanje na koje fajl ne može odgovoriti. Nema reda u PDF-u za selektovanje, i bilo koja implementacija koja se pretvara drugačije nagađa. Ovaj članak se bavi time da nagađanje bude namerno i da bude konzistentno. Ako vam zapravo treba izvlačenje teksta iz dokumenta, pogledajte izvlačenje teksta iz PDF dokumenata sa PDFium; ako raspoređujete tekst i trebate širine, pogledajte merenje teksta i prelom reda. Ovde je tema uža: odlučivanje gde vizuelan red počinje i završava se, i selektovanje tačno toga
Zašto PDF tekstualna stranica nema objekte reda?
Zato što PDF tok sadržaja opisuje crtanje, ne strukturu. ISO 32000-1 §9.4 definiše tekstualni objekat kao par BT / ET koji sadrži operatore pozicioniranja i prikazivanja. Operatori pozicioniranja iz §9.4.2 (Td, TD, Tm, T*) pomeraju tekstualnu matricu po stranici, a operatori prikazivanja iz §9.4.3 (Tj, TJ, ', ") boje glifove gde god ta matrica trenutno pokazuje. Ništa u tom modelu ne kaže "ovaj niz glifova je red". Red je ono što čovek vidi pošto je bojenje završeno
Producenti ovo pogoršavaju na načine koje ne možete kontrolisati. Poravnat pasus se može emitovati kao jedan niz TJ po redu, ili kao jedan Tj po reči sa eksplicitnim Tm pre svake, ili kao jedna operacija prikazivanja sa podešavanjima kerninga koja nose razmak. Raspored sa dve kolone može emitovati levu kolonu odozgo naniže, a zatim desnu kolonu, ili ih može isprepletati ako je producent obišao svoju internu listu objekata drugačijim redosledom. Sekvenca znakova koju vam PDFium daje prati tok sadržaja, a tok sadržaja prati šta god je generišuća aplikacija htela da radi. Tako da su dve funkcije koje zapravo dobijate FPDFText_CountChars, koja prijavljuje koliko znakova stranica sadrži, i FPDFText_GetCharBox, koja vraća granični okvir jednog znaka u prostoru stranice. To je čitav sirov rečnik. Sve iznad toga, reči, redovi, pasusi, kolone, je zaključivanje koje vi izvodite nad geometrijom
Zašto je detekcija CR i LF pogrešan test?
Zato što znakovi naspram kojih biste testirali nisu pouzdano prisutni, a kad su prisutni, nisu pouzdano vaši. PDFium ubacuje sintetičke znakove u tekstualnu stranicu da učini izvučen tekst čitljivim: razmak gde su dva niza vizuelno razdvojena, CR ili LF gde sledeći niz počinje na novoj bazičnoj liniji. FPDFText_IsGenerated postoji tačno zato da ih razlikujete od znakova koji su izašli iz fajla, a PDFium Component ovo izlaže kao svojstvo CharacterGenerated
Podelite na tim znakovima i nasleđujete svaku procenu koju je PDFium napravio dok ih je sintetisao. Tvrd prelom reda unutar prelomljenog pasusa i mek prelom izgledaju identično posle sinteze. Red tabele koji je producent emitovao ćeliju po ćeliju možda ne dobije nikakav prelom između poslednje ćelije i prve ćelije sledećeg reda, jer se bazične linije slučajno dovoljno približe. U međuvremenu, naslov praćen telom teksta u drugačijoj veličini može dobiti dva preloma gde čovek vidi jedan. Generisani znakovi su pogodnost renderovanja za izvlačenje čitave stranice; oni nisu model reda, i degradiraju tačno u dokumentima gde je selekcija najbitnija
Grupisanje okvira znakova po vertikalnom centru
Pouzdan signal je geometrija. Uzmite znak koji je korisnik kliknuo kao seme, izračunajte vertikalni centar njegovog okvira, i idite spolja u oba pravca dok susedni okviri drže svoje vertikalne centre unutar tolerancije. PDFium Component koristi polovinu visine seed okvira kao tu toleranciju, sa podom od 0.5 jedinica stranice tako da degenerativni okviri, tačka, tanak razmak, glif sa okvirom skoro nulte visine, ne sruše toleranciju na ništa i preseku red posle 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 mesto. Tolerancija se izvodi iz semena, a ne iz konstante, tako da naslov od 24pt dobija širok pojas, a fusnota od 7pt uzak, i nijedan ne krade znakove drugom. Poređenje koristi vertikalne centre umesto bazičnih linija ili vrhova okvira, što drži superskript, umetnut niz drugačije veličine, ili rečenicu mešanog fonta na istom redu kao njegove susede. A neuspeo FPDFText_GetCharBox prekida skeniranje umesto da se preskoči, jer znak bez dostupne geometrije ne daje nikakav dokaz ni u jednu stranu, a nastavljanje pored njega bi dozvolilo obilasku da preskoči preko prave granice na osnovu znaka dalje niz put
Zašto svaka putanja selekcije mora deliti jednog pomoćnika?
Zato što će se tri putanje koda koje svaka implementira "red" razići, i razići će se tiho. U PDFium Component, proširenje trostrukog klika, Shift+Home, Shift+End, i javna metoda SelectLineAt svi razrešavaju svoje granice kroz isti poziv LineRangeAt. Trostruki klik postavlja seme iz sidra selekcije; shift tasteri postavljaju seme iz kursora selekcije i pomeraju samo taj kraj; SelectLineAt postavlja seme iz indeksa znaka koji dostavi pozivalac i predaje rezultat SelectTextRange, istom validatoru opsega koji koristi putanja miša. Duplirajte logiku umesto toga i otkaz nije pad, već spor drift. Neko podesi toleranciju trostrukog klika da popravi izveštaj sa uskim proredom, i sada Shift+End staje jedan znak kraće od mesta gde staje trostruki klik na istom pasusu. Korisnik selektuje red mišem, proširi ga tastaturom, i posmatra kako se selekcija smanjuje. Pošto SelectLineAt hrani običan pipeline selekcije, programska selekcija takođe ostaje nezavisna od toga da li je ulaz miša omogućen, i i dalje dobija validaciju opsega, repaint, i obaveštenje OnSelectionChange besplatno
// 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;
Primetite argumente tolerancije na CharacterIndexAtPos. Testiranje pogotka ima sopstvenu slabinu, izraženu u jedinicama stranice, i to je odvojena briga od tolerancije reda. Klik koji sleti u prored između dva reda razrešava se ka bilo kom znaku koji je najbliži unutar tog okvira; skeniranje reda se onda pokreće iz bilo kog znaka koji je to ispalo. Prosleđivanje previše velikodušne tolerancije pogotka u seme je jedan od lakših načina da selektujete red na koji korisnik nije pokazivao
Dva prostora indeksa: indeks znaka i indeks teksta
Čim imate opseg, odupriti se iskušenju da ga koristite kao offset string-a. FPDFText_GetText vraća tekst stranice kao UTF-16 bafer, ali njegovi indeksi nisu isti prostor indeksa kao indeksi znakova koje koriste FPDFText_GetCharBox i FPDFText_CountChars. Generisani znakovi ranije razmatrani sede u tekstualnom baferu dok zauzimaju slotove znakova bez upotrebljive geometrije, a dva brojanja se razilaze preko stranice. Mostovi su FPDFText_GetTextIndexFromCharIndex i FPDFText_GetCharIndexFromTextIndex, umotani od strane 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;
Smer koji najviše ujeda je obrnuti. Pretraga implementirana preko izvučenog string-a daje vam indekse teksta, a prosleđivanje tih direktno API-ju okvira ili selekcije tiho adresira pogrešne znakove, sa greškom koja raste što dalje idete niz stranicu. Konvertujte sa TextIndexToCharacterIndex pre nego što bilo šta geometrijsko dodirne broj. Surogatni parovi dodaju drugi, nezavisan problem offseta na ovo, pokriven u članku o emoji, CJK, i surogatnim parovima
Gde se heuristika savija
Budite iskreni prema sebi o ograničenjima, jer su stvarna i dostižna. Rotiran tekst je najjasniji slučaj: okvir znaka je osno poravnat pravougaonik u prostoru stranice, tako da za tekst rotiran 90 stepeni okviri jednog vizuelnog reda imaju vertikalne centre raštrkane po stranici, a skeniranje se zaustavlja skoro odmah. Ono što dobijate je kratka selekcija umesto pogrešne, što je bolji režim otkaza, ali je i dalje otkaz. Vertikalni režimi pisanja se ponašaju isto iz istog razloga. Rasporedi sa dve kolone rade kad su kolone vertikalno pomerene jedna u odnosu na drugu i lome se kad nisu. Ako obe kolone dele mrežu bazičnih linija, znakovi iz desne kolone sede unutar tolerancije reda leve kolone, a skeniranje će trčati pravo preko razmaka, jer u čistoj geometriji tu nema ničega na čemu bi se zaustavilo. Detekcija toga treba test horizontalnog razmaka na vrh vertikalnog grupisanja, a biranje praga razmaka je sopstvena procena o tome za koje dokumente ste voljni da pogrešite. Mešane veličine fonta su slučaj koji tolerancija relativna semenu dobro rukuje: umetnut kod raspon od 8pt unutar tela teksta od 11pt drži svoj centar unutar pojasa, a naslov od 24pt na sledećoj bazičnoj liniji ne povlači telo reda u sebe
Semantika selekcije reda opisana ovde isporučuje se u PDFium Component za Delphi i C++Builder, uz API testiranja pogotka, opsega selekcije, i indeksa teksta korišćenih u primerima; stranica proizvoda nosi kompletnu referencu za model tekstualne stranice i selekcije