Technical Article

Teksto, vaizdų ir šriftų išgavimas iš PDF „Delphi“ programoje su „PDFlibPas“

Teksto, vaizdų ir šriftų išgavimas iš esamo PDF failo skamba kaip išspręsta problema, kol nepradedate dirbti su realiu dokumentų rinkiniu. Nukreipkite paieškos indeksavimo sistemą į keturiasdešimt tūkstančių klientų failų, ir visos klaidos pasidalins į kelias lengvai atpažįstamas grupes. Žodžiai susilieja, nes niekas nenurodė tekstą išgaunančiai programai, kokio dydžio tarpas laikomas žodžių skyrikliu. Kiti puslapiai grąžina nesąmoningus simbolius, nes dalinis šriftas (angl. subsetted font) neturi žemėlapio, susiejančio jo glifų kodus su tikraisiais simboliais. O „įmonės logotipas“ pasirodo esąs devyni atskiri vaizdo objektai, sudėti vienas ant kito po permatoma kauke. Nė vienas iš šių atvejų nėra bibliotekos klaida. Tai yra skirtumas tarp paprasto išgavimo funkcijos iškvietimo ir supratimo, ką funkcija iš tikrųjų gali ir ko negali atkurti iš baitų diske.

„losLab PDF Library“ (Pascal versija) suteikia „Delphi“ ir „C++Builder“ kūrėjams daugiau nei vieną būdą skaityti kiekvieną iš šių trijų srautų, o metodai skiriasi savo suteikiamomis garantijomis. Pagrindinis uždavinys yra suderinti išgavimo metodą su konkrečiu darbu: paieškos indeksui, peržiūros įrankiui ir PDF/A patikrai reikia visiškai skirtingų duomenų iš to paties puslapio, o pasirinkus netinkamą funkciją veltui gaištamas laikas arba gaunamas rezultatas, kuriuo negalima pasitikėti.

Teksto išgavimo lygiai ir jų galimybės

Funkcija GetPageText priima parinkčių reikšmę nuo 0 iki 8, ir šis skaičius parenka atvaizdavimo variklį, o ne tiesiog formatą. Reikšmės nuo 0 iki 2 atlieka greitą analizę, kuri puikiai tinka greitai peržiūrai. Reikšmės nuo 3 iki 8 nukreipia apdorojimą per išdėstymą suprantantį variklį (angl. layout-aware engine), kuris atkuria eilutes ir tarpus pagal tai, kur glifai iš tikrųjų yra puslapyje. Šiame diapazone parinktys skiriasi: 4 ir 6 skaido išvestį į žodžius, 5 ir 6 pateikia glifų plotį, o 7 grąžina gryną tekstą be šrifto, spalvos ir blokų metaduomenų. Būtent 7 parinktis yra tinkamiausia paieškos indeksui, nes jam reikalingi tik žodžiai ir nieko daugiau.

Joks nustatymas negali išgelbėti dokumento, kuriame ši informacija niekada nebuvo įrašyta. PDF susieja simbolių kodus su glifų formomis, o vienintelis dalykas, kuris susieja šiuos kodus atgal su skaitomu tekstu, yra šrifto „ToUnicode CMap“ lentelė (ISO 32000-1 §9.10). Kai dalinis šriftas platinamas be šios lentelės, teksto išgavimas tampa neįmanomas. Ši biblioteka, teksto kopijavimas peržiūros programoje ar bet koks kitas įrankis – visi jie gali tik spėlioti pagal glifų pavadinimus arba negrąžinti nieko. Praktinis sprendimas šiuo atveju yra aptikimas, o ne bandymai atspėti. Įvertinkite tokį puslapį kaip žemo patikimumo ir nusiųskite jį OCR apdorojimui, nes tylus klaidingo teksto indeksavimas yra blogiau nei prisipažinimas, kad puslapio neįmanoma perskaityti.

Tais atvejais, kai standartinės parinktys netinka (pavyzdžiui, kuriant savo taisykles atitinkantį teksto apdorojimą), galima naudoti žemesnio lygio dešifravimo įrankį. Klasė TPDFExtractor sukuriama pagal puslapio išteklių žodyną ir šriftų rinkinį. Jos metodas ExtractTextW apdoroja grynąsias turinio srauto operacijas per šriftų sistemą, kad atkurtų „Unicode“ tekstą, o įvykis OnFindObject perduoda kiekvieną objektą tiesiai jo apdorojimo metu. Daugumai programų nereikia pasiekti šio lygio. Tačiau sudėtingesnių sprendimų kūrėjai džiaugiasi, kad šis sluoksnis yra viešas, o ne paslėptas.

Struktūrizuoti blokai: paieškos rezultatų ir peržiūros pagrindas

