Technical Article

Teksto išgavimas iš PDF failų naudojant PDFium VCL programoje Delphi

Teksto išgavimas iš PDF atrodo paprastas tol, kol nesusiduriate su dokumentu, kuriame tekstinis sluoksnis neegzistuoja, yra sugadintas arba išbarstytas po dešimtis smulkių simbolių eilučių be jokios prasmingos tvarkos. „PDFium VCL“ siūlo du prieigos taškus: Character[] masyvą tiesioginei prieigai prie kiekvieno puslapio simbolio pagal indeksą ir ReadablePageContent struktūrizuotam vaizdui gauti, kuris atkuria pastraipas ir antraštes iš PDF struktūros medžio arba heuristinės analizės. Nei vienas iš jų nėra universalus sprendimas, todėl svarbu suprasti, ką kiekvienas iš jų pateikia.

Dokumento atidarymas ir tyliosios nesėkmės spąstai

TPdf atidaro failą nustatant savybę FileName ir priskyrus reikšmę Active := True. Svarbi detalė: Active := True niekada nesukelia išimties klaidos. Jei failo nėra, jis apsaugotas slaptažodžiu arba sugadintas, PDFium apdoroja šią klaidą viduje, o Active savybė tiesiog lieka False. Tai reiškia, kad kiekvienas išgavimo ciklas turi turėti tokį apsauginį patikrinimą:

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;

Slaptažodžiu apsaugotiems failams prieš nustatant Active := True reikia nurodyti Pdf.Password := '...'. Antro šanso nėra: jei Active nustatymas nepavyko, turite uždaryti komponentą ir atidaryti jį iš naujo su teisingu slaptažodžiu.

Teksto išgavimas po puslapį naudojant Character[]

Žemiausio lygio metodas apima kiekvieno puslapio simbolio patikrą. Nustatykite Pdf.PageNumber, kad įkeltumėte to puslapio tekstinį sluoksnį, o kavada atlikite ciklą per CharacterCount simbolių skaičių, naudodami savybę Character[]. Verta patikrinti dvi vėliavėles kiekvienam simboliui: CharacterGenerated[i] pažymi sintetinius simbolius, kuriuos įterpė atvaizdavimo variklis (pavyzdžiui, minkštuosius brūkšnelius eilučių sandūrose) ir kurie neturi tikros „Unicode“ reikšmės, o CharacterMapError[i] rodo, kad PDFium negalėjo susieti simbolio su kodu (taip nutinka su šriftų kodavimais, neturinčiais ToUnicode lentelės).

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;

Rezultatas yra paprasta „Unicode“ simbolių eilutė ta tvarka, kuria juos pateikia PDFium. Ši tvarka atitinka turinio srauto eilę ir nebūtinai sutampa su skaitymo tvarka iš kairės į dešinę. Daugumai lotyniškų dokumentų, sukurtų standartiniais biuro įrankiais, šio metodo pakanka. Tačiau nuskenuotuose PDF failuose, kurie buvo atpažinti naudojant OCR programinę įrangą su neįprasta simbolių seka, arba tekste iš dešinės į kairę, tvarka gali būti klaidinga. Tokiais atvejais naudingiau naudoti ReadablePageContent.

Struktūrizuotas teksto išgavimas naudojant ReadablePageContent

Metodas ReadablePageContent veikia vienu lygiu aukščiau: jis grąžina TPdfReadableContent įrašą, kurio Fragments masyvas turi pažymėtus turinio fragmentus. Kiekvienas fragmentas turi tipą Kind, kuris identifikuoja pastraipas, antraštes, sąrašo elementus, lentelės langelius ir t. t. Kai PDF turi struktūros medį (patikrinkite Pdf.IsTagged), šaltinis yra rosStructure ir skaitymo tvarka yra autoritetinga. Nepažymėtuose failuose PDFium naudoja rosHeuristic režimą, kuris sugrupuoja simbolius pagal their bounding boxes (ribojančius rėmelius) į tikėtinus skaitymo blokus, tačiau negali garantuoti visiško tikslumo.

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;

Jei Content.Source = rosHeuristic ir gautas rezultatas atrodo sumaišytas, tikriausiai dokumento tekstinis sluoksnis buvo įrašytas nepaisant skaitymo tvarkos. Tokiu atveju vienintelis patikimas sprendimas yra eksportuoti dokumentą iš naujo iš pradinės programos su teisingu žymėjimu arba pritaikyti papildomą apdorojimą, kuris surūšiuoja simbolių pradžios koordinates pagal Y ir X ašis.

