Techninis straipsnis

Teksto išgavimas iš įkelto PDF failo Delphi aplinkoje su HotPDF

„HotPDF Component“ išgauna Unicode tekstą iš bet kurio įkelto PDF failo Delphi aplinkoje dviem iškvietimais: ExtractLoadedPageText grąžina puslapio skaitymo srauto tekstą, o ExtractLoadedPageTextLayout (pridėta versijoje v2.263.0) atkuria puslapio vizualinį išdėstymą kaip paprastą tekstą, todėl stulpeliai, įtraukos ir lentelių lygiavimas išlieka išvestyje. Abu metodai veikia su dokumentais, kurių „HotPDF“ nesukūrė, o tai yra iš tikrųjų svarbus atvejis: sąskaita faktūra, kurią klientas atsiuntė el. paštu, skenavimo biuro pristatyta ataskaita arba sutartis, sukurta programinės įrangos, kurios pavadinimo niekas nebežino

Tam pasiekti prireikė daugiau mechanizmų nei rodo abu iškvietimai, nes PDF nesaugo teksto taip, kaip tekstinis failas. Šiame straipsnyje apžvelgiami abu išgavimo režimai, o po to paaiškina, kas slepiasi po variklio gaubtu – trimis pagrindinėmis dalimis: CMap skaitytuvu, turinio srauto interpretatoriumi ir šriftų iškodavimo atsargine grandine. Žinojimas, kaip veikia atvaizdavimas, padeda ne šiaip gūžčioti pečiais dėl sugadintos išvesties, o diagnozuoti problemą

Kodėl teksto išgavimas yra sudėtingesnis nei eilučių skaitymas iš failo?

PDF turinio srautas įrašo simbolių kodus, o ne simbolius. Operatoriai Tj ir TJ (ISO 32000-1 §9.4.3) perneša baitų eilutes, kurių reikšmė visiškai priklauso nuo šrifto, pasirinkto prieš tai einančiu Tf: baitas 0x41 gali būti raidė A pagal „WinAnsi“, pasirinktinis glifas subgrupuotame šrifte arba pusė dviejų baitų CID sudėtiniame CJK šrifte. ISO 32000-1 §9.10 apibrėžia teksto išgavimą būtent kaip šią iškodavimo problemą – kiekvieno kodo susiejimą su Unicode, naudojant bet kokią informaciją, kurią suteikia šrifto žodynas. Standarte aiškiai nurodyta, kad reikalavimus atitinkantis failas neprivalo pateikti pakankamai informacijos tam atlikti

Ši paskutinė nuostata paaiškina kiekvieną matytą pranešimą apie klaidą „kodėl kopijuojant ir įklijuojant iš šio PDF gaunama nesąmonė“. Kūrėjas, kuris įterpia subgrupuotą šriftą be /ToUnicode lentelės, įrašo failą, kuris atvaizduojamas puikiai, tačiau išgaunamas kaip nesąmonė, nes kodo-glifo atvaizdavimas egzistuoja, tačiau kodo-Unicode atvaizdavimas niekada nebuvo pateiktas. Todėl bet kuri sąžininga išgavimo API yra atsarginių variantų grandinė, o naudingas klausimas yra tai, kokia gili yra ši grandinė

Skaitymo srauto išgavimas naudojant „ExtractLoadedPageText“

Paieškos indeksavimui, raktinių žodžių atitikimui arba teksto perdavimui į analizės srautą geriausiai tinka ExtractLoadedPageText. Jo parašas yra function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean – puslapių indeksai prasideda nuo nulio, rezultatas pateikiamas kaip vietinė Delphi UnicodeString eilutė, o funkcija grąžina False, kai puslapyje nėra nuskaitomo turinio srauto, užuot sukėlusi klaidą

var
  Pdf: THotPDF;
  PageCount, I: Integer;
  PageText, AllText: UnicodeString;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('invoice.pdf');
    AllText := '';
    for I := 0 to PageCount - 1 do
      if Pdf.ExtractLoadedPageText(I, PageText) then
        AllText := AllText + PageText + #13#10;
    // AllText now holds the reading-flow text of the document
  finally
    Pdf.Free;
  end;
end;

Eilučių lūžiai išvestyje gaunami naudojant sąmoningai paprastą heuristiką: kai glifo vertikali pradžia pasislenka daugiau nei per pusę dabartinio šrifto dydžio – kas rodo Td arba T* žingsnį turinio sraute – įterpiama nauja eilutė. Simboliai, kurių dešifratorius negali išspręsti, virsta tarpais, užuot išnykę, todėl žodžių ribos išlieka net ir tada, kai atskiri glifai pradingsta. Šis režimas nebando atlikti skaitymo tvarkos grupavimo ar kelių stulpelių aptikimo: dviejų stulpelių puslapis pateikiamas įsiterpęs turinio srauto tvarka, kuri paprastai, bet ne visada, sutampa su vizualia tvarka

Kada vietoj to reikėtų naudoti maketą išsaugantį išgavimą?