Grynasis tekstas parodo, kas parašyta puslapyje. Tačiau anksčiau ar vėliau programai prireikia sužinoti, kur tiksliai tai parašyta, kad būtų galima pažymėti paieškos rezultatą, nubrėžti rėmelį aplink slepiamą informaciją arba susieti anotaciją su tinkama vieta. Funkcija ExtractPageTextBlocks grąžina nuorodą į teksto eilučių sąrašą, kur kiekviena eilutė turi savo tekstą, ribojantįjį rėmelį bei šrifto pavadinimą ir dydį:

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;

Viena šios srities detalė kelia daugiausiai problemų integracijose. Funkcijos SetTextExtractionArea, SetTextExtractionWordGap ir SetTextExtractionOptions nustato išliekančią dokumento lygio būseną, o ne parametrus, perduodamus su kiekvienu iškvietimu. Jei nustatysite srities apribojimą vienai funkcijai (pavyzdžiui, skaityti tik antraštę dokumento klasifikavimui), tai tyliai apkarpyti kiekvieną vėlesnį teksto išgavimą su tuo pačiu handle, įskaitant ir vėliau naudojamus GetPageText lygius. Todėl visada atstatykite teksto išgavimo būseną tarp skirtingų užduočių arba sukurkite atskirą dokumento handle kiekvienai užduočiai.

Tarpų tarp žodžių slenkstis yra priemonė išvengti pirmosios problemos – žodžių susiliejimo. Funkcija SetTextExtractionWordGap nurodo išdėstymo varikliui, koks horizontalus tarpas (matuojamas pagal paties puslapio glifų tarpus) skiria vieną žodį nuo kito. Tankiai užpildytai lentelei reikia mažesnio tarpo nei laisvai sumontuotam rinkodaros puslapiui, todėl kiekvienam dokumento tipui pritaikytas slenkstis yra pranašesnis už vieną visuotinę konstantą. Kadangi šis nustatymas išlieka dokumento būsenoje, nustatykite jį apgalvotai, o ne vieną kartą visam laikui.

Vaizdai: originalūs srautai, o ne ekrano kopijos

Netinkamas būdas gauti vaizdus iš PDF failo – atvaizduoti puslapį ekrane ir jį apkarpyti. Tai pakeičia pikselių skaičių, pritaiko pasukimą ir praranda originalaus vaizdo kokybę. Tuo tarpu funkcija GetPageImageList pateikia tikruosius vaizdo išteklius, į kuriuos nuorodas turi puslapis, o kiekvienas elementas grąžina savo savybes ir nepakeistus originalius duomenis:

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;

Prieš darydami prielaidas apie vaizdą, patikrinkite GetImageListItemFormatDesc rezultatą, nes puslapio nuoroda retai būna vienas aiškus paveikslėlis. Pavyzdžiui, švelnioji kaukė (angl. soft mask) rodoma kaip atskiras elementas. Tas pats „XObject“ vaizdo objektas dažnai kartojasi daugelyje puslapių, todėl prieš išsaugodami visus vaizdus pašalinkite dublikatus pagal turinio maišos kodą (angl. hash), kad šimtą kartų neįrašytumėte to paties logotipo. CMYK formato JPEG vaizdams vėliau reikia pritaikyti spalvų valdymą, kitaip programose jie bus rodomi atvirkštinėmis spalvomis. Jei norite gauti viso dokumento vaizdų sąrašą, o ne po vieną puslapį, funkcija FindImages kartu su SetFindImagesMode nuskaito visą failą vienu praėjimu.

Yra viena riba, kurią verta aptarti su užsakovais prieš patvirtinant reikalavimus: vaizdų išgavimas grąžina tik taškinės grafikos išteklius. Logotipas ar diagrama, nupiešti vektoriniais keliais, nėra vaizdas išteklių prasme ir niekada nepasirodys jokiame vaizdų sąraše, nesvarbu, kaip aiškiai jie matomi ekrane. Jei tikrai reikia pateikti tokią diagramą kaip failą, teisingas kelias yra atvaizduoti puslapio sritį į bitų žemėlapį. Tai yra kita operacija su visiškai kitokiu tikslumu. Šių dviejų tipų rezultatai neturi būti dedami į tą patį aplanką nepažymėjus, kuris yra kuris.

Šriftai: audito priemonė, o ne eksporto funkcija

Šriftų API atsako į klausimus apie šriftus. Ji nepateikia pačių šriftų failų, ir šis skirtumas apibrėžia viską, ką galite sukurti. Po to, kai funkcija FindFonts nuskaito dokumentą, sąrašas pereina šriftus pagal ID, o savybių iškvietimai pateikia informaciją apie šiuo metu pasirinktą šriftą:

var
  I: Integer;
begin
  Pdf.FindFonts;
  for I := 1 to Pdf.FontCount do        // font indexes start at 1, not 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;

