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