Tekninen artikkeli

PDF-tekstin poiminta Delphissä: sanavälit ja rivinvaihdot

HotPDF Delphi Component rakentaa sanavälit ja rivinvaihdot metodissa THotPDF.ExtractLoadedPageText glyyfigeometriasta, eivät välilyöntimerkeistä. Väilyönti tulee väliin, kun glyyfin oman leveyden jälkeinen rako ylittää 0,15 tekstikorkeudesta, ja uusi rivi alkaa vasta, kun tekstiorigo siirtyy kirjoitussuunnan yli yli puolet tekstikorkeudesta. Versiosta v2.768.3 alkaen sivuteksti sisältää myös Form XObjectien kautta piirretyn tekstin ja jättää pois näkyvän rajausalueen ulkopuoliset glyyfit. Tämän kirjoituksen loppuosa selittää, miksi jokainen sääntö näyttää siltä miltä näyttää, koska jokainen niistä korvasi yksinkertaisemman säännön, joka tuotti uskottavaa mutta väärää tulosta oikeissa asiakirjoissa

Oireet ovat tuttuja jokaiselle, joka on syöttänyt PDF-tekstiä hakemistoon. Kansisivu poimitaan muodossa PDFReferenceManualNovember4,1998, verolomake hajoaa 156 riviksi, vinossa oleva vesileima saapuu yksi merkki rivillä kohden, ja rajattu vedos alkaa tulostimen slug-rivillä, jota mikään katselin koskaan näytä. Yksikään näistä tiedostoista ei ole rikki. Jokainen käyttää täysin laillista tapaa sijoittaa tekstiä, jonka naiivi poimija lukee väärin

Miksi poimittu PDF-teksti menettää sanavälinsä?

Poimittu teksti menettää sanavälit, koska PDF:n ei koskaan vaadita sisältävän niitä. Tuottaja voi erottaa sanat näyttämällä välilyöntimerkin, mutta se voi yhtä hyvin siirtää kynää TJ-taulukon numerolla (ISO 32000-1 §9.4.3) tai tuoreella Tdllä (§9.4.2), ja TeX-tuloste, monet Distiller-tiedostot ja useimmat tasatut asettelut tekevät juuri niin. Ennen v2.766.76 HPDFAssemblePageText katsoi vain pystysuuntaista liikettä, joten sijoittelulla tehty sanaväli katosi yksinkertaisesti. Kokoonpanija mittaa nyt edellisen glyyfin kirjoitussuuntaa pitkin etäisyyden kyseisen glyyfin oman leveyden päästä nykyisen glyyfin origoon ja lisää yhden välilyönnin, kun etäisyys ylittää 0,15 nykyisen glyyfin laatikon korkeudesta, mitattuna ascentista descentiin käyttäjätilassa. Väilyöntiä ei lisätä, kun kumpikaan puoli on jo tyhjä, eikä kahden CJK-merkin väliin, koska tasa venyttää ideografeja erilleen ilman, että venytys tarkoittaisi sanarajaa. Glyyfitietueet paljastavat saman geometrian, joten voit toistaa päätöksen, kun jokin tiedosto askarruttaa

uses
  SysUtils, HPDFDoc, HPDFContentStream;

procedure DumpWordGaps(Pdf: THotPDF; PageIndex: Integer);
var
  Glyphs: THPDFGlyphArray;
  I: Integer;
  Height, Gap: Double;
begin
  if not Pdf.ExtractLoadedPageGlyphs(PageIndex, Glyphs) then
    Exit;
  for I := 1 to High(Glyphs) do
  begin
    // glyyfilaatikon ascentista descentiin -korkeus, käyttäjätilassa
    Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
      Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
    // vaakateksti: rako edellisen glyyfin oman leveyden päästä
    Gap := Glyphs[I].BaselineStartX - Glyphs[I - 1].GlyphEndX;
    if (Height > 0) and (Gap > 0.15 * Height) then
      Writeln(Format('U+%.4x gap %.2f height %.2f: space',
        [Glyphs[I].Unicode, Gap, Height]));
  end;
