PDF textová strana sprístupňuje znaky a boxy, nikdy riadky. PDFium Component stavia vizuálny riadok zoskupovaním boxov znakov, ktorých vertikálne stredy padajú do polovice výšky seed znaku, skenujúc smerom von od kliknutého znaku, kým sa neprekročí tolerancia. Každá cesta výberu vo viewri volá tohto jedného pomocníka, takže myš, klávesnica a kód sa zhodujú
Príznak, ktorý vás sem privedie hľadať, je konkrétny a nepríjemný. Používateľ trojklikne odsek v dvojstĺpcovej správe a dostane polovicu strany. Alebo trojklikne bunku tabuľky a výber pohltí celý riadok plus číslo strany v pätičke. Viewer nie je pokazený; kladie otázku, na ktorú súbor nedokáže odpovedať. V PDF neexistuje riadok na výber, a akákoľvek implementácia, ktorá to predstiera, hádže. Tento článok sa venuje tomu, aby bol hádaný odhad zámerný, a aby bol konzistentný. Ak skutočne potrebujete vytiahnuť text z dokumentu, pozrite si extrakciu textu z PDF dokumentov s PDFium; ak rozvrhujete text a potrebujete šírky, pozrite si meranie textu a zalamovanie slov. Tu je predmetom niečo užšie: rozhodnúť, kde vizuálny riadok začína a končí, a vybrať presne to
Prečo PDF textová strana nemá objekty riadkov?
Pretože PDF obsahový stream opisuje kreslenie, nie štruktúru. ISO 32000-1 §9.4 definuje textový objekt ako dvojicu BT / ET obsahujúcu polohovacie a zobrazovacie operátory. Polohovacie operátory z §9.4.2 (Td, TD, Tm, T*) posúvajú textovú maticu po strane, a zobrazovacie operátory z §9.4.3 (Tj, TJ, ', ") maľujú glyfy tam, kam táto matica práve ukazuje. Nič v tomto modeli nehovorí „táto sekvencia glyfov je riadok“. Riadok je to, čo človek vidí po tom, čo je maľovanie hotové
Producenti to zhoršujú spôsobmi, ktoré nemôžete kontrolovať. Zarovnaný odsek môže byť vydaný ako jedno pole TJ na riadok, alebo ako jedno Tj na slovo s explicitným Tm pred každým, alebo ako jedna zobrazovacia operácia s úpravami kerningu nesúcimi rozostup. Dvojstĺpcový layout môže vydať ľavý stĺpec zhora nadol a potom pravý stĺpec, alebo ich môže preložiť, ak producent prešiel svoj vlastný interný zoznam objektov v inom poradí. Sekvencia znakov, ktorú vám PDFium odovzdá, sleduje obsahový stream, a obsahový stream sleduje čokoľvek, čo sa generujúcej aplikácii zachcelo urobiť. Takže dve funkcie, ktoré skutočne dostanete, sú FPDFText_CountChars, ktorá hlási, koľko znakov strana obsahuje, a FPDFText_GetCharBox, ktorá vracia ohraničujúci box jedného znaku v priestore strany. To je celý surový slovník. Všetko nad tým, slová, riadky, odseky, stĺpce, je inferencia, ktorú vykonávate nad geometriou
Prečo je detekcia CR a LF nesprávny test?
Pretože znaky, voči ktorým by ste testovali, nie sú spoľahlivo prítomné, a keď prítomné sú, nie sú spoľahlivo vaše. PDFium vkladá syntetické znaky do textovej strany, aby extrahovaný text bol čitateľný: medzeru tam, kde sú dva behy vizuálne oddelené, CR alebo LF tam, kde ďalší beh začína na novej základni. FPDFText_IsGenerated existuje presne na to, aby ste ich vedeli odlíšiť od znakov, ktoré vyšli zo súboru, a PDFium Component to sprístupňuje ako vlastnosť CharacterGenerated
Rozdeľte podľa týchto znakov a zdedíte každé úsudkové rozhodnutie, ktoré PDFium urobilo počas ich syntézy. Tvrdý zalom riadku vnútri zalomeného odseku a mäkký zalom vyzerajú po syntéze identicky. Riadok tabuľky, ktorý producent vydal bunku po bunke, nemusí dostať žiadny zalom medzi poslednou bunkou a prvou bunkou ďalšieho riadku, pretože základne sú náhodou dosť blízko. Medzitým nadpis nasledovaný telom textu v inej veľkosti môže dostať dva zalomy tam, kde človek vidí jeden. Generované znaky sú vykresľovacia pomôcka pre extrakciu celej strany; nie sú modelom riadku, a degradujú presne v dokumentoch, kde na výbere záleží najviac
Zoskupovanie boxov znakov podľa vertikálneho stredu
Spoľahlivým signálom je geometria. Vezmite znak, na ktorý používateľ kliknul, ako seed, vypočítajte vertikálny stred jeho boxu, a prechádzajte smerom von v oboch smeroch, kým susediace boxy udržujú svoje vertikálne stredy v tolerancii. PDFium Component používa polovicu výšky seed boxu ako túto toleranciu, s dolnou hranicou 0,5 jednotky strany, takže degenerované boxy, bodka, tenká medzera, glyf s takmer nulovou výškou boxu, nezredukujú toleranciu na nič a neoreže riadok po jednom znaku
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 detaily v tejto slučke si zarábajú svoje miesto. Tolerancia sa odvodzuje od seedu namiesto konštanty, takže 24pt nadpis dostane široké pásmo a 7pt text poznámky pod čiarou dostane úzke, a žiadny neukradne znaky tomu druhému. Porovnanie používa vertikálne stredy namiesto základní alebo horných hrán boxov, čo udržuje horný index, vloženú časť inej veľkosti, alebo zmiešanú vetu s viacerými fontmi na tom istom riadku ako jej susedov. A zlyhané FPDFText_GetCharBox ukončí sken namiesto toho, aby bolo preskočené, pretože znak bez získateľnej geometrie vám nedáva žiadny dôkaz ani jedným smerom, a pokračovanie za ním by dovolilo prechodu preskočiť cez skutočnú hranicu na základe znaku ďalej
Prečo musí každá cesta výberu zdieľať jedného pomocníka?
Pretože tri cesty kódu, ktoré každá implementuje „riadok“, sa rozídu, a rozídu sa ticho. V PDFium Component riešia trojklikové rozšírenie, Shift+Home, Shift+End, a verejná metóda SelectLineAt všetky svoje hranice cez to isté volanie LineRangeAt. Trojklik ho seeduje z kotvy výberu; shift klávesy ho seedujú z kurzora výberu a hýbu iba týmto koncom; SelectLineAt ho seeduje z volajúcim dodaného indexu znaku a výsledok odovzdá SelectTextRange, tomu istému validátoru rozsahu, ktorý používa cesta myši. Duplikujte logiku namiesto toho a zlyhanie nie je pád, je to pomalý drift. Niekto naladí toleranciu trojkliku, aby opravil správu s tesným riadkovaním, a teraz Shift+End zastaví o jeden znak skôr, než kde zastaví trojklik na tom istom odseku. Používateľ vyberie riadok myšou, rozšíri ho klávesnicou, a pozerá, ako sa výber zmenšuje. Pretože SelectLineAt napája obyčajnú pipeline výberu, programový výber tiež zostáva nezávislý od toho, či je vstup myšou povolený, a napriek tomu zdarma dostáva validáciu rozsahu, prekreslenie a notifikáciu 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;
Všimnite si argumenty tolerancie na CharacterIndexAtPos. Hit testing má svoju vlastnú vôľu, vyjadrenú v jednotkách strany, a je to samostatná záležitosť od tolerancie riadku. Klik, ktorý pristane v riadkovaní medzi dvoma riadkami, sa vyrieši na ktorýkoľvek znak najbližší vnútri tohto boxu; sken riadku potom beží od toho, čím sa tento znak ukázal byť. Napájanie príliš veľkorysej tolerancie hit testu do seedu je jeden z ľahších spôsobov, ako vybrať riadok, na ktorý používateľ neukazoval
Dva priestory indexov: index znaku a textový index
Akonáhle máte rozsah, odolajte pokušeniu použiť ho ako offset reťazca. FPDFText_GetText vracia text strany ako UTF-16 buffer, ale jeho indexy nie sú ten istý priestor indexov ako indexy znakov použité FPDFText_GetCharBox a FPDFText_CountChars. Generované znaky diskutované vyššie sedia v textovom bufferi, pričom obsadzujú sloty znakov bez použiteľnej geometrie, a tieto dve číslovania sa naprieč stranou rozchádzajú. Mosty sú FPDFText_GetTextIndexFromCharIndex a FPDFText_GetCharIndexFromTextIndex, obalené PDFium Component ako CharacterIndexToTextIndex a 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, ktorý hryzie najtvrdšie, je ten opačný. Vyhľadávanie implementované nad extrahovaným reťazcom vám dá textové indexy, a ich priame odovzdanie API pre box alebo výber ticho adresuje nesprávne znaky, s chybou, ktorá rastie čím ďalej po strane idete. Konvertujte pomocou TextIndexToCharacterIndex skôr, než sa čísla dotkne čokoľvek geometrické. Náhradné páry (surrogate pairs) pridávajú druhý, nezávislý problém s offsetom navrch tohto, čo je pokryté v článku o emoji, CJK a náhradných pároch
Kde sa heuristika ohýba
Buďte k sebe úprimní ohľadom limitov, pretože sú skutočné a dosiahnuteľné. Otočený text je najjasnejší prípad: box znaku je osiam zarovnaný obdĺžnik v priestore strany, takže pri texte otočenom o 90 stupňov majú boxy jedného vizuálneho riadku vertikálne stredy rozptýlené naprieč stranou, a sken sa zastaví takmer okamžite. Dostanete krátky výber namiesto nesprávneho, čo je lepší režim zlyhania, ale je to stále zlyhanie. Vertikálne módy písania sa správajú rovnako z rovnakého dôvodu. Dvojstĺpcové layouty fungujú, keď sú stĺpce navzájom vertikálne posunuté, a lámu sa, keď nie sú. Ak oba stĺpce zdieľajú základňovú mriežku, znaky z pravého stĺpca sedia v tolerancii riadku ľavého stĺpca, a sken pôjde priamo cez medzeru, pretože v čistej geometrii tam nie je nič, čo by ho zastavilo. Detekcia toho potrebuje test horizontálnej medzery navrch vertikálneho zoskupovania, a voľba prahu medzery je vlastné úsudkové rozhodnutie o tom, o ktorých dokumentoch ste ochotní byť nesprávni. Zmiešané veľkosti fontov sú prípad, ktorý tolerancia relatívna k seedu zvláda dobre: vložený 8pt kódový úsek vnútri 11pt tela textu si udrží svoj stred vnútri pásma, a 24pt nadpis na ďalšej základni nevtiahne riadok tela do seba
Sémantika výberu riadku opísaná tu je súčasťou PDFium Component pre Delphi a C++Builder, popri hit testing, rozsahu výberu a API textového indexu použitých v príkladoch; stránka produktu nesie úplnú referenciu pre model textovej strany a výberu