Atkreipkite dėmesį į ciklo ribas. Šriftų indeksai prasideda nuo 1 iki FontCount, o prieš tai minėti teksto blokų ir vaizdų sąrašų indeksai prasideda nuo nulio. Supainiojus šias taisykles gausite off-by-one klaidą, kuri arba praleis pirmąjį šriftą, arba viršys sąrašo ribas. Ši klaida gali likti nepastebėta paprasto testavimo metu, nes dauguma dokumentų turi kelis šriftus ir neteisingas indeksas vis tiek atrodys tinkamas. Taip pat aiškiai supraskite šio įrankio paskirtį. Šis API neatlieka baitų lygio šriftų eksporto. Joks iškvietimas negrąžina įkelto šrifto programos kaip TTF ar OTF failo (visas numatytas modelis yra tik metaduomenų patikra ir peržiūra). Šis modelis vis tiek apima viską, ko reikia gamyboje: dalinio šrifto identifikavimą pagal pavadinimo šabloną, įterpimo auditą prieš archyvavimą (neįterptas šriftas yra esminė kliūtis PDF/A suderinamumui, kaip aprašyta straipsnyje PDF/A ir PDF/UA patikra „Delphi“ programoje) ir kodavimo diagnostiką, kai teksto išgavimo tikslumas sumažėja. Taip pat yra licencijavimo priežastis, kodėl ši riba yra nustatyta. Dalinio šrifto programa yra licencijuota medžiaga ir, neturėdama daugumos savo glifų, vis tiek yra nenaudinga kaip įdiegiama sistema. Metaduomenų tikrinimas yra vienintelė saugi pozicija.

Paskutinis iškvietimas yra labai naudingas pirminiam vertinimui. Paleiskite GetFontEncoding kiekvienam šriftui, įvertinkite jį kartu su dalinio šrifto vėliavėle, ir galėsite nuspėti teksto išgavimo kokybę prieš perskaitant bent vieną simbolį. Puslapis, kurio visi šriftai yra daliniai su nestandartiniu kodavimu, iškart tampa kandidatu į OCR apdorojimą. Tai leidžia nukreipti dokumentą tinkamu keliu neišeikvojant išteklių nesėkmingam teksto išgavimui.

Išgavimas dideliu mastu neįkeliant viso dokumento

Dokumentų apdorojimo konvejeryje viso dokumento įkėlimas tik tam, kad perskaitytumėte vieną puslapį, yra didelis papildomas I/O darbas, kuris greitai susikaupia visame rinkinyje. Vienkartiniai iškvietimai ExtractFilePageText ir ExtractFilePageTextBlocks priima failo pavadinimą, slaptažodį ir puslapio numerį tiesiogiai, praleisdami viso dokumento įkėlimą. Gigabaitų dydžio failams yra dar žemesnis lygis. Tiesioginės prieigos kelias atveria failą per transliacinius xref skaitymus, todėl DAOpenFileReadOnly, o po jo einanti funkcija DAExtractPageText, paliečia tik tuos objektus, kurių reikia tam konkrečiam puslapiui. Čia pasikeičia konvencija, kurią verta įsiminti: DA funkcijos kreipiasi į puslapius pagal PageRef – objekto nuorodos handle, kurį gaunate iš DAFindPage, o ne pagal puslapio numerį. Perdavus numerį ten, kur turi būti handle, iškvietimas bus taikomas neteisingam objektui nesukeldamas klaidos, o tokią klaidą ištaisyti ypač sunku. Kiti tiesioginės prieigos įrankiai aprašyti straipsnyje didelio PDF sujungimas, skaidymas ir tiesioginė prieiga.

Jei yra vienas įprotis, skiriantis gerą teksto išgavimo kodą nuo problematiško, tai yra puslapio vertinimas kaip nepatikimo šaltinio, o ne švaraus duomenų rinkinio. Tekstas, kuris skiriasi nuo ekrane rodomo vaizdo, beveik visada yra kodavimo problema (glifų susiliejimas arba trūkstamos ToUnicode lentelės daliniuose šriftuose), ir teisingas sprendimas yra matuoti patikimumą bei nukreipti probleminius puslapius į OCR, o ne kovoti su baitais. Šriftų API niekada nesugeneruos TTF ar OTF failo dėl licencijavimo ir struktūros apribojimų, todėl kurkite šriftų procesus remdamiesi tik audito klausimais. O išliekanti teksto išgavimo būsena (ypač srities stačiakampis) yra nustatymas, kurį valdote visą dokumento handle gyvavimo laiką, o ne parametras, kurį pamirštate po vieno iškvietimo. Teisingai įvertinus šias tris taisykles, visas API veiks sklandžiai.

Vertinimo versijas, demonstracinius projektus ir pilną teksto išgavimo API aprašymą rasite „losLab PDF Library for Delphi“ produkto puslapyje losLab PDF Library for Delphi.