Articol tehnic

Extragerea textului din fișiere PDF în Delphi cu PDFium Component

Extragerea textului dintr-un PDF pare simplă până când dați de un document unde stratul de text lipsește, este corupt sau este împărțit în zeci de fragmente mici de caractere fără o ordine logică. PDFium Component vă oferă două puncte de acces: tabloul Character[] pentru acces brut, bazat pe index, la fiecare glifă de pe o pagină, și ReadablePageContent pentru o vizualizare structurată care reconstruiește paragrafele și antetele din arborele de etichete (tag tree) al PDF-ului sau prin analiză euristică. Niciuna dintre acestea nu este întotdeauna alegerea ideală, așa că înțelegerea a ceea ce expune fiecare metodă este importantă

Deschiderea documentului și capcana eșecului silențios

TPdf deschide un fișier prin setarea FileName și comutarea Active := True. Detaliul critic: Active := True nu generează niciodată excepții. Dacă fișierul lipsește, este protejat prin parolă sau corupt, PDFium capturează eroarea intern, iar Active rămâne pur și simplu False. Aceasta înseamnă că fiecare buclă de extragere trebuie să fie protejată împotriva acestui caz:

Pdf := TPdf.Create(nil);
try
  Pdf.FileName := 'report.pdf';
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    ShowMessage('Nu s-a putut deschide PDF-ul (deteriorat sau parolă greșită)');
    Exit;
  end;
  // extragerea urmează aici
finally
  Pdf.Active := False;
  Pdf.Free;
end;

Fișierele protejate prin parolă necesită setarea Pdf.Password := '...' înainte de Active := True. Nu există o a doua șansă: odată ce Active eșuează, trebuie să închideți și să redeschideți cu parola corectă

Extragerea pagină cu pagină utilizând Character[]

Cea mai simplă abordare la nivel scăzut parcurge fiecare caracter de pe fiecare pagină. Setați Pdf.PageNumber pentru a încărca stratul de text pentru acea pagină, apoi parcurgeți cele CharacterCount intrări folosind proprietatea Character[]. Merită verificate două indicatoare pe fiecare intrare: CharacterGenerated[i] marchează glifele sintetice introduse de motorul de randare (de exemplu, cratime la sfârșit de rând) care nu au o valoare Unicode reală, iar CharacterMapError[i] semnalează că PDFium nu a putut mapa glifa la un cod de caracter, lucru care se întâmplă în cazul codificărilor de fonturi cărora le lipsește tabelul 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;   // normalizează CR la LF
      Line := Line + Ch;
    end;
    Output.Add(Line);
  end;
end;

Rezultatului este un șir simplu de coduri Unicode în ordinea în care le enumeră PDFium, adică ordinea în care apar în fluxul de conținut, nu neapărat ordinea de citire de la stânga la dreapta. Pentru majoritatea documentelor cu caractere latine produse de instrumente standard de birou, acest lucru este în regulă. Pentru PDF-urile scanate care au fost supuse OCR-ului cu secvențe neobișnuite de glife, sau pentru textul de la dreapta la stânga, ordinea poate fi greșită. În astfel de cazuri, ReadablePageContent devine mai util

Extragerea structurată cu ReadablePageContent

ReadablePageContent trece la nivelul următor: returnează o înregistrare TPdfReadableContent al cărei tablou Fragments conține fragmente de conținut etichetate, fiecare având un tip (Kind) care identifică paragrafe, titluri, elemente de listă, celule de tabel și așa mai departe. Când PDF-ul conține un arbore structural (verificați Pdf.IsTagged), sursa este rosStructure, iar ordinea de citire este cea autoritară. Pentru fișierele neetichetate, PDFium revine la rosHeuristic, care grupează caracterele după casetele lor de încadrare (bounding boxes) în unități de citire plauzibile, dar nu poate garanta acuratețea

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;

