Scoaterea textului, a imaginilor și a fonturilor dintr-un PDF existent pare o problemă rezolvată până când treci prin ea un corpus real. Îndreaptă un indexator de căutare spre patruzeci de mii de fișiere ale clienților și defecțiunile se așază în câteva grămezi ușor de recunoscut. Cuvintele se lipesc unul de altul fiindcă nimeni nu i-a spus extractorului cât de mare trebuie să fie o pauză ca să conteze drept spațiu. Alte pagini se întorc ca text de neînțeles, pentru că un font cu subset nu poartă nicio hartă de la codurile lui de glife către caractere reale. Iar „sigla companiei” se dovedește a fi nouă obiecte-imagine separate, stivuite în spatele unei măști moi. Nimic din toate acestea nu este un defect al bibliotecii. Este diferența dintre a apela o funcție de extragere și a înțelege ce poate și ce nu poate recupera funcția aceea din octeții de pe disc
losLab PDF Library, ediția Pascal, oferă codului Delphi și C++Builder mai multe căi de a citi fiecare dintre aceste trei fluxuri, iar nivelurile diferă prin ceea ce garantează. Șiretlicul stă în potrivirea nivelului cu sarcina: un index de căutare, un revizor de redactare și o trecere de preflight PDF/A vor lucruri diferite de la aceeași pagină, iar apelul greșit risipește efort sau produce o ieșire în care nu poți avea încredere
Nivelurile de extragere a textului și ce promite fiecare
GetPageText primește o valoare de opțiuni de la 0 la 8, iar numărul acela alege un motor, nu un format. Valorile de la 0 la 2 rulează o trecere ușoară, potrivită pentru o previzualizare rapidă. Valorile de la 3 la 8 trec prin motorul conștient de aspect, care reconstruiește liniile și spațierea din locul în care stau efectiv glifele pe pagină. În acest interval variațiile contează: 4 și 6 împart ieșirea în cuvinte, 5 și 6 emit lățimi per glifă, iar 7 întoarce text simplu, cu metadatele de font, de culoare și de bloc lăsate deoparte intenționat. Opțiunea 7 este cea cu care alimentezi un index de căutare, fiindcă indexul vrea cuvinte și nimic altceva
Nicio setare de opțiuni nu poate salva un document care nu a purtat informația de la bun început. PDF pune codurile de caracter în corespondență cu forme de glife, iar singurul lucru care duce acele coduri înapoi la text lizibil este CMap-ul ToUnicode al unui font (ISO 32000-1 §9.10). Când un font cu subset se livrează fără el, orice extractor rămâne blocat. Această bibliotecă, funcția de copiere dintr-un vizualizator, o trusă concurentă: toate sunt reduse la ghicit după numele glifelor sau la a nu întoarce nimic. Răspunsul practic este detecția, nu eroismul. Notează pagina drept una cu încredere scăzută și trimite-o la OCR, fiindcă indexarea tăcută a gunoiului este mai rea decât recunoașterea faptului că nu o poți citi
Pentru cazurile pe care opțiunile plate nu le acoperă, adică tokenizare personalizată, analiză criminalistică a fluxului de conținut, o pâlnie de text construită după regulile tale, decodorul este disponibil cu un strat mai jos. TPDFExtractor se construiește peste dicționarul de resurse al unei pagini și peste colecția ei de fonturi. Metoda lui ExtractTextW trece operațiile brute de text din fluxul de conținut înapoi prin aceeași mașinărie de fonturi ca să recupereze Unicode, iar evenimentul lui OnFindObject îți predă fiecare obiect pe măsură ce trece în flux. Cea mai mare parte a codului nu are niciodată nevoie să coboare atât de adânc. Aplicațiile care au nevoie sunt cele bucuroase că stratul este public, nu îngropat
Blocuri poziționate: unitatea rezultatelor de căutare și a revizuirii de redactare
Textul simplu îți spune ce zice pagina. Mai devreme sau mai târziu, un produs are nevoie să știe și unde o zice, ca să poată evidenția un rezultat de căutare, să deseneze o casetă în jurul unui candidat la redactare sau să ancoreze o adnotare în locul potrivit. ExtractPageTextBlocks întoarce un handle către o listă de secvențe de text, iar fiecare secvență poartă textul ei, caseta ei de încadrare, precum și numele și dimensiunea fontului în care a fost compusă:
var
Pdf: TPDFlib;
Blocks, I: Integer;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('contract.pdf', '') <> 1 then
raise Exception.Create('load failed');
Pdf.SelectPage(1);
Blocks := Pdf.ExtractPageTextBlocks(0);
for I := 0 to Pdf.GetTextBlockCount(Blocks) - 1 do
Writeln(Format('%s [%s %.1f pt at %.0f,%.0f]',
[Pdf.GetTextBlockText(Blocks, I),
Pdf.GetTextBlockFontName(Blocks, I),
Pdf.GetTextBlockFontSize(Blocks, I),
Pdf.GetTextBlockBound(Blocks, I, 0),
Pdf.GetTextBlockBound(Blocks, I, 1)]));
Pdf.ReleaseTextBlocks(Blocks);
finally
Pdf.Free;
end;
end;
Un detaliu din această zonă încurcă integrările mai mult decât oricare altul. SetTextExtractionArea, SetTextExtractionWordGap și SetTextExtractionOptions sunt stare la nivel de document care persistă, nu argumente transmise la fiecare apel. Configurează o restricție de zonă pentru o singură funcționalitate, să zicem citirea doar a benzii de antet pentru a clasifica un document, și ea trunchiază tacit orice extragere ulterioară făcută pe același handle, inclusiv nivelurile GetPageText conștiente de aspect la care apelezi mai târziu. Ori resetezi starea de extragere între sarcinile logice, ori dai fiecărei sarcini propriul handle de document
Pragul de pauză între cuvinte este pârghia pentru prima grămadă de defecțiuni, cuvintele lipite. SetTextExtractionWordGap îi spune motorului de aspect cât spațiu orizontal, măsurat față de spațierea proprie a glifelor din pagină, desparte un cuvânt de următorul. Un tabel dens vrea o pauză mai mică decât o pagină de marketing compusă lejer, așa că un prag reglat per clasă de documente bate o singură constantă globală. Persistă pe document ca restul stării de extragere, deci plănuiește să îl setezi deliberat, nu o dată și gata
Imagini: fluxuri originale, nu capturi de ecran
Calea greșită de a scoate imaginile dintr-un PDF este să randezi pagina și să o decupezi. Asta reeșantionează pixelii, coace înăuntru orice rotație și aruncă tot ce a fost originalul. GetPageImageList enumeră în schimb resursele-imagine efective la care face referire pagina, iar fiecare element îți dă înapoi proprietățile lui și datele lui originale, netulburate:
var
ImgList, I: Integer;
begin
Pdf.SelectPage(1);
ImgList := Pdf.GetPageImageList(0);
for I := 0 to Pdf.GetImageListCount(ImgList) - 1 do
begin
Writeln(Pdf.GetImageListItemFormatDesc(ImgList, I, 0));
Pdf.SaveImageListItemDataToFile(ImgList, I, 0,
Format('page1-img%.2d.bin', [I]));
end;
Pdf.ReleaseImageList(ImgList);
end;
Verifică GetImageListItemFormatDesc înainte să presupui ceva despre un element, fiindcă la ce face referire o pagină este rareori câte o poză cuminte pentru fiecare imagine vizibilă. O mască moale apare ca intrare separată, de sine stătătoare. Același XObject se repetă adesea pe multe pagini, așa că deduplică după hash-ul conținutului înainte să arhivezi un export cu „toate imaginile”, altfel vei scrie aceeași siglă de o sută de ori. JPEG-urile CMYK au nevoie de gestiune de culoare aplicată mai departe în lanț, altfel se redau inversat în vizualizatoarele care iau canalele ca atare. Când vrei un inventar la nivel de document, nu pagină cu pagină, FindImages împreună cu SetFindImagesMode scanează întregul fișier într-o singură trecere
Există o graniță care merită ridicată cu părțile interesate înainte să scrie cineva criterii de acceptare: extragerea de imagini întoarce doar resurse raster. O siglă sau o diagramă desenată ca trasee vectoriale nu este o imagine în sensul de resursă și nu va apărea niciodată în vreo listă de imagini, oricât de limpede s-ar citi ca poză pe ecran. Când cerința chiar este livrarea acelei diagrame ca fișier, abordarea onestă este randarea regiunii de pagină într-un bitmap, care este o operație diferită, cu altă fidelitate. Cele două feluri de ieșire nu au ce căuta în același dosar de export fără o etichetă care să spună care e care
Fonturi: o suprafață de audit, nu o funcție de export
API-ul de fonturi răspunde la întrebări despre fonturi. Nu îți dă fișierele de font în sine, iar distincția aceasta modelează tot ce poți construi peste el. După ce FindFonts scanează documentul, enumerarea parcurge fonturile după ID, iar apelurile de proprietăți raportează despre fontul selectat în acel moment:
var
I: Integer;
begin
Pdf.FindFonts;
for I := 1 to Pdf.FontCount do // indexurile de fonturi încep de la 1, nu de la 0
if Pdf.SelectFont(Pdf.GetFontID(I)) = 1 then
Writeln(Format('%s type=%d embedded=%d subset=%d',
[Pdf.FontName, Pdf.FontType,
Pdf.GetFontIsEmbedded, Pdf.GetFontIsSubsetted]));
end;
Atenție la limitele buclei. Indexurile de fonturi merg de la 1 la FontCount, în timp ce indexurile pentru blocurile de text și pentru lista de imagini, de cu câteva paragrafe mai sus, pornesc de la zero. Duci o convenție în cealaltă și obții o eroare de unu, care fie sare peste primul font, fie iese din capătul listei, și va trece de o testare superficială fiindcă majoritatea documentelor au mai multe fonturi, iar cel greșit tot arată plauzibil. Fii limpede și în privința domeniului de acoperire. Acest API nu are export de font la nivel de octeți. Niciun apel nu întoarce programul de font încorporat ca fișier TTF sau OTF, iar enumerarea plus inspecția metadatelor este întregul model intenționat. Modelul acela acoperă totuși ceea ce cere efectiv munca de producție de la fonturi: detectarea subseturilor după tiparul numelui, auditul de încorporare înaintea unei conversii de arhivare (un font neîncorporat este un blocaj dur pentru PDF/A, așa cum detaliază preflight PDF/A și PDF/UA în Delphi) și diagnostice de codificare pentru momentele în care încrederea în extragere scade. Mai există și un motiv de licențiere pentru care granița stă aici. Un program de font cu subset este material licențiat și, lipsindu-i cele mai multe glife, oricum inutil ca font instalabil. Tratarea lui ca metadate de audit, nu ca activ extractibil, este poziția pe care o poți apăra
Ultimul apel își merită locul în triaj. Rulează GetFontEncoding pe fiecare font, citește-l alături de indicatorul de subset și poți prezice calitatea extragerii înainte să scoți un singur caracter. O pagină ale cărei fonturi sunt toate cu subset și cu codificări nestandard este candidat la OCR numai la inspecție, ceea ce lasă un flux pe loturi să o dirijeze corect fără să irosească întâi o trecere de extragere eșuată pe ea
Extragere la scară fără încărcarea documentelor
Într-un flux pe loturi, încărcarea unui document întreg doar ca să citești o pagină este I/O irosit, iar pe un corpus se adună repede. Variantele cu un singur apel, ExtractFilePageText și ExtractFilePageTextBlocks, primesc direct un nume de fișier, o parolă și un număr de pagină și sar peste încărcarea completă. Pentru fișiere de ordinul gigaocteților mai există o treaptă și mai jos. Traseul de acces direct deschide un fișier prin citiri xref în flux, așa că DAOpenFileReadOnly urmat de DAExtractPageText atinge doar obiectele de care are nevoie efectiv acea pagină. Vine cu o schimbare de convenție pe care merită să o ții minte: funcțiile DA adresează paginile după PageRef, un handle de referință de obiect pe care îl obții din DAFindPage, niciodată după numărul brut al paginii. Transmite numărul acolo unde e locul handle-ului și apelul lucrează pe obiectul greșit fără să ridice vreo eroare, adică cel mai urât fel de greșeală de depanat. Restul trusei de acces direct este desfășurat în îmbinarea, divizarea și accesul direct la PDF-uri mari
Dacă există un singur obicei care desparte codul de extragere ce supraviețuiește unui corpus real de codul care șchiopătează, acela este tratarea paginii ca intrare în care nu ai încredere, nu ca sursă curată de date. Textul care nu se potrivește cu ce randează vizualizatorul este aproape întotdeauna o problemă de codificare, o ligatură care se prăbușește într-o singură glifă sau un font cu subset căruia îi lipsesc intrările ToUnicode, iar soluția este să măsori încrederea și să deviezi paginile proaste spre OCR, nu să te lupți cu octeții. API-ul de fonturi nu va produce niciodată un TTF sau un OTF, iar asta e prin proiectare, deci construiește fluxurile de fonturi în jurul întrebărilor de audit. Iar starea persistentă de extragere, mai ales dreptunghiul de zonă, este o setare pe care o deții pe toată durata de viață a unui handle de document, nu un parametru pe care îl uiți după un apel. Prinde bine aceste trei reflexe și restul API-ului se poartă cuminte
Build-urile de evaluare, proiectele demonstrative și referința completă a API-ului de extragere se află pe pagina de produs losLab PDF Library for Delphi