Tekninen artikkeli

Tekstin poimiminen PDF-tiedostoista Delphissä PDFium-komponentilla

PDF-tekstin poimiminen vaikuttaa yksinkertaiselta, kunnes kohtaat asiakirjan, josta tekstikerros puuttuu, on korruptoitunut tai jaettu kymmeniin pikkuruisiin merkkijonoihin ilman loogista järjestystystä. PDFium-komponentti tarjoaa kaksi sisääntulopistettä: Character[]-taulukon raakaan, indeksipohjaiseen pääsyyn sivun jokaiseen glyyfiin, sekä ReadablePageContent-metodin rakenteelliseen näkymään, joka muodostaa kappaleet ja otsikot uudelleen PDF:n tagipuun tai heuristisen analyysin perusteella. Kumpikaan ei ole aina oikea valinta, joten niiden tarjoamien tietojen ymmärtämisellä on merkitystä

Asiakirjan avaaminen ja hiljaisen epäonnistumisen sudenkuoppa

TPdf avaa tiedoston asettamalla FileName-propertyn ja kytkemällä Active := True. Kriittinen yksityiskohta: Active := True ei koskaan heitä poikkeusta. Jos tiedosto puuttuu, on suojattu salasanalla tai vioittunut, PDFium havaitsee virheen sisäisesti ja Active pysyy yksinkertaisesti tilassa False. Tämä tarkoittaa, että jokaisen poimintasilmukan on suojauduttava tältä:

Pdf := TPdf.Create(nil);
try
  Pdf.FileName := 'report.pdf';
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    ShowMessage('Could not open PDF (damaged or wrong password)');
    Exit;
  end;
  // extraction follows here
finally
  Pdf.Active := False;
  Pdf.Free;
end;

Salasanalla suojatut tiedostot vaativat asetuksen Pdf.Password := '...' ennen kuin Active := True suoritetaan. Toista mahdollisuutta ei ole: kun Active epäonnistuu, suljet ja avaat uudelleen oikealla salasanalla

Sivuittaisten tekstien poiminta Character[]-taulukolla

Matalimman tason lähestymistapa käy läpi jokaisen merkin kullakin sivulla. Aseta Pdf.PageNumber ladataksesi kyseisen sivun tekstikerroksen, ja käy sitten läpi CharacterCount-määrän mukaiset merkinnät Character[]-ominaisuuden avulla. Kaksi lippua jokaisessa merkinnässä on syytä tarkistaa: CharacterGenerated[i] merkitsee renderöijän lisäämiä synteettisiä glyyfejä (kuten pehmeitä tavuviivoja rivinvaihdoissa), joilla ei ole todellista Unicode-arvoa, ja CharacterMapError[i] ilmoittaa, ettei PDFium pystynyt kartoittamaan glyyfiä koodipisteeksi, mikä tapahtuu fonttikoodauksissa, joista puuttuu ToUnicode-taulukko

procedure ExtractAllText(Pdf: TPdf; Output: TStrings);
var
  Page, I: Integer;
  Line: string;
  Ch: WideChar;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := Page;
    Line := '';
    for I := 0 to Pdf.CharacterCount - 1 do
    begin
      if Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I] then
        Continue;
      Ch := Pdf.Character[I];
      if Ch = #13 then
        Ch := #10;   // normalize CR to LF
      Line := Line + Ch;
    end;
    Output.Add(Line);
  end;
end;

Tuloksena on litteä merkkijono Unicode-koodipisteitä siinä järjestyksessä, jossa PDFium luettelee ne, eli järjestyksessä, jossa ne esiintyvät sisältövirrassa – ei välttämättä vasemmalta oikealle -lukujärjestyksessä. Useimmille latinalaisilla aakkosilla kirjoitetuille asiakirjoille, jotka on tuotettu tavallisilla toimistotyökaluilla, tämä on riittävää. Skannatuille PDF-tiedostoille, jotka on OCR-käsitelty epätavallisilla glyyfijärjestyksillä, tai oikealta vasemmalle kirjoitetulle tekstille järjestys voi olla väärä. Tällöin ReadablePageContent on hyödyllisempi

Rakenteinen poiminta ReadablePageContent-metodilla

ReadablePageContent nousee yhtä tasoa ylemmäksi: se palauttaa TPdfReadableContent-tietueen, jonka Fragments-taulukko kantaa tagitettuja sisältöfragmentteja. Jokaisella fragmentilla on Kind-arvo, joka tunnistaa kappaleet, otsikot, luettelomerkit, taulukon solut ja niin edelleen. Kun PDF-tiedostossa on rakennekuvaus eli tagipuu (tarkista Pdf.IsTagged), lähde on rosStructure ja lukujärjestys on auktoritatiivinen. Tagittamattomissa tiedostoissa PDFium turvautuu arvoon rosHeuristic, joka ryhmittelee merkit niiden rajoituslaatikoiden mukaan uskottaviksi lukuyksiköiksi mutta ei voi taata tarkkuutta

procedure ExtractStructured(Pdf: TPdf; Output: TStrings);
var
  Page: Integer;
  Content: TPdfReadableContent;
  Fragment: TPdfContentFragment;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Content := Pdf.ReadablePageContent(Page);
    for Fragment in Content.Fragments do
    begin
      case Fragment.Kind of
        cfHeading   : Output.Add('# ' + Fragment.Text);
        cfParagraph : Output.Add(Fragment.Text);
        cfListItem  : Output.Add('- ' + Fragment.Text);
      else
        Output.Add(Fragment.Text);
      end;
    end;
  end;
end;

Jos Content.Source = rosHeuristic ja tulosteesi näyttää sekavalta, asiakirjan tekstikerrosta ei todennäköisesti kirjoitettu lukujärjestystä silmällä pitäen. Siinä vaiheessa ainoa luotettava korjaus on viedä asiakirja uudelleen lähdesovelluksesta asianmukaisella tagituksella, tai ajaa jälkikäsittelyvaihe, joka lajittelee merkkien origot Y-koordinaatin ja sitten X-koordinaatin mukaan

Mitä CharacterOrigin ja CharacterRectangle tarjoavat

Molemmat propertyt palauttavat merkin sijainnin sivun avaruudessa (pisteinä, origo vasemmassa alakulmassa, Y kasvaa ylöspäin). CharacterOrigin[i] on glyyfin perusviivan ankkuripiste; CharacterRectangle[i] on koko rajoituslaatikko (bounding box). Nämä ovat rakennuspalikoita kaikelle pelkän tekstin ulkopuolelle menevälle: sarakerajojen havaitsemiselle, merkkijonojen ryhmittelylle riveiksi vertaamalla Y-koordinaatteja toleranssin rajoissa tai osumatestikartan rakentamiselle tekstin valintaa varten katseluohjelmassa. Jos sinun on löydettävä, mikä merkki on hiiren napsautuksen alla, CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) tekee tuon haun suoraan ilman, että joudut käymään suorakulmioita läpi silmukassa

DLL-kirjaston saaminen paikalleen

PDFium-komponentti delegoi kaiken PDF-jäsennyksen natiiville DLL-kirjastolle, joko pdfium32.dll- tai pdfium64.dll-tiedostolle kohdealustastasi riippuen. Komponentin mukana toimitetaan CopyDlls.bat-skripti, joka kopioi oikean tiedoston Windowsin järjestelmähakemistoon. Sen suorittaminen kerran järjestelmänvalvojana kehityskoneella riittää; jakelussa kopioit DLL-tiedoston sovelluksen ajotiedoston viereen. V8-käyttöiset variantit (pdfium32v8.dll, pdfium64v8.dll) ovat huomattavasti suurempia ja tarpeellisia vain, jos PDF-tiedostosi sisältävät suoritettavaa JavaScriptiä. Tekstin poimintaan standardiversio on oikea valinta

Jos DLL-kirjasto puuttuu ajon aikana, Active := True epäonnistuu hiljaisesti aivan kuten puuttuvan tiedoston tapauksessa, koska komponentti ottaa latausvirheen kiinni sisäisesti. Testaa aina puhtaalla koneella ennen toimitusta

FontSize[]-taulukon käyttö Character[]-taulukon ohella asetteluanalyysissä

Pelkän tekstin lisäksi merkkitasoinen API tuo esiin FontSize[i]-arvon, joka palauttaa kunkin glyyfin renderöidyn pistekoon. Yhdessä CharacterOrigin[i]- ja CharacterRectangle[i]-tietojen kanssa tämä antaa sinun erottaa leipätekstin otsikoista ilman rakennekuvaukseen turvautumista. Merkkijono, jossa fonttikoko hyppää kynnysarvon yli, on lähes varmasti otsikko tagittamattomassa asiakirjassa. Sama tekniikka pätee kuvatekstien (pieni teksti kuvan rajoituslaatikon alla) tai alaviitteiden (pieni teksti lähellä sivun alareunaa) tunnistamiseen. Mikään tästä ei vaadi renderöintiä; kaikki kolme propertyä lukevat suoraan tekstikerrosta, jonka PDFium rakentaa Active := True -vaiheessa

Eräs vivahde: FontSize[i] heijastaa kokoa sen jälkeen, kun sivun CTM-matriisi (current transformation matrix) on sovellettu, joten asiakirja, jossa tekijä on skaalannut koko sivun, raportoi suhteellisesti mukautetut koot. Jos vertailet kokoja eri sivuilla, joilla on eri sivumitat, normalisoi ne kunkin sivun MediaBox-korkeutta vasten ennen kynnyspäätösten tekemistä

Tulosteen kirjoittaminen tiedostoon

Delphin TStringList käsittelee UTF-8-tulostetta siististi XE-versiosta alkaen. Aseta WriteBOM := False, jos tarvitset BOM-vapaan tiedoston ( monet jatkokäsittelijät takertuvat alussa olevaan BOM-merkkiin):

var
  Lines: TStringList;
begin
  Lines := TStringList.Create;
  try
    ExtractAllText(Pdf, Lines);
    Lines.WriteBOM := False;
    Lines.SaveToFile('output.txt', TEncoding.UTF8);
  finally
    Lines.Free;
  end;
end;

Erittäin suurissa asiakirjoissa, joissa muisti on huolenaihe, kirjoita suoraan TStreamWriter-olioon käyttäen TEncoding.UTF8-koodausta sivusilmukan sisällä sen sijaan, että keräisit kaiken ensin listaan

Tässä esitetyt Character[]-, CharacterCount-, CharacterOrigin[]-, CharacterRectangle[]-, ReadablePageContent- ja CharacterIndexAtPos-API:t ovat osa Delphille ja C++Builderille tarkoitettua PDFium-komponenttia