Tekninen artikkeli

PDF-tekstirivin valinta PDFium-merkkilaatikoilla Delphissä

PDF-tekstisivu paljastaa merkkejä ja laatikoita, ei koskaan rivejä. PDFium Component rakentaa visuaalisen rivin klusteroimalla merkkilaatikoita, joiden pystysuuntaiset keskipisteet osuvat puoleen siemenmerkin korkeudesta, skannaten ulospäin klikatusta merkistä, kunnes toleranssi ylittyy. Jokainen katseluohjelman valintapolku kutsuu tuota yhtä apuria, joten hiiri, näppäimistö ja koodi ovat samaa mieltä

Oire, joka lähettää sinut etsimään tätä, on tarkka ja epämiellyttävä. Käyttäjä kolmoisklikkaa kappaletta kaksipalstaisessa raportissa ja saa puolet sivusta. Tai hän kolmoisklikkaa taulukon solua, ja valinta nielee koko rivin plus sivunumeron alatunnisteesta. Katseluohjelma ei ole rikki; se kysyy kysymystä, johon tiedosto ei voi vastata. PDF:ssä ei ole riviä valittavaksi, ja mikä tahansa toteutus, joka teeskentelee toisin, arvaa. Tämä artikkeli käsittelee sen tekemistä, että arvaus on tarkoituksellinen ja johdonmukainen. Jos todella tarvitset tekstin poimimista asiakirjasta, katso tekstin poimiminen PDF-asiakirjoista PDFiumilla; jos asettelet tekstiä ja tarvitset leveyksiä, katso tekstin mittaus ja rivitys. Tässä aihe on kapeampi: sen päättäminen, missä visuaalinen rivi alkaa ja päättyy, ja täsmälleen tuon valitseminen

Miksi PDF-tekstisivulla ei ole rivioppiota?