ExtractLoadedPageTextLayout yra tinkamas iškvietimas, kai pozicija turi reikšmę: lentelėms, formoms, kodo sąrašams, viskam, ką planuojate lyginti (diff), ieškoti (grep) ar analizuoti pagal stulpelius. Užuot išlyginusi glifus į srautą, ši funkcija sugrupuoja juos į bazines linijas, surūšiuoja kiekvieną bazinę liniją pagal X koordinates ir atkuria horizontalius bei vertikalius tarpus vienodo pločio simbolių tinklelyje, kurio dydis nustatomas pagal medianinį glifo poslinkį ir šrifto dydį. Dideli tarpai tarp paleidimų toje pačioje bazinėje linijoje virsta tarpais; dideli tarpai tarp bazinių linijų virsta tuščiomis eilutėmis. Rezultatas skaitomas taip, kaip atrodo puslapis

var
  Grid: UnicodeString;
begin
  if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
    TFile.WriteAllText('page1.txt', Grid, TEncoding.UTF8);
  // Columns, indentation and table alignment survive as
  // spaces and blank lines on a character grid
end;

Abu režimai naudoja tuos pačius iškodavimo mechanizmo baitus ir skiriasi tik tuo, kaip jie išdėsto iškoduotus glifus, todėl šis pasirinkimas nedaro jokios įtakos tikslumui. Pasirinkite ExtractLoadedPageText, kai svarbūs tik žodžiai, ir ExtractLoadedPageTextLayout, kai svarbus išdėstymas. Kelių stulpelių skaitymo tvarkos aptikimas lieka už abiejų režimų ribų – dviejų stulpelių puslapio vaizdas tinklelyje rodo abu stulpelius šalia vienas kito, kas lyginimui (diff) yra visiškai teisinga, o prozos perorientavimui (re-flow) – ne

Kaip HotPDF iškoduoja simbolių kodus į Unicode?

„HotPDF Component“ išsprendžia kiekvieną simbolio kodą per prioritetinę atsarginių variantų grandinę: pirmiausia šrifto įterptąjį /ToUnicode CMap, tada įrašą /Encoding (srautą arba pavadintą CMap), tada – sudėtiniams šriftams – „Adobe“ standartinius CMap failus simbolių rinkiniams, tokiems kaip Adobe-GB1, Adobe-CNS1, Adobe-Japan1 ir Adobe-KR, ir galiausiai integruotas „WinAnsi“ bei „MacRoman“ lenteles paprastiems šriftams. Strategija, kuri negali pateikti atsakymo, tyliai pereina prie kitos, užuot sukėlusi klaidą, o kodas, kuris išnaudoja visą grandinę, išsprendžiamas kaip 0, kad kviečiantysis galėtų suskaičiuoti praleidimus, o ne spėlioti

/ToUnicode CMap (ISO 32000-1 §9.10.3) yra pirmoje vietoje, nes tai yra atvaizdavimas, kurį kūrėjas parašė specialiai išgavimui. „Adobe“ standartinis CMap kelias yra svarbus CJK dokumentams, kurie naudoja iš anksto apibrėžtus CMap, pavyzdžiui, UniGB-UTF16-H, užuot ką nors įterpę: „HotPDF“ pateikia rinkinio failus savo resources\CMap kataloge, suranda juos vykdymo metu pagal vykdomąjį failą ir išsaugo kiekvieną išanalizuotą žemėlapį procese. Verta žinoti, kad didžiausias iš jų – Adobe-GB1 žemėlapis – yra maždaug 2 MB šaltinio teksto, kurio nenorite iš naujo analizuoti kiekviename puslapyje. Jei katalogo nėra, dešifratorius tiesiog praleidžia diske esančius CMap ir dirba su įterptomis lentelėmis bei integruotais kodavimais. Tai yra skaitymo pusės atspindys formavimo (shaping) problemai, aptartai straipsnyje apie sudėtingo šrifto teksto formavimą su HotPDF, kur tas pats kodo ir glifo skirtumas sprendžiamas rašymo metu

Du CMap sintaksės spąstai, kuriuos verta žinoti

CMap failai atrodo lengvai analizuojami, tačiau taip nėra, o du dalykai lemia daugumą nesėkmių bandant sukurti analizatorių pirmą kartą. Pirmasis yra tas, kad įrašų skaičius pateikiamas *prieš* sekcijos raktinį žodį: sekcija nuskaitoma kaip 2 beginbfchar, o ne beginbfchar 2. Analizatorius, kuris tikisi skaičiaus po raktinio žodžio, suvartoja skaičių kaip nuklydusį žetoną (token), o tada randa nulį įrašų kiekvienoje sekcijoje. Patikimas būdas – tas, kurį pasirinko „HotPDF“ skaitytuvas – yra visiškai ignoruoti skaičių ir ciklą vykdyti tol, kol randamas atitinkamas raktinis žodis endbfchar / endbfrange, o tai papildomai leidžia toleruoti realaus pasaulio failus, kurių skaičiai tiesiog neteisingi

Antrieji spąstai yra tie, kad bfchar ir bfrange tikslai yra UTF-16BE eilutės, o ne sveikieji skaičiai. Tikslas <D83DDE00> reiškia U+1F600 – surogatinę porą, kuri turi būti sujungta į vieną kodinį tašką. Šių keturių baitų skaitymas kaip sveikojo skaičiaus su dideliu galu (big-endian) sukuria beprasmę reikšmę kiekvienam kodiniam taškui už pagrindinio daugiakalbio plano ribų. Smailikai (emoji) PDF failuose nebėra egzotika, zodžiu, dešifratorius, kuris praleidžia surogatų sujungimą, sugenda failuose, kuriuos jūsų vartotojai iš tikrųjų turi. „HotPDF“ pirmiausia išanalizuoja šešioliktainį literalą į neapdorotus baitus, o tada sujungia UTF-16BE kodavimo vienetus, kas taip pat apima kelių simbolių tikslus, kuriuos sukuria ligatūrų atvaizdavimai

Nusileidimas iki glifų lygio su „ExtractLoadedPageGlyphs“

Abu teksto iškvietimai sukurti remiantis ExtractLoadedPageGlyphs, o esamas THPDFGlyphArray yra prieinamas ir jūsų kodui. Kiekvienas THPDFGlyphRecord turi išspręstą Unicode kodinį tašką kartu su neapdorotu simbolio kodu, kodo baitų pločiu (1, 2 arba 4, nustatytu pagal CMap codespacerange), aktyviuoju šrifto ištekliaus raktu bei dydžiu, vartotojo erdvės X ir Y pradžios koordinatėmis bei horizontaliu poslinkiu. To pakanka, kad sukurtumėte žodžių ribų aptikimą, pozicionuojamą paryškinimą arba pasirinktinį maketo algoritmą patys neliesdami turinio srauto

var
  Glyphs: THPDFGlyphArray;
  I, Unresolved: Integer;
begin
  if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
  begin
    Unresolved := 0;
    for I := 0 to High(Glyphs) do
      if Glyphs[I].Unicode = 0 then
        Inc(Unresolved);
    if Unresolved > 0 then
      ShowMessageFmt('%d of %d glyphs have no Unicode mapping',
        [Unresolved, Length(Glyphs)]);
  end;
end;

Įrašų Unicode = 0 skaičiavimas, kaip parodyta aukščiau, yra sąžiningas būdas įvertinti išgavimo kokybę konkrečiame dokumente prieš pasitikint tekstu vėlesniuose etapuose. Glifų įrašai taip pat susieja kiekvieną simbolį su šaltinio operandu turinio sraute, o tai leidžia realizuoti įkelto dokumento teksto paiešką ir pakeitimą „HotPDF“ sistemoje remiantis tuo pačiu pagrindu

Kurie PDF failai neatiduos savo teksto?

Kai kai kurie failai įveikia bet kurį išgaviklį, ir geriau juos aptikti nei pateikti jų išvestį. Nuskenuoti dokumentai yra aiškiausias atvejis: puslapis, kuris yra vienas didelis vaizdas, neturi jokių teksto operatorių, todėl išgavimas teisingai grąžina tuščią eilutę – sprendimas yra OCR, o puslapio vaizdų išgavimas iš įkelto PDF yra pirmasis šio proceso žingsnis. Subgrupuoti šriftai be /ToUnicode lentelės yra sudėtingesnis atvejis: jei /Encoding kelias ir standartiniai CMap taip pat yra tušti, šie glifai išsprendžiami kaip 0 ir teksto iškvietimuose rodomi kaip tarpai. Šifruoti dokumentai išgaunami normaliai, jei įkeliate juos su slaptažodžiu per LoadFromFile perkrovą, todėl srautai iššifruojami prieš interpretatoriui juos pamatant

Viena siauresnė riba

Verta aiškiai įvardyti vieną siauresnę ribą: iškodavimo grandinė nuskaito CMap ir turinio srautus per „HotPDF“ Flate kelią, todėl šriftas, kurio „ToUnicode“ srautas naudoja neįprastą filtrą, pereina prie kitos strategijos, užuot sugadinęs puslapį. Praktikoje FlateDecode apima beveik viską, kas buvo sukurta per pastaruosius du dešimtmečius, o perėjimas yra tylus pagal projektą – gaunate geriausią tekstą, kurį leidžia failas, o ne išimtį. Tas pats skaitymo pusės objektų mechanizmas, kuris čia išsprendžia šriftų žodynus, taip pat valdo metaduomenų redagavimą įkeltuose dokumentuose, todėl dokumentų apdorojimo srautas gali išgauti, patikrinti ir pažymėti vienu žingsniu

Teksto išgavimas, maketą išsaugantis atvaizdavimas, prieiga glifų lygiu ir jais pagrįstos paieškos bei pakeitimo funkcijos yra standartinės „HotPDF Component“ dalis, skirtos Delphi ir C++Builder – jokių išorinių DLL, jokių operacinės sistemos teksto paslaugų, tiesiog Object Pascal, kurį galite žingsnis po žingsnio peržiūrėti, kai į jūsų eilę patenka keistas failas