Egy PDF szövegoldal karaktereket és dobozokat tesz közzé, sorokat soha. A PDFium Component úgy épít fel egy vizuális sort, hogy összeklaszterezi azokat a karakterdobozokat, amelyek függőleges középpontja a magkarakter magasságának felén belül esik, kifelé pásztázva a kattintott karaktertől, amíg a tolerancia túl nem lépődik. A megjelenítő minden kijelölési útvonala ezt az egy segédfüggvényt hívja, így az egér, a billentyűzet és a kód egyetért egymással
A tünet, amely ide vezet, konkrét és kellemetlen. Egy felhasználó hármas kattintást tesz egy bekezdésre egy kéthasábos jelentésben, és megkapja a fél oldalt. Vagy hármas kattintást tesz egy táblacellára, és a kijelölés lenyeli az egész sort, plusz az oldalszámot a lábjegyzetben. A megjelenítő nem hibás; olyan kérdést tesz fel, amelyre a fájl nem tud válaszolni. Nincs sor egy PDF-ben, amelyet ki lehetne jelölni, és bármely megvalósítás, amely úgy tesz, mintha lenne, csak találgat. Ez a cikk arról szól, hogyan tegyük tudatossá ezt a találgatást, és hogyan tegyük konzisztenssé. Ha valójában szöveg kihúzására van szükségünk egy dokumentumból, lásd a szöveg kinyerése PDF dokumentumokból PDFium-mal cikket; ha szöveget helyezünk el, és szélességekre van szükségünk, lásd a szövegmérés és sortördelés cikket. Itt a téma szűkebb: eldönteni, hol kezdődik és ér véget egy vizuális sor, és pontosan azt kijelölni
Miért nincs sorobjektuma egy PDF szövegoldalnak?
Mert egy PDF tartalomfolyam rajzolást ír le, nem struktúrát. Az ISO 32000-1 §9.4 egy szövegobjektumot BT / ET párként definiál, amely pozicionáló és megjelenítő operátorokat tartalmaz. A §9.4.2 pozicionáló operátorai (Td, TD, Tm, T*) egy szövegmátrixot mozgatnak az oldalon, a §9.4.3 megjelenítő operátorai (Tj, TJ, ', ") pedig glifákat festenek oda, ahova az a mátrix éppen mutat. Semmi ebben a modellben nem mondja azt, hogy "ez a glifasorozat egy sor". Egy sor az, amit egy ember lát, miután a festés megtörtént
A generátorok olyan módokon rontják ezt tovább, amelyeket nem tudunk befolyásolni. Egy sorkizárt bekezdés kiadható soronként egy TJ tömbként, vagy szavanként egy Tj-ként egy explicit Tm-mel mindegyik előtt, vagy egyetlen megjelenítési műveletként, ahol kerning-korrekciók hordozzák a térközt. Egy kéthasábos elrendezés kiadhatja a bal hasábot felülről lefelé, majd a jobb hasábot, vagy összefésülheti őket, ha a generátor a saját belső objektumlistáját más sorrendben járta be. A karaktersorozat, amelyet a PDFium ad nekünk, a tartalomfolyamot követi, a tartalomfolyam pedig azt követi, amit a generáló alkalmazás éppen tenni akart. Így a két függvény, amelyet valójában kapunk, az FPDFText_CountChars, amely jelenti, hány karaktert tartalmaz az oldal, és az FPDFText_GetCharBox, amely visszaadja egy karakter határoló dobozát az oldal terében. Ez a teljes nyers szókincs. Minden efölött, szavak, sorok, bekezdések, hasábok, olyan következtetés, amelyet mi végzünk a geometrián
Miért rossz teszt a CR és LF felismerése?
Mert a karakterek, amelyek ellen tesztelnénk, nem megbízhatóan vannak jelen, és amikor jelen vannak, nem megbízhatóan a mieink. A PDFium szintetikus karaktereket szúr be a szövegoldalba, hogy a kinyert szöveg olvasható legyen: egy szóközt, ahol két futam vizuálisan elválik, egy CR-t vagy LF-et, ahol a következő futam egy új alapvonalon kezdődik. Az FPDFText_IsGenerated pontosan azért létezik, hogy meg tudjuk különböztetni ezeket a fájlból származó karakterektől, a PDFium Component pedig CharacterGenerated tulajdonságként teszi közzé
Ha ezeken a karaktereken szét választunk, örököljük minden ítéletet, amelyet a PDFium hozott, miközben szintetizálta őket. Egy kemény sortörés egy tördelt bekezdésen belül és egy puha tördelés a szintetizálás után egyformán néz ki. Egy táblasor, amelyet a generátor cellánként adott ki, esetleg egyáltalán nem kap törést az utolsó cella és a következő sor első cellája között, mert az alapvonalak történetesen elég közel vannak egymáshoz. Eközben egy címsor, amelyet más méretű törzsszöveg követ, két törést kaphat, ahol egy ember egyet lát. A generált karakterek egy renderelési kényelem a teljes oldalas kinyeréshez; nem egy sormodell, és pontosan azokban a dokumentumokban romlanak el, ahol a kijelölés a legjobban számít
Karakterdobozok klaszterezése függőleges középpont szerint
A megbízható jel a geometria. Vegyük a felhasználó által kattintott karaktert magként, számítsuk ki a dobozának függőleges középpontját, és haladjunk kifelé mindkét irányban, amíg a szomszédos dobozok tartják a függőleges középpontjukat a tolerancián belül. A PDFium Component a magdoboz magasságának felét használja toleranciaként, egy 0.5 oldalegység alsó korláttal, hogy a degenerált dobozok, egy pont, egy vékony szóköz, egy közel nulla magasságú dobozzal rendelkező glifa, ne zsugorítsák a toleranciát semmivé, és ne vágják el a sort egyetlen karakter után
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;
Három részlet ebben a ciklusban kiérdemli a helyét. A tolerancia a magból származik, nem egy konstansból, így egy 24 pontos címsor széles sávot kap, egy 7 pontos lábjegyzetszöveg pedig szűket, és egyik sem lopja el a másik karaktereit. Az összehasonlítás függőleges középpontokat használ, nem alapvonalakat vagy dobozteteket, ami egy felső indexet, egy soron belüli más méretű futamot vagy egy vegyes betűtípusú mondatot ugyanazon a soron tart a szomszédjaival. Egy sikertelen FPDFText_GetCharBox pedig megszakítja a pásztázást ahelyett, hogy kihagynánk, mert egy olyan karakter, amelynek nincs lekérhető geometriája, semmilyen bizonyítékot nem ad egyik irányba sem, és a folytatás mellette engedné a bejárást átugrani egy valódi határon egy távolabbi karakter erejéig
Miért kell minden kijelölési útvonalnak egy segédfüggvényt megosztania?
Mert három kódútvonal, amelyik mindegyik önmaga valósítja meg "a sort", el fog térni egymástól, és csendben fog eltérni. A PDFium Componentben a hármas kattintásos kiterjesztés, a Shift+Home, a Shift+End, és a nyilvános SelectLineAt metódus mind ugyanazon a LineRangeAt híváson keresztül oldja fel a határait. A hármas kattintás a kijelölés horgonyából inicializálja; a shift billentyűk a kijelölés kurzorából inicializálják, és csak azt a végét mozgatják; a SelectLineAt egy hívó által megadott karakterindexből inicializál, és az eredményt átadja a SelectTextRange-nek, ugyanannak a tartományellenőrzőnek, amelyet az egér útvonal is használ. Ha ehelyett duplikálnánk a logikát, a hiba nem összeomlás lenne, hanem lassú eltérés. Valaki ráhangolja a hármas kattintás toleranciáját, hogy javítson egy szűk sorközű jelentést, és most a Shift+End egy karakterrel korábban áll meg, mint ahol a hármas kattintás megáll ugyanazon a bekezdésen. Egy felhasználó kijelöl egy sort az egérrel, kiterjeszti a billentyűzettel, és nézi, ahogy a kijelölés zsugorodik. Mivel a SelectLineAt a közönséges kijelölési pipeline-t táplálja, a programozott kijelölés is független marad attól, hogy az egérbemenet engedélyezve van-e, és ingyen kapja a tartományellenőrzést, az újrarajzolást és az OnSelectionChange értesítést
// 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;
Vegyük észre a tolerancia-argumentumokat a CharacterIndexAtPos-on. A találatvizsgálatnak saját, oldalegységben kifejezett tűrése van, és ez külön ügy a sortoleranciától. Egy kattintás, amely két sor közötti sorközbe landol, arra a karakterre oldódik fel, amelyik a legközelebb van azon a dobozon belül; a sorpásztázás ezután onnan fut, amely karakter az lett. Egy túl nagyvonalú találattűrés a magba táplálása az egyik könnyebb módja annak, hogy egy olyan sort jelöljünk ki, amelyre a felhasználó nem is mutatott
Két indextér: karakterindex és szövegindex
Ha már van egy tartományunk, álljunk ellen a kísértésnek, hogy sztringeltolásként használjuk. Az FPDFText_GetText az oldal szövegét UTF-16 pufferként adja vissza, de az indexei nem ugyanaz az indextér, mint az FPDFText_GetCharBox és az FPDFText_CountChars által használt karakterindexek. A korábban tárgyalt generált karakterek a szövegpufferben ülnek, miközben olyan karakterhelyeket foglalnak el, amelyeknek nincs használható geometriájuk, a két számozás pedig egyre jobban eltér egymástól az oldalon végig. A hidak az FPDFText_GetTextIndexFromCharIndex és az FPDFText_GetCharIndexFromTextIndex, amelyeket a PDFium Component CharacterIndexToTextIndex és TextIndexToCharacterIndex néven burkol
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;
Az az irány harap a legerősebben, amelyik fordított. Egy a kinyert sztring fölött megvalósított keresés szövegindexeket ad, és ezek egyenesen egy doboz- vagy kijelölési API-nak való átadása csendben rossz karaktereket céloz meg, egy hibával, amely annál nagyobb, minél lejjebb megyünk az oldalon. Konvertáljunk a TextIndexToCharacterIndex-szel, mielőtt bármi geometriai hozzáérne a számhoz. A surrogate párok egy második, önálló eltolási problémát adnak ehhez, amelyet az emoji, CJK és surrogate párokról szóló cikk tárgyal
Ahol a heurisztika meghajlik
Legyünk őszinték magunkkal a korlátokat illetően, mert valósak és elérhetők. A forgatott szöveg a legvilágosabb eset: egy karakterdoboz egy tengelyillesztett téglalap az oldal terében, így 90 fokkal forgatott szövegnél egy vizuális sor dobozainak függőleges középpontjai szétszóródnak az oldalon, a pásztázás pedig szinte azonnal megáll. Amit kapunk, egy rövid kijelölés, nem egy hibás, ami a jobb hibamód, de attól még hiba. A függőleges írási módok ugyanígy viselkednek ugyanezen okból. A kéthasábos elrendezések akkor működnek, amikor a hasábok függőlegesen el vannak tolva egymáshoz képest, és akkor törnek el, amikor nincsenek. Ha mindkét hasáb osztozik egy alapvonalrácson, a jobb hasáb karakterei a bal hasáb sorának tolerancián belül ülnek, a pásztázás pedig egyenesen átfut a közön, mert tiszta geometriában nincs semmi, ami megállítaná. Ennek felismeréséhez egy vízszintes rés-teszt kell a függőleges klaszterezés tetejére, a résküszöb megválasztása pedig önálló ítélet arról, mely dokumentumokban vagyunk hajlandók tévedni. A vegyes betűméretek azok az esetek, amelyeket a mag-relatív tolerancia jól kezel: egy 11 pontos törzsszövegen belüli soron belüli 8 pontos kódrészlet a sávon belül tartja a középpontját, a következő alapvonalon lévő 24 pontos címsor pedig nem húzza be magába a törzssort
Az itt leírt sorkijelölési szemantika a PDFium Component részeként érkezik Delphihez és C++Builderhez, a példákban használt találatvizsgálati, kijelöléstartomány- és szövegindex-API-k mellett; a termékoldal hordozza a szövegoldal- és kijelölési modell teljes referenciáját