Dacă Content.Source = rosHeuristic iar textul extras pare amestecat, probabil că stratul de text al documentului nu a fost scris luându-se în considerare ordinea de citire. În acest punct, singura soluție sigură este re-exportarea din aplicația sursă cu etichetare adecvată, sau rularea unui pas de post-procesare care sortează originile caracterelor după ordonata Y, apoi după abscisa X

Ce vă oferă CharacterOrigin și CharacterRectangle

Ambele proprietăți returnează poziția unui caracter în spațiul paginii (puncte, originea în colțul din stânga-jos, axa Y crescând în sus). CharacterOrigin[i] este punctul de ancorare al liniei de bază a glifei; CharacterRectangle[i] este caseta de încadrare completă. Acestea sunt elementele de bază pentru orice depășește textul simplu: detectarea marginilor coloanelor, gruparea caracterelor în rânduri prin compararea coordonatelor Y într-o anumită toleranță sau construirea unei hărți de tip hit-test pentru selectarea textului într-un vizualizator. Dacă trebuie să găsiți ce caracter se află sub un clic de mouse, CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) efectuează acea căutare în mod direct, fără a fi nevoie să parcurgeți dreptunghiurile

Configurarea fișierelor DLL

PDFium Component deleagă întreaga analiză PDF unui DLL nativ, fie pdfium32.dll sau pdfium64.dll, în funcție de platforma țintă. Componenta conține un script CopyDlls.bat script care copiază fișierul potrivit în directorul de sistem Windows. Rularea acestuia ca Administrator o singură dată pe mașina de dezvoltare este suficientă; pentru distribuție, copiați DLL-ul în același director cu executabilul aplicației. Variantele cu motor V8 (pdfium32v8.dll, pdfium64v8.dll) sunt considerabil mai mari și sunt necesare doar dacă fișierele PDF conțin cod JavaScript care trebuie executat. Pentru extragerea pură a textului, versiunea standard este alegerea corectă

Dacă DLL-ul lipsește la rulare, setarea Active := True va eșua în mod silențios la fel ca în cazul unui fișier care lipsește, deoarece componenta capturează intern eroarea de încărcare. Testați întotdeauna pe o mașină curată înainte de livrare

Utilizarea FontSize[] împreună cu Character[] pentru analiza structurii

Dincolo de textul simplu, API-ul la nivel de caracter expune FontSize[i], care returnează dimensiunea în puncte randată a fiecărei glife. În combinație cu CharacterOrigin[i] și CharacterRectangle[i], aceasta vă permite să diferențiați corpul textului de antete fără a depinde de arborele structural. O secvență de caractere unde dimensiunea fontului depășește un prag reprezintă aproape sigur un antet într-un document neetichetat. Aceeași tehnică se aplică pentru detectarea legendelor (text de dimensiuni mici situat sub caseta de încadrare a unei imagini) sau a notelor de subsol (text mic în apropierea părții de jos a paginii). Nimic din toate acestea nu necesită randare; toate cele trei proprietăți citesc direct din stratul de text pe care îl construiește PDFium în timpul apelului Active := True

O nuanță: FontSize[i] reflectă dimensiunea după aplicarea matricei de transformare curentă (CTM) a paginii, deci un document în care autorul a redimensionat întreaga pagină va raporta dimensiuni ajustate proporțional. Dacă comparați dimensiunile pe pagini cu dimensiuni diferite, normalizați în funcție de înălțimea MediaBox a fiecărei pagini înainte de a lua decizii de prag

Scrierea rezultatului într-un fișier

Clasa TStringList din Delphi gestionează corect ieșirea UTF-8 începând cu versiunea XE. Setați WriteBOM := False dacă aveți nevoie de un fișier fără indicator BOM (mulți consumatori software întâmpină probleme cu prezența unui indicator BOM la începutul fișierului):

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;

Pentru documente foarte mari, unde consumul de memorie este o problemă, scrieți direct într-un TStreamWriter cu codificarea TEncoding.UTF8 în interiorul buclei de pagini, în loc să acumulați mai întâi totul într-o listă

API-urile Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent și CharacterIndexAtPos prezentate aici fac parte din componenta PDFium Component pentru Delphi și C++Builder