end;

Miksi mitata glyyfin omasta leveydestä kynäaseman sijaan?

HotPDF mittaa sanaraot arvoista GlyphEndX / GlyphEndY, koska glyyfin jälkeinen kynäasema sisältää jo väistystä, joka ei ole rako. ISO 32000-1 §9.4.4 määrittelee vaakasiirtymän kaavana glyyfin leveys kertaa fonttikoko, plus merkistäväystä Tc, plus sanaväli Tw, kaikki skaalattuna arvolla Tz. BaselineEndX / BaselineEndY kantavat koko siirtymän, kun taas GlyphEndX / GlyphEndY kantavat vain fontin etenemän ja Tzin. Ero merkitsee tuottajille, jotka kiristävät kirjainsväystä negatiivisella Tcllä ja antavat tilan sitten takaisin TJ-säädön kautta jokaisen glyyfin jälkeen: kynäasemasta mitattuna takaisinanto näyttää raolta, ja kiinalainen termi “95后” poimintaan muodossa “9 5 后”. Kynnys on sidottu glyyfilaatikon korkeuteen Tf-koon sijaan samasta syystä. Word-viennit kirjoittavat usein arvon 1 Tf ja kantavat oikeaa kokoa skaalatussa Tmssä, joten Tfs sanoo 1, vaikka teksti on 10 pistettä korkea, ja Tfs:een sidottu sääntö kohtelisi saman sivun kahta kirjoitusasua eri tavalla

HotPDF:n sanavälisääntö funktiolle ExtractLoadedPageText Delphissä: välilyönti lisätään vain, kun etäisyys edellisen glyyfin GlyphEndX:stä seuraavan glyyfin BaselineStartX:hen ylittää 0,15 ascentista descentiin mitatusta laatikkokorkeudesta, koska kynäasema BaselineEndXissä sisältää jo Tc:n, Tw:n ja Tz:n ja kääntää tasattujen kirjainsväysten takaisinannot vääriksi raoksi kuten 9 5 后
Geometria, eivät välilyöntimerkit, päättävät mihin sanat katkeavat — glyyfitietueet paljastavat samat mittaukset, joten voit toistaa päätöksen mille tahansa askarruttavalle tiedostolle

Säännöllä on rehelliset reunat. Otsikko, joka on ladattu hyvin löyhällä kirjainsväyställä, jossa pelkkä Tc avaa yli 0,15 tekstikorkeudesta kirjainten väliin, poimitaan välilyönnillä jokaisen kirjaimen välissä, mikä on se, miltä sivu näyttää, mutta tuskin sitä, mitä halusit indeksoida. Poikkeuksellisessa järjestyksessä samalle peruslinjalle piirretyt palaset tuottavat negatiivisen raon ja liittyvät ilman välilyöntiä. Kumpikaan tapaus ei ole yleinen leipätekstissä, ja testikorpuksella muutos nosti sanatäsmäyksiä referenssipoimijaan verrattuna 28 sivulla laskematta yhtään

Milloin HotPDF aloittaa uuden rivin poimitussa tekstissä?

