PDF текстова страница излага символи и кутии, никога редове. PDFium Component изгражда визуален ред, като групира кутии на символи, чиито вертикални центрове попадат в рамките на половината от височината на семенния символ, сканирайки навън от кликнатия символ, докато толерансът не бъде надвишен. Всеки път за селекция във визуализатора извиква този един помощник, така че мишка, клавиатура и код се съгласяват
Симптомът, който ви праща да търсите това, е конкретен и неприятен. Потребител прави тройно кликване върху параграф в двуколонен отчет и получава половината страница. Или прави тройно кликване върху клетка на таблица и селекцията поглъща целия ред плюс номера на страницата в долния колонтитул. Визуализаторът не е счупен; той задава въпрос, на който файлът не може да отговори. Няма ред в PDF за избиране, а всяка имплементация, преструваща се на друго, гадае. Тази статия е за това да направи гадаенето умишлено и последователно. Ако това, от което наистина се нуждаете, е изваждане на текст от документ, вижте извличане на текст от PDF документи с PDFium; ако подреждате текст и се нуждаете от ширини, вижте измерване на текст и пренасяне на думи. Тук темата е по-тясна: решаване къде визуален ред започва и завършва, и избиране на точно него
Защо PDF текстова страница няма обекти за редове?
Защото поток със съдържание на PDF описва рисуване, не структура. ISO 32000-1 §9.4 дефинира текстов обект като двойка BT / ET, съдържаща оператори за позициониране и показване. Операторите за позициониране от §9.4.2 (Td, TD, Tm, T*) местят текстова матрица по страницата, а операторите за показване от §9.4.3 (Tj, TJ, ', ") рисуват глифи там, където тази матрица в момента сочи. Нищо в този модел не казва „тази поредица от глифи е ред“. Ред е това, което човек вижда, след като рисуването е завършено
Продуцентите влошават това по начини, които не можете да контролирате. Подравнен параграф може да бъде излъчен като един масив TJ на ред, или като един Tj на дума с изрична Tm преди всяка, или като единична операция за показване с настройки за керниране, носещи разстоянието. Двуколонен layout може да излъчи лявата колона отгоре надолу и после дясната колона, или може да ги преплете, ако продуцентът е обходил собствения си вътрешен списък от обекти в различен ред. Последователността от символи, която PDFium ви подава, следва потока със съдържание, а потокът със съдържание следва каквото на генериращото приложение му се е приискало да направи. Така двете функции, които всъщност получавате, са FPDFText_CountChars, която отчита колко символа съдържа страницата, и FPDFText_GetCharBox, която връща ограждащата кутия на един символ в пространството на страницата. Това е целият суров речник. Всичко над него — думи, редове, параграфи, колони — е извод, който вие правите над геометрия
Защо откриването на CR и LF е грешният тест?
Защото символите, за които бихте тествали, не са надеждно налични, а когато са налични, не са надеждно ваши. PDFium инжектира синтетични символи в текстовата страница, за да направи извлечения текст четим: интервал, където две последователности са визуално разделени, CR или LF, където следващата последователност започва на нова базова линия. FPDFText_IsGenerated съществува точно за да можете да ги различите от символи, дошли от файла, а PDFium Component го излага като свойството CharacterGenerated
Разделете по тези символи и наследявате всяко преценъчно решение, взето от PDFium, докато ги е синтезирал. Твърд разрив на ред вътре в увит параграф и мек пренос изглеждат идентично след синтезата. Ред на таблица, който продуцентът е излъчил клетка по клетка, може изобщо да не получи разрив между последната клетка и първата клетка на следващия ред, защото базовите линии се оказват достатъчно близо. Междувременно заглавие, следвано от текст в основния текст с различен размер, може да получи два разрива, където човек вижда един. Генерираните символи са удобство за рендиране при извличане на цяла страница; те не са модел за редове, и деградират точно в документите, където селекцията има най-голямо значение
Групиране на кутии на символи по вертикален център
Надеждният сигнал е геометрията. Вземете символа, който потребителят е кликнал, като семе, изчислете вертикалния център на кутията му, и обхождайте навън в двете посоки, докато съседните кутии пазят вертикалните си центрове в рамките на толеранс. PDFium Component използва половината от височината на семенната кутия като този толеранс, с под от 0.5 единици на страница, така че изродени кутии — точка, тесен интервал, глиф с почти нулева височина на кутията — не свиват толеранса до нищо и не отрязват реда след един символ
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;
Три подробности в този цикъл заслужават мястото си. Толерансът произлиза от семето, а не от константа, така че заглавие с 24pt получава широка лента, а бележка под линия с 7pt получава тясна, и никое от двете не открадва символи от другото. Сравнението използва вертикални центрове, а не базови линии или горни ръбове на кутии, което държи горен индекс, вградена последователност с различен размер, или изречение със смесен шрифт на същия ред като съседите му. А провалено FPDFText_GetCharBox прекратява сканирането, вместо да бъде пропуснато, защото символ без извлекаема геометрия не ви дава доказателство в никоя посока, а продължаването отвъд него би позволило на обхождането да прескочи истинска граница на силата на символ по-нататък
Защо всеки път за селекция трябва да споделя един помощник?
Защото три пътя на кода, всеки от които имплементира „реда“, ще се разминат, и ще се разминат тихо. В PDFium Component разширяването при тройно кликване, Shift+Home, Shift+End, и публичният метод SelectLineAt всички разрешават границите си чрез едно и също извикване на LineRangeAt. Тройното кликване го засява от котвата на селекцията; клавишите shift го засяват от курсора на селекцията и местят само този край; SelectLineAt го засява от индекс на символ, подаден от извикващия, и подава резултата на SelectTextRange, същия валидатор на диапазон, който пътят на мишката използва. Дублирайте логиката вместо това и провалът не е срив, той е бавно отклонение. Някой настройва толеранса на тройното кликване, за да поправи отчет с плътно междуредие, и сега Shift+End спира един символ преди мястото, където тройното кликване спира на същия параграф. Потребител избира ред с мишката, разширява го с клавиатурата, и гледа как селекцията се свива. Защото SelectLineAt захранва обикновения pipeline за селекция, програмната селекция също остава независима от това дали входът от мишката е активиран, и все пак получава валидация на диапазон, препрерисуване и известяването 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;
Отбележете аргументите за толеранс на CharacterIndexAtPos. Тестването за попадение има собствен запас, изразен в единици на страница, и е отделна грижа от толеранса за реда. Кликване, попадащо в междуредието между два реда, се разрешава до който и да е символ, най-близък в тази кутия; сканирането на реда после се изпълнява от какъвто символ се окаже това. Подаването на твърде щедър толеранс за попадение в семето е един от по-лесните начини да изберете ред, към който потребителят не е сочил
Две пространства на индекси: индекс на символ и текстов индекс
Щом имате диапазон, устоявайте на изкушението да го използвате като офсет на низ. FPDFText_GetText връща текста на страницата като буфер в UTF-16, но индексите му не са същото пространство на индекси като индексите на символи, използвани от FPDFText_GetCharBox и FPDFText_CountChars. Генерираните символи, обсъдени по-рано, седят в текстовия буфер, докато заемат слотове за символи без използваема геометрия, а двете номерации се разминават през страницата. Мостовете са FPDFText_GetTextIndexFromCharIndex и FPDFText_GetCharIndexFromTextIndex, обвити от PDFium Component като CharacterIndexToTextIndex и 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;
Посоката, която хапе най-силно, е обратната. Търсене, имплементирано над извлечения низ, ви дава текстови индекси, а подаването им директно на API за кутия или селекция тихо адресира грешните символи, с грешка, растяща колкото по-надолу по страницата отидете. Преобразувайте с TextIndexToCharacterIndex, преди каквото и да е геометрично да докосне числото. Двойките surrogate добавят втори, независим проблем с офсет върху това, разгледан в статията за емоджита, CJK и двойки surrogate
Къде евристиката се огъва
Бъдете честни със себе си за границите, защото те са реални и достижими. Завъртян текст е най-ясният случай: кутия на символ е правоъгълник, подравнен с осите, в пространството на страницата, така че за текст, завъртян на 90 градуса, кутиите на един визуален ред имат вертикални центрове, разпръснати през страницата, и сканирането спира почти веднага. Това, което получавате, е кратка селекция, вместо грешна, което е по-добрият режим на провал, но все пак е провал. Вертикалните режими на писане се държат по същия начин по същата причина. Двуколонни layout-и работят, когато колоните са вертикално изместени една от друга, и се чупят, когато не са. Ако двете колони споделят мрежа от базови линии, символи от дясната колона седят в толеранса на реда на лявата колона, а сканирането ще премине право през жлеба, защото в чиста геометрия няма нищо там, на което да спре. Откриването на това се нуждае от хоризонтален тест за пролука върху вертикалното групиране, а изборът на прага на пролуката е собствено преценъчно решение за това кои документи сте готови да сгрешите. Смесените размери на шрифта са случаят, който толерансът, относителен спрямо семето, обработва добре: вграден код с 8pt вътре в основен текст с 11pt пази центъра си вътре в лентата, а заглавие с 24pt на следващата базова линия не издърпва реда на основния текст в себе си
Семантиката за избор на ред, описана тук, се доставя в PDFium Component за Delphi и C++Builder, заедно с API-тата за тестване на попадение, диапазон на селекция и текстов индекс, използвани в примерите; страницата на продукта носи пълната справка за модела на текстовата страница и селекцията