Ką pateikia CharacterOrigin ir CharacterRectangle

Abi savybės grąžina simbolio poziciją puslapio erdvėje (taškais, atskaitos taškas yra apatiniame kairiajame kampe, Y ašis didėja į viršų). CharacterOrigin[i] nurodo simbolio bazinės linijos inkarą; CharacterRectangle[i] pateikia pilną ribojantį rėmelį. Tai yra pagrindiniai elementai kuriant sudėtingesnes funkcijas nei paprastas teksto nuskaitymas: stulpelių ribų nustatymui, simbolių grupavimui į eilutes lyginant Y koordinates su leistina paklaida arba teksto pasirinkimo žemėlapio kūrimui peržiūros lange. Jei norite sužinoti, koks simbolis yra po pelės paspaudimu, funkcija CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) atlieka šią paiešką tiesiogiai, todėl jums nereikia tikrinti visų stačiakampių rankiniu būdu.

DLL bibliotekos paruošimas darbui

„PDFium VCL“ visą PDF analizavimą perleidžia vietinei DLL bibliotekai – pdfium32.dll arba pdfium64.dll, priklausomai nuo jūsų taikomosios platformos. Komponentas platinamas kartu su CopyDlls.bat scenarijumi, kuris nukopijuoja reikiamą failą į „Windows“ sistemos katalogą. Kūrimo kompiuteryje užtenka vieną kartą paleisti jį administratoriaus teisėmis; diegimui pas klientus nukopijuokite DLL failą šalia vykdomojo programos failo. Variantai su V8 variklio palaikymu (pdfium32v8.dll, pdfium64v8.dll) yra žymiai didesni ir reikalingi tik tuo atveju, jei jūsų PDF dokumentuose yra JavaScript kodas, kurį būtina įvykdyti. Paprastam teksto išgavimui pilnai pakanka standartinės bibliotekos versijos.

Jei vykdymo metu DLL bibliotekos nebuvo, savybės Active := True nustatymas nepavyks tyliai, kaip ir trūkstamo failo atveju, nes komponentas klaidą apdoroja viduje. Prieš pateikdami programą klientams, visada patikrinkite ją švarioje operacinėje sistemoje.

FontSize[] ir Character[] naudojimas maketo analizei

Be paprasto teksto, simbolių lygio API pateikia savybę FontSize[i], kuri grąžina kiekvieno simbolio atvaizduojamą dydį taškais. Kartu su CharacterOrigin[i] ir CharacterRectangle[i] tai leidžia atskirti pagrindinį tekstą nuo antraščių nenaudojant struktūros medžio. Simbolių seka, kurioje šrifto dydis viršija nustatytą ribą, nepažymėtame dokumente beveik neabejotinai yra antraštė. Tačia technika tinka ir užrašams po vaizdais (mažas tekstas po vaizdo rėmeliu) arba išnašoms puslapio apačioje aptikti. Tam nereikia atlikti atvaizdavimo – visos trys savybės nuskaitomos tiesiai iš tekstinio sluoksnio, kurį sukuria PDFium priskyrus Active := True.

Vienas niuansas: FontSize[i] grąžina dydį pritaikius puslapio CTM (dabartinę transformacijos matricą), todėl dokumente, kur autorius pakeitė viso puslapio mastelį, dydžiai bus proporcingai pakoreguoti. Jei lyginate dydžius skirtingų matmenų puslapiuose, prieš priimdami sprendimus dėl ribinių reikšmių, normalizuokite juos pagal kiekvieno puslapio MediaBox aukštį.

Rezultato įrašymas į failą

„Delphi“ klasė TStringList tvarkingai apdoroja UTF-8 išvestį pradedant nuo XE versijos. Nustatykite savybę WriteBOM := False, jei reikia failo be BOM žymos (dauguma vėlesnių sistemų tikisi būtent tokio formato):

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;

Apdorojant labai didelius dokumentus, kai svarbu taupyti atmintį, geriau rašyti tiesiai į TStreamWriter su TEncoding.UTF8 puslapių cikle, užuot iš pradžių kaupus visą tekstą sąraše.

Šiame straipsnyje aprašyti Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent ir CharacterIndexAtPos metodai yra dalis bibliotekos PDFium VCL Component, skirtos Delphi ir C++Builder programuotojams.