Versiosta v2.766.79 alkaen uusi rivi alkaa, kun siirtymä edellisen glyyfin origosta nykyiseen, projisoituna edellisen kirjoitussuunnan normaalin päälle, ylittää puolet glyyfien kahdesta suuremmasta laatikkokorkeudesta. Aiempi sääntö vertasi raakaa Y-siirtymää puoleen arvosta Tfs, mikä epäonnistui kahteen suuntaan. Arvolla 1 Tf ja skaalatulla Tmillä kynnys kutistui puoleen yksikköön, joten yläindeksi, jonka tekstin noste on 0,4, tai tavallinen peruslinjan tärinä rikkoi rivin. Sääntö ohitti myös X:n kokonaan, joten kierretyn Tmin alla oleva teksti astui alas sivua jokaisen glyyfin kohdalla ja tuli ulos yhtenä glyyfinä per rivi. Projektointi suunnan normaalin päälle saa kierretyt ajot käyttäytymään kuin vaakasuoria, ja kahdesta korkeudesta suuremman ottaminen pitää ison näyttesanan ja sen pienen kuvatekstin yhdellä rivillä, kun ne jakavat peruslinjan. Yllä mainitulla verolomakkeella rivimäärä putosi arvosta 156 arvoon 97. Pystyteksti kirjoitustilassa 1 (§9.7.4.3) seuraa erillistä polkua: ne glyyfit ryhmitellään sarakkeisiin, luetaan oikealta vasemmalle ja ylhäältä alas, rivinvaihdolla jokaisen sarakevaihdoksen kohdalla

Miten HotPDF:n ExtractLoadedPageText päättää rivinvaihdot Delphissä: glyyfien origojen välinen siirtymä projisoidaan kirjoitussuunnan normaalin päälle ja verrataan puoleen suuremmasta laatikkokorkeudesta, joten yläindeksi, jonka nostaa pieni tekstin noste arvon 1 Tf fontin alla, eikä teksti, joka astuu alas sivua kierretyn Tm:n alla, hajoa enää yhdeksi glyyfiksi riville
Projektointi saa kierretyt ajot käyttäytymään kuin vaakasuoria, ja kahdesta laatikkokorkeudesta suuremman ottaminen pitää ison näyttesanan ja sen pienen kuvatekstin yhdellä rivillä

Minkä tekstin ExtractLoadedPageText ottaa mukaan ja minkä jättää pois?

ExtractLoadedPageText palauttaa tekstin, jonka katselin näyttää. Versiosta v2.766.80 alkaen se toimii vain näkyvistä glyyfeistä pudottaen jokaisen glyyfin, jonka laatikon keskipiste putoaa ulos GetLoadedPageVisibleBoxista, joka on CropBox rajattuna MediaBoxiin (§14.11.2). Se poistaa slug-rivit ja muut tulostimen merkinnät, jotka on ladottu tekstinä rajausalueen ulkopuolelle. ExtractLoadedPageGlyphs tahallaan jatkaa jokaisen sivun sisältöstreamin glyyfin palauttamista, joten löydät kyseisen materiaalin yhä, kun tarvitset sitä. Suodatin on laatikkotesti, ei näkyvyystesti: leikkauspolulla piilotettu, valkoisella maalattu tai kuvan peittämä teksti poimitaan yhä

var
  Pdf: THotPDF;
  Glyphs: THPDFGlyphArray;
  PageText: UnicodeString;
  L, B, R, T: Single;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('trimmed-proof.pdf');
    if Pdf.GetLoadedPageVisibleBox(0, L, B, R, T) then
      Writeln(Format('Visible box: %.1f %.1f %.1f %.1f', [L, B, R, T]));
    // jokainen sivun sisältöstreamin glyyfi, slug-rivi mukaan lukien
    if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
      Writeln(Length(Glyphs), ' glyphs in the page content stream');
    // vain se, mitä sivu näyttää, Form XObject -teksti sisällytettynä
    if Pdf.ExtractLoadedPageText(0, PageText) then
      Writeln(PageText);
  finally
    Pdf.Free;
  end;
end;