Koska PDF-sisältövirta kuvaa piirtämistä, ei rakennetta. ISO 32000-1 §9.4 määrittelee tekstiobjektin BT / ET-parina, joka sisältää sijoittelu- ja näyttöoperaattoreita. §9.4.2:n sijoitteluoperaattorit (Td, TD, Tm, T*) siirtävät tekstimatriisia sivun ympäri, ja §9.4.3:n näyttöoperaattorit (Tj, TJ, ', ") maalaavat glyyfit sinne, minne tuo matriisi kulloinkin osoittaa. Mikään tuossa mallissa ei sano "tämä glyyfien juoksu on rivi". Rivi on se, mitä ihminen näkee, kun maalaus on valmis

Tuottajat pahentavat tätä tavoilla, joita et voi hallita. Tasattu kappale voidaan antaa yhtenä TJ-taulukkona per rivi, tai yhtenä Tj:na per sana nimenomaisella Tm:llä ennen jokaista, tai yhtenä näyttöoperaationa, jossa kernaussäädöt kantavat välistyksen. Kaksipalstainen asettelu voi antaa vasemman palstan ylhäältä alas ja sitten oikean palstan, tai se voi lomittaa ne, jos tuottaja käveli oman sisäisen objektilistansa eri järjestyksessä. Merkkisekvenssi, jonka PDFium antaa sinulle, seuraa sisältövirtaa, ja sisältövirta seuraa mitä tahansa, mitä generoiva sovellus halusi tehdä. Joten kaksi funktiota, jotka todella saat, ovat FPDFText_CountChars, joka raportoi, kuinka monta merkkiä sivu pitää sisällään, ja FPDFText_GetCharBox, joka palauttaa yhden merkin rajaavan laatikon sivuavaruudessa. Se on koko raaka sanasto. Kaikki sen yläpuolella, sanat, rivit, kappaleet, palstat, on päättelyä, jonka suoritat geometrian yli

Miksi CR- ja LF-tunnistus on väärä testi?

Koska merkit, joita vastaan testaisit, eivät ole luotettavasti läsnä, ja kun ne ovat läsnä, ne eivät ole luotettavasti omiasi. PDFium ruiskuttaa synteettisiä merkkejä tekstisivulle tehdäkseen poimitun tekstin luettavaksi: välilyönnin, missä kaksi juoksua on visuaalisesti erotettu, CR:n tai LF:n, missä seuraava juoksu alkaa uudella perusviivalla. FPDFText_IsGenerated on olemassa juuri, jotta voit erottaa ne merkeistä, jotka tulivat tiedostosta, ja PDFium Component paljastaa sen CharacterGenerated-ominaisuutena

Jaa noiden merkkien kohdalla, ja perit jokaisen arviointipäätöksen, jonka PDFium teki niitä synteesoidessaan. Kova rivinvaihto rivitetyn kappaleen sisällä ja pehmeä rivitys näyttävät identtisiltä synteesin jälkeen. Taulukkorivi, jonka tuottaja antoi solu solulta, ei ehkä saa lainkaan katkoa viimeisen solun ja seuraavan rivin ensimmäisen solun välille, koska perusviivat sattuvat olemaan tarpeeksi lähellä toisiaan. Samaan aikaan otsikko, jota seuraa eri kokoinen leipäteksti, voi saada kaksi katkoa, missä ihminen näkee yhden. Generoidut merkit ovat renderöintimukavuus koko sivun poimintaa varten; ne eivät ole rivimalli, ja ne rappeutuvat juuri niissä asiakirjoissa, joissa valinnalla on eniten merkitystä

Merkkilaatikoiden klusterointi pystysuuntaisen keskipisteen mukaan

Luotettava signaali on geometria. Ota merkki, jota käyttäjä klikkasi, siemeneksi, laske sen laatikon pystysuuntainen keskipiste, ja kävele ulospäin molempiin suuntiin, kun naapurilaatikot pitävät pystysuuntaiset keskipisteensä toleranssin sisällä. PDFium Component käyttää puolta siemenlaatikon korkeudesta tuona toleranssina, lattialla 0.5 sivuyksikköä, jotta rappeutuneet laatikot, piste, ohut välilyönti, glyyfi lähes nollakorkuisella laatikolla, eivät romahduta toleranssia ei-mihinkään ja katkaise riviä yhden merkin jälkeen

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;

Kolme yksityiskohtaa tuossa silmukassa ansaitsevat paikkansa. Toleranssi johdetaan siemenestä eikä vakiosta, joten 24pt otsikko saa leveän vyön ja 7pt alaviitetekstiä saa kapean, eikä kumpikaan varasta merkkejä toiselta. Vertailu käyttää pystysuuntaisia keskipisteitä perusviivojen tai laatikoiden yläreunojen sijaan, mikä pitää yläindeksin, sisäisen eri kokoisen juoksun tai sekafontti-lauseen samalla rivillä kuin naapurinsa. Ja epäonnistunut FPDFText_GetCharBox päättää skannauksen sen sijaan, että se ohitettaisiin, koska merkki ilman noudettavaa geometriaa ei anna todisteita kumpaankaan suuntaan, ja jatkaminen sen ohi antaisi kävelyn hypätä aidon rajan yli myöhemmän merkin voimalla

Miksi jokaisen valintapolun täytyy jakaa yksi apuri?

Koska kolme koodipolkua, jotka kukin toteuttavat "rivin", ajautuvat erilleen, ja ne ajautuvat erilleen hiljaa. PDFium Componentissa kolmoisklikkauslaajennus, Shift+Home, Shift+End ja julkinen SelectLineAt-metodi ratkaisevat kaikki rajansa saman LineRangeAt-kutsun kautta. Kolmoisklikkaus siementää sen valinnan ankkurista; shift-näppäimet siementävät sen valinnan kursorista ja siirtävät vain sitä päätä; SelectLineAt siementää sen kutsujan antamasta merkki-indeksistä ja antaa tuloksen SelectTextRange:lle, samalle aluevalidaattorille, jota hiiripolku käyttää. Kaksinkerta logiikka sen sijaan, ja epäonnistuminen ei ole kaatuminen, se on hidas ajautuminen. Joku virittää kolmoisklikkauksen toleranssia korjatakseen raportin, jossa on tiukka riviväli, ja nyt Shift+End pysähtyy yhden merkin ennen sitä, mihin kolmoisklikkaus pysähtyy samassa kappaleessa. Käyttäjä valitsee rivin hiirellä, laajentaa sitä näppäimistöllä ja katsoo valinnan kutistuvan. Koska SelectLineAt ruokkii tavallista valintaputkea, myös ohjelmallinen valinta pysyy riippumattomana siitä, onko hiirisyöte käytössä, ja saa silti aluevalidoinnin, uudelleenmaalauksen ja OnSelectionChange-ilmoituksen ilmaiseksi

// 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;

Huomaa toleranssiargumentit CharacterIndexAtPos:issa. Osumatestauksella on oma väljyytensä, ilmaistuna sivuyksiköissä, ja se on erillinen huoli rivin toleranssista. Klikkaus, joka osuu kahden rivin väliseen liehuun, ratkeaa lähimpään merkkiin tuon laatikon sisällä; rivin skannaus ajetaan sitten mistä tahansa merkistä, joka osoittautui siksi. Liian antelias osumatoleranssi siemeneen syöttäminen on yksi helpoimmista tavoista valita rivi, johon käyttäjä ei osoittanut

Kaksi indeksiavaruutta: merkki-indeksi ja tekstin indeksi

Kun sinulla on alue, vastusta houkutusta käyttää sitä merkkijonosiirtymänä. FPDFText_GetText palauttaa sivun tekstin UTF-16-puskurina, mutta sen indeksit eivät ole sama indeksiavaruus kuin merkki-indeksit, joita FPDFText_GetCharBox ja FPDFText_CountChars käyttävät. Aiemmin käsitellyt generoidut merkit istuvat tekstipuskurissa samalla kun ne miehittävät merkkipaikkoja ilman käyttökelpoista geometriaa, ja kaksi numerointia ajautuvat erilleen sivun yli. Sillat ovat FPDFText_GetTextIndexFromCharIndex ja FPDFText_GetCharIndexFromTextIndex, käärittynä PDFium Componentin toimesta CharacterIndexToTextIndex:ksi ja TextIndexToCharacterIndex:ksi

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;

Suunta, joka puree kovimmin, on käänteinen. Poimitun merkkijonon yli toteutettu haku antaa sinulle tekstin indeksejä, ja niiden syöttäminen suoraan laatikko- tai valinta-API:in osoittaa hiljaa väärät merkit, virheellä, joka kasvaa mitä alemmas sivulla menet. Muunna TextIndexToCharacterIndex:lla ennen kuin mikään geometrinen koskettaa lukua. Sijaisparit lisäävät toisen, itsenäisen siirtymäongelman tämän päälle, mikä käsitellään artikkelissa emojit, CJK ja sijaisparit

Missä heuristiikka taipuu

Ole rehellinen itsellesi rajoista, koska ne ovat todellisia ja saavutettavissa. Käännetty teksti on selvin tapaus: merkkilaatikko on akselinsuuntainen suorakulmio sivuavaruudessa, joten 90 astetta käännetylle tekstille yhden visuaalisen rivin laatikkojen pystysuuntaiset keskipisteet leviävät sivun yli, ja skannaus pysähtyy lähes heti. Se, minkä saat, on lyhyt valinta väärän sijaan, mikä on parempi vikamoodi, mutta se on silti epäonnistuminen. Pystysuuntaiset kirjoitustilat käyttäytyvät samalla tavalla samasta syystä. Kaksipalstaiset asettelut toimivat, kun palstat ovat pystysuunnassa siirtyneet toisistaan, ja rikkoutuvat, kun eivät ole. Jos molemmat palstat jakavat perusviivaristikon, oikean palstan merkit istuvat vasemman palstan rivin toleranssin sisällä, ja skannaus ajaa suoraan kourun yli, koska puhtaassa geometriassa siellä ei ole mitään, mihin pysähtyä. Sen tunnistaminen tarvitsee vaakasuuntaisen välitestin pystysuuntaisen klusteroinnin päälle, ja välikynnyksen valitseminen on oma arviointipäätöksensä siitä, minkä asiakirjojen kanssa olet valmis olemaan väärässä. Sekakoossa olevat fontit ovat tapaus, jonka siemensuhteinen toleranssi käsittelee hyvin: sisäinen 8pt koodin pätkä 11pt leipätekstin sisällä pitää keskipisteensä vyön sisällä, eikä 24pt otsikko seuraavalla perusviivalla vedä leipätekstin riviä itseensä

Tässä kuvattu rivinvalintasemantiikka toimitetaan PDFium Componentissa Delphille ja C++Builderille, esimerkeissä käytettyjen osumatestaus-, valinta-alue- ja tekstiindeksi-API:en rinnalla; tuotesivu kantaa täyden viitteen tekstisivulle ja valintamallille