En PDF-textsida exponerar tecken och rutor, aldrig rader. PDFium Component bygger en visuell rad genom att klustra teckenrutor vars vertikala centrum faller inom halva fröteckenhöjden, skannande utåt från det klickade tecknet tills toleransen överskrids. Varje markeringsväg i visaren anropar den hjälpfunktionen, så mus, tangentbord, och kod är överens
Symptomet som skickar dig hit letande är specifikt och obehagligt. En användare trippelklickar ett stycke i en tvåkolumnsrapport och får halva sidan. Eller de trippelklickar en tabellcell och markeringen sväljer hela raden plus sidnumret i sidfoten. Visaren är inte trasig; den ställer en fråga filen inte kan besvara. Det finns ingen rad i en PDF att markera, och varje implementation som låtsas annat gissar. Den här artikeln handlar om att göra gissningen avsiktlig och göra den konsistent. Om det du faktiskt behöver är att dra ut text ur ett dokument, se att extrahera text från PDF-dokument med PDFium; om du lägger ut text och behöver bredder, se textmätning och radbrytning. Här är ämnet smalare: att avgöra var en visuell rad börjar och slutar, och att markera exakt det
Varför har en PDF-textsida inga radobjekt?
Eftersom en PDF-innehållsström beskriver ritning, inte struktur. ISO 32000-1 §9.4 definierar ett textobjekt som ett BT / ET-par innehållande positionerings- och visningsoperatorer. Positioneringsoperatorerna i §9.4.2 (Td, TD, Tm, T*) flyttar en textmatris runt sidan, och visningsoperatorerna i §9.4.3 (Tj, TJ, ', ") målar glyfer var den matrisen för närvarande pekar. Inget i den modellen säger "den här sekvensen av glyfer är en rad". En rad är vad en människa ser efter att målningen är klar
Producenter gör detta värre på sätt du inte kan kontrollera. Ett justerat stycke kan avges som en TJ-array per rad, eller som en Tj per ord med en explicit Tm innan varje, eller som en enda visningsoperation med kerning-justeringar som bär mellanrummet. En tvåkolumnslayout kan avge vänsterkolumnen uppifrån och ner och sedan högerkolumnen, eller den kan interfoliera dem om producenten vandrade sin egen interna objektlista i en annan ordning. Teckensekvensen PDFium ger dig följer innehållsströmmen, och innehållsströmmen följer vad den genererande applikationen kände för att göra. Så de två funktioner du faktiskt får är FPDFText_CountChars, som rapporterar hur många tecken sidan håller, och FPDFText_GetCharBox, som returnerar gränsrutan för ett tecken i sidrymden. Det är hela det råa vokabuläret. Allt ovanför det, ord, rader, stycken, kolumner, är inferens du utför på geometri
Varför är CR- och LF-detektering fel test?
Eftersom tecknen du skulle testa mot inte tillförlitligt är närvarande, och när de är närvarande är de inte tillförlitligt dina. PDFium injicerar syntetiska tecken i textsidan för att göra extraherad text läsbar: ett mellanslag där två körningar är visuellt separerade, en CR eller LF där nästa körning börjar på en ny baslinje. FPDFText_IsGenerated existerar precis så du kan skilja dem från tecken som kom ur filen, och PDFium Component exponerar det som CharacterGenerated-egenskapen
Dela på de tecknen och du ärver varje bedömning PDFium gjorde medan den syntetiserade dem. En hård radbrytning inuti ett radbrutet stycke och en mjuk radbrytning ser identiska ut efter syntetisering. En tabellrad som producenten avgav cell för cell kanske inte får någon brytning alls mellan sista cellen och första cellen i nästa rad, eftersom baslinjerna råkar vara tillräckligt nära. Samtidigt kan en rubrik följd av brödtext i en annan storlek få två brytningar där en människa ser en. De genererade tecknen är en renderingsbekvämlighet för hela-sidan-extraktion; de är inte en radmodell, och de degraderar precis i de dokument där markering spelar störst roll
Klustring av teckenrutor efter vertikalt centrum
Den tillförlitliga signalen är geometri. Ta tecknet användaren klickade som fröet, beräkna det vertikala centrumet av dess ruta, och vandra utåt i båda riktningarna medan angränsande rutor håller sina vertikala centrum inom tolerans. PDFium Component använder halva frörutans höjd som den toleransen, med ett golv på 0.5 sidenheter så att degenererade rutor, en punkt, ett smalt mellanslag, en glyf med en nästan-noll-höjds ruta, inte kollapsar toleransen till ingenting och skär av raden efter ett tecken
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;
Tre detaljer i den loopen förtjänar sin plats. Toleransen härleds från fröet snarare än från en konstant, så en 24pt-rubrik får ett brett band och 7pt-fotnotstext får ett smalt, och ingen stjäl tecken från den andra. Jämförelsen använder vertikala centrum snarare än baslinjer eller ruttoppar, vilket håller en upphöjd text, en inline-körning i en annan storlek, eller en blandad-typsnitt-mening på samma rad som sina grannar. Och ett misslyckat FPDFText_GetCharBox avslutar skanningen snarare än att hoppas över, eftersom ett tecken utan hämtbar geometri inte ger dig något bevis åt endera hållet, och att fortsätta förbi det skulle låta vandringen hoppa över en genuin gräns på styrkan av ett tecken längre bort
Varför måste varje markeringsväg dela en hjälpfunktion?
Eftersom tre kodvägar som var och en implementerar "raden" kommer divergera, och de kommer divergera tyst. I PDFium Component löser trippelklick-expansion, Shift+Home, Shift+End, och den publika SelectLineAt-metoden alla sina gränser genom samma LineRangeAt-anrop. Trippelklick fröar den från markeringsankaret; shift-tangenterna fröar den från markeringsmarkören och flyttar bara den änden; SelectLineAt fröar den från ett anropar-tillhandahållet teckenindex och lämnar resultatet till SelectTextRange, samma intervallvalidator musvägen använder. Duplicera logiken i stället och felet är inte en krasch, det är en långsam drift. Någon justerar trippelklicktoleransen för att fixa en rapport med tät radavstånd, och nu stannar Shift+End ett tecken kort av var trippelklick stannar på samma stycke. En användare markerar en rad med musen, utökar den med tangentbordet, och ser markeringen krympa. Eftersom SelectLineAt matar den vanliga markeringspipelinen förblir också programmatisk markering oberoende av om musinmatning är aktiverad, och får ändå intervallvalidering, omritning, och OnSelectionChange-notifieringen gratis
// 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;
Notera toleransargumenten på CharacterIndexAtPos. Träffkontroll har sin egen marginal, uttryckt i sidenheter, och det är en separat angelägenhet från radtoleransen. Ett klick som landar i radavståndet mellan två rader löses upp till vilket tecken som helst som är närmast inom den rutan; radskanningen körs sedan från vad det tecknet visade sig vara. Att mata en för generös träfftolerans in i fröet är ett av de enklare sätten att markera en rad användaren inte pekade på
Två indexrymder: teckenindex och textindex
När du väl har ett intervall, motstå impulsen att använda det som en strängoffset. FPDFText_GetText returnerar sidtexten som en UTF-16-buffert, men dess index är inte samma indexrymd som teckenindexen som används av FPDFText_GetCharBox och FPDFText_CountChars. De genererade tecknen diskuterade tidigare sitter i textbufferten medan de upptar teckenplatser utan användbar geometri, och de två numreringarna driver isär genom sidan. Broarna är FPDFText_GetTextIndexFromCharIndex och FPDFText_GetCharIndexFromTextIndex, omslutna av PDFium Component som CharacterIndexToTextIndex och 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;
Riktningen som biter hårdast är den omvända. En sökning implementerad över den extraherade strängen ger dig textindex, och att skicka de direkt till ett rut- eller markerings-API adresserar tyst fel tecken, med ett fel som växer ju längre ner på sidan du kommer. Konvertera med TextIndexToCharacterIndex innan något geometriskt rör talet. Surrogatpar lägger till ett andra, oberoende offset-problem ovanpå detta, vilket täcks i artikeln om emoji, CJK, och surrogatpar
Var heuristiken böjer sig
Var ärlig med dig själv om gränserna, eftersom de är verkliga och de är nåbara. Roterad text är det tydligaste fallet: en teckenruta är en axelanpassad rektangel i sidrymden, så för text roterad 90 grader har rutorna i en visuell rad vertikala centrum spridda över sidan, och skanningen stannar nästan omedelbart. Vad du får är en kort markering snarare än en felaktig, vilket är det bättre felläget, men det är ändå ett fel. Vertikala skrivlägen beter sig på samma sätt av samma anledning. Tvåkolumnslayouter fungerar när kolumnerna är vertikalt förskjutna från varandra och går sönder när de inte är det. Om båda kolumnerna delar ett baslinjenät sitter tecken från högerkolumnen inom tolerans av vänsterkolumnens rad, och skanningen kommer köra rakt över gränden, eftersom det i ren geometri inte finns något där att stanna vid. Att upptäcka det behöver ett horisontellt gap-test ovanpå den vertikala klustringen, och att välja gap-tröskeln är sin egen bedömning om vilka dokument du är villig att ha fel om. Blandade typsnittsstorlekar är fallet den fröjämförda toleransen hanterar bra: en inline 8pt kodrad inuti 11pt brödtext håller sitt centrum inom bandet, och en 24pt rubrik på nästa baslinje drar inte in brödtextraden i sig
Radmarkeringssemantiken beskriven här levereras i PDFium Component för Delphi och C++Builder, tillsammans med träffkontroll-, markeringsintervall-, och textindex-API:erna använda i exemplen; produktsidan bär den fullständiga referensen för textsidan och markeringsmodellen