Form XObjectien kautta piirretty teksti kuuluu sivutekstiin versiosta v2.768.3 alkaen. Otsikot, leimat ja vesileimat asuvat hyvin usein formeissa, ja jotkut standardiasiakirjat menettivät ennen muutosta 30–35 prosenttia merkeistään. THotPDF.InterpretContentWithForms kirjaa jokaisen Don yhdessä voimassa olevan CTM:n kanssa, tulkitsee formin sen /Matrixin kertaa kyseinen CTM (§8.10.1) ja liittää formin glyyfit paikoilleen Don kohdalle, rekursiolla sisäkkäisiin formeihin. Formi ilman omaa /Resourcesia lainaa sen streamin resurssit, joka sen piirtää, niin kuin §7.8.3 sallii. Formiglyyfit kantavat arvoa TokenIndex = -1, ja ExtractLoadedPageGlyphs palauttaa yhä vain sivustreamin glyyfit, koska haku, korvaus ja mustaus kirjoittavat muutokset takaisin TokenIndexin kautta ja muokkaisivat vääriä tavuja, jos formiglyyfi sujahtaisi joukkoon. Kaksi yksinkertaistusta on syytä tuntea: formitekstiä ei rajata formin /BBoxiin, ja rekursio pysähtyy 12 tasoon syklintunnistuksen sijaan, joten väärinmuodostettu formi, joka piirtää itsensä, toistaa tekstinsä kunnes saavuttaa tuon katon

Mitkä glyyfit HotPDF ottaa mukaan poimiessaan PDF-sivun tekstiä Delphissä: ExtractLoadedPageText pitää vain glyyfit, joiden laatikon keskipiste putoaa sisälle GetLoadedPageVisibleBoxiin, eli CropBox rajattuna MediaBoxiin, joten tulostimen slug-rivit katoavat, kun taas InterpretContentWithForms liittää Form XObject -glyyfit jokaisen Do:n kohdalle arvolla TokenIndex = -1, ja glyyfitason API palauttaa yhä kaiken
Laatikkotesti glyyfin keskipisteeseen ei ole näkyvyystesti — valkoinen teksti, leikattu teksti ja peitetty teksti tulevat yhä ulos, ja formiteksti lasketaan mukaan versiosta v2.768.3 alkaen

Miksi Q-operaattorin jälkeinen teksti dekoodautui rosaksi?

Teksti Q:n jälkeen saattoi dekoodautua väärin ennen v2.766.73, koska poimija talletti vain CTM:n qssa. Tekstitilan parametrit, nimittäin fontti, koko, Tc, Tw, Tz, TL, renderöintitila ja noste, kuuluvat grafiikkatilaan (§9.3.1), joten Q:n on palautettava ne kaiken muun pinossa olevan mukana (§8.4.2). Eräs toimialaraportti valitsi kaksitavuisen Identity-H-fontin q … Qin sisällä ja näytti sitten yksitavuista WinAnsi-tekstiä ilman omaa Tfä. Poimija piti sisäisen fontin, luki johdantoviivat ja sanan “Adobe” sisällysluettelossa kaksitavuisina koodina ja pudotti 15 % sivun merkeistä. Tulkki q/Q-pinossaan pitää nyt koko tekstitilaa. Tässä kuvatut poimintasäännöt koskevat jokaista sivua, joten koko asiakirja mahtuu tiedostoon yhdellä kutsulla

var
  Output: TFileStream;
  Pages: Integer;
begin
  Output := TFileStream.Create('report.txt', fmCreate);
  try
    // tyhjä alue = jokainen sivu; sivunvaihtomerkki sivujen väliin; UTF-8 BOM
    Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
    Writeln(Pages, ' pages extracted');
  finally
    Output.Free;
  end;
end;

Mitä HotPDF:n teksti-API:a kannattaa käyttää?

ExtractLoadedPageText pysyy sisältöstreamin järjestyksessä, mikä on oikea oletus hakuun ja indeksointiin; sen alla oleva dekoodausketju on käsitelty kirjoituksessa tekstin poiminta ladatuista PDF:istä HotPDF:llä. Tagitetuille asiakirjoille, joiden laatimisjärjestyksellä on väliä, kirjoitus rakennejärjestyksessä tapahtuva tekstin poiminta kävelee rakennepuun geometriasta arvaamisen sijaan, ja taulukoihin lukittuun dataan kirjoitus tyypitetty taulukon poiminta sivunvaihtojen yli palauttaa solut eikä rivejä. Täysi API-referenssi ja kokeiluversio ovat HotPDF Delphi PDF Component -tuotesivulla