Technical Article

Črpanje besedila iz datotek PDF s komponento PDFium VCL v Delphi

Črpanje besedila iz datotek PDF se zdi preprosto, dokler ne naletite na dokument, v katerem besedilna plast manjka, je poškodovana ali razdeljena na desetine majhnih nizov znakov brez smiselnega vrstnega reda. PDFium VCL ponuja dve vstopni točki: polje Character[] za neposreden, na indeksih temelječ dostop do vsakega glifa na strani, in ReadablePageContent za strukturiran pogled, ki rekonstruira odstavke in naslove iz drevesa oznak PDF ali hevristične analize. Nobena od teh možnosti ni vedno prava izbira, zato je pomembno razumeti, kaj posamezna metoda ponuja.

Odpiranje dokumenta in past tihega neuspeha

TPdf odpre datoteko tako, da nastavi FileName in preklopi Active := True. Ključna podrobnost: nastavitev Active := True nikoli ne sproži izjeme. Če datoteka manjka, je zaščitena z geslom ali poškodovana, PDFium interno ujame napako, lastnost Active pa preprosto ostane False. To pomeni, da se mora vsaka zanka za črpanje zaščititi pred tem:

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;

Datoteke, zaščitene z geslom, zahtevajo nastavitev Pdf.Password := ‘...’ pred nastavitvijo Active := True. Druge priložnosti ni: ko Active ne uspe, morate zapreti in znova odpreti s pravilnim geslom.

Črpanje stran za stranjo s Character[]

Najbolj nizkonivojski pristop pregleda vsak znak na vsaki strani. Nastavite Pdf.PageNumber, da naložite besedilno plast za to stran, nato pa se pomaknite skozi vnose CharacterCount z uporabo lastnosti Character[]. Pri vsakem vnosu je smiselno preveriti dve zastavici: CharacterGenerated[i] označuje sintetične glife, ki jih je vstavil upodabljalnik (na primer mehke vezaje ob prelomih vrstic), ki nimajo dejanske vrednosti Unicode, in CharacterMapError[i], ki sporoča, da PDFium ni mogel preslikati glifa v kodno točko, kar se zgodi pri kodiranju pisav brez tabele ToUnicode.

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;

Rezultat je preprost niz kodnih točk Unicode v vrstnem redu, kot jih PDFium našteje, ku je vrstni red, v katerem se pojavijo v toku vsebine, in ni nujno bralni vrstni red od leve proti desni. Za večino dokumentov v latinici, ki so ustvarjeni s standardnimi pisarniškimi orodji, je to povsem primerno. Pri skeniranih dokumentih PDF, ki so bili pretvorjeni z OCR z nenavadnimi zaporedji glifov, ali pri besedilu od desne proti levi, pa je vrstni red lahko napačen. V teh primerih je bolj uporabna funkcija ReadablePageContent.

Strukturirano črpanje z ReadablePageContent

Funkcija ReadablePageContent deluje na višjem nivoju: vrne zapis TPdfReadableContent, katerega polje Fragments vsebuje označene fragmente vsebine, pri čemer vsak vsebuje Kind, ki določa odstavke, naslove, elemente seznama, celice tabel in podobno. Če PDF vsebuje strukturno drevo (preverite Pdf.IsTagged), je vir rosStructure, vrstni red branja pa je zanesljiv. Za neoznačene datoteke PDFium uporabi metodo rosHeuristic, ki združuje znake glede na njihove omejitvene okvirje v verjetne bralne enote, vendar ne more jamčiti natančnosti.

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;

Če je Content.Source = rosHeuristic in je vaš izhod popačen, besedilna plast dokumenta verjetno ni bila zapisana z mislijo na bralni vrstni red. V tem primeru je edina zanesljiva rešitev ponoven izvoz iz izvorne aplikacije s pravilnim označevanjem ali izvedba koraka poobdelave, ki razvrsti izhodišča znakov po osi Y in nato po osi X.

Kaj omogočata CharacterOrigin in CharacterRectangle

Obe lastnosti vračata položaj znaka v prostoru strani (točke, izhodišče v spodnjem levem kotu, os Y narašča navzgor). CharacterOrigin[i] je sidrna točka osnovne črte glifa; CharacterRectangle[i] je celoten omejitveni okvir. To so gradniki za vse, kar presega navadno besedilo: zaznavanje meja stolpcev, združevanje znakov v vrstice s primerjavo koordinat Y znotraj tolerance ali izdelava zemljevida klikov za izbiro besedila v pregledovalniku. Če želite ugotoviti, kateri znak se nahaja pod klikom miške, funkcija CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) opravi to poizvedbo neposredno, ne da bi vam bilo treba pregledovati pravokotnike.

Namestitev datoteke DLL

PDFium VCL prenese celotno razčlenjevanje PDF na izvorno knjižnico DLL, bodisi pdfium32.dll or pdfium64.dll, odvisno od vaše ciljne platforme. Komponenta vsebuje skript CopyDlls.bat, ki kopira pravilno datoteko v sistemski imenik Windows. Enkratni zagon kot skrbnik na razvojnem računalniku zadostuje; za namestitev pri strankah pa kopirajte knjižnico DLL poleg same izvršljive datoteke aplikacije. Različici s podporo za V8 (pdfium32v8.dll, pdfium64v8.dll) sta bistveno večji in potrebni le, če vaši PDF-ji vsebujejo kodo JavaScript, ki jo je treba izvesti. Za čisto črpanje besedila je standardna različica prava izbira.

Če knjižnica DLL ob zagonu ni prisotna, nastavitev Active := True tiho ne uspe, enako kot pri manjkajoči datoteki, saj komponenta vrne napako pri nalaganju. Pred izdajo aplikacijo vedno preizkusite na čistem računalniku.

Uporaba FontSize[] skupaj s Character[] za analizo postavitve

Poleg navadnega besedila API na ravni znakov ponuja lastnost FontSize[i], ki vrne izrisano velikost vsakega glifa v točkah. V kombinaciji s CharacterOrigin[i] in CharacterRectangle[i] vam to omogoča razlikovanje med osnovnim besedilom in naslovi brez uporabe strukturnega drevesa. Zaporedje znakov, kjer velikost pisave skoči nad določen prag, je v neoznačenem dokumentu skoraj zagotovo naslov. Isti postopek velja za zaznavanje napisov (majhno besedilo pod omejitvenim okvirjem slike) ali opomb (majhno besedilo blizu dna strani). Nič od tega ne zahteva izrisovanja; vse tri lastnosti berejo neposredno iz besedilne plasti, ki jo PDFium zgradi med izvajanjem Active := True.

Ena podrobnost: FontSize[i] odraža velikost po uporabi matrike transformacije strani (CTM), zato bo dokument, v katerem je avtor spremenil merilo celotne strani, vrnil sorazmerno prilagojene velikosti. Če primerjate velikosti na straneh z različnimi dimenzijami, jih pred odločanjem o pragovih normalizirajte glede na višino MediaBox posamezne strani.

Zapisovanje izhoda v datoteko

Delphijev razred TStringList od različice XE dalje čisto upravlja z izhodom UTF-8. Nastavite WriteBOM := False, če potrebujete datoteko brez oznake BOM (mnogi zunanji sistemi jo pričakujejo):

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;

Za zelo velike dokumente, kjer je pomemben pomnilnik, pišite neposredno v TStreamWriter s kodiranjem TEncoding.UTF8 znotraj zanke strani, namesto da bi vse najprej kopičili v seznamu.

Tukaj prikazani vmesniki API Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent in CharacterIndexAtPos so del komponente PDFium VCL Component za Delphi in C++Builder.