Odborný článok

Extrakcia textu z načítaného PDF v Delphi pomocou HotPDF

Komponent HotPDF Component extrahuje text v kódovaní Unicode z akéhokoľvek PDF, ktoré načítate v Delphi, a to pomocou dvoch volaní: ExtractLoadedPageText vracia text strany v poradí na čítanie, a ExtractLoadedPageTextLayout (pridané vo verzii v2.263.0) rekonštruuje vizuálne rozloženie strany ako čistý text, takže stĺpce, zarážky a zarovnanie tabuliek zostanú zachované aj na výstupe. Obe volania fungujú aj na dokumentoch, ktoré neboli vytvorené pomocou HotPDF, čo je prípad, na ktorom skutočne záleží: faktúra, ktorú vám poslal zákazník e-mailom, správa, ktorú dodalo skenovacie stredisko, alebo zmluva vygenerovaná softvérom, ktorého názov už nikto nepozná

Dosiahnutie tohto výsledku si vyžadovalo zložitejší aparát, než by naznačovali podpisy oboch metód, pretože PDF neukladá text spôsobom, akým to robí obyčajný textový súbor. Tento článok vás prevedie oboma režimami extrakcie, a potom odhalí tajomstvo troch vnútorných častí — čítačky tabuliek CMap, interpretátora toku obsahu a záložného reťazca dekódovania písiem — pretože pochopenie fungovania tohto mapovania je rozdielom medzi bezradným krčením ramien nad nepoužiteľným výstupom a jeho úspešnou diagnostikou

Prečo je extrakcia textu náročnejšia ako len čítanie reťazcov zo súboru?

Prečo je extrakcia textu náročnejšia ako len čítanie reťazcov zo súboru?

Tok obsahu PDF (content stream) zaznamenáva kódy znakov, nie samotné znaky. Operátory Tj a TJ (ISO 32000-1 §9.4.3) nesú reťazce bajtov, ktorých význam závisí výlučne od písma vybraného predchádzajúcim operátorom Tf: bajt 0x41 môže byť písmeno A v kódovaní WinAnsi, ľubovoľný glyf v písme s podmnožinou, alebo polovica dvojbajtového CID v zloženom písme CJK. Norma ISO 32000-1 §9.10 definuje extrakciu textu presne ako tento problém dekódovania — mapovanie každého kódu späť na Unicode s použitím akýchkoľvek informácií, ktoré poskytuje slovník písma — a norma výslovne uvádza, že vyhovujúci súbor nemusí obsahovať dostatok informácií na to, aby sa to dalo vykonať

Táto posledná veta vysvetľuje každé hlásenie o chybe typu „prečo kopírovanie a vkladanie z tohto PDF vytvára nezmysly“, s akým ste sa kedy stretli. Tvorca, ktorý vloží písmo s podmnožinou bez tabuľky /ToUnicode, vytvoril súbor, ktorý sa vykresľuje dokonale, ale extrahuje sa ako nezmysel, pretože mapovanie z kódu na glyf existuje, ale mapovanie z kódu na Unicode nebolo dodané. Každé korektné API pre extrakciu je preto reťazcom záložných riešení s najlepším možným úsilím a dôležitou otázkou je, ako hlboko tento reťazec siaha

Extrakcia v poradí na čítanie pomocou ExtractLoadedPageText

Pre indexovanie vyhľadávania, porovnávanie kľúčových slov alebo odosielanie textu do analytického systému je ExtractLoadedPageText tým správnym volaním. Podpis metódy je function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean — indexy stránok začínajú od nuly, výsledok prichádza ako natívny delphijský UnicodeString a funkcia namiesto vyvolania výnimky vracia False, ak stránka nemá žiadny čitateľný tok obsahu

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;

Zalomenia riadkov na výstupe pochádzajú zo zámerne jednoduchej heuristiky: keď sa vertikálny počiatok glyfu posunie o viac ako polovicu aktuálnej veľkosti písma — čo je príznak kroku Td or T* v toku obsahu — vloží sa nový riadok. Znaky, ktoré dekodér nedokáže vyriešiť, sa namiesto vymazania zmenia na medzery, takže hranice slov zostanú zachované, aj keď jednotlivé glyfy nie. Tento režim sa nepokúša o zoskupovanie podľa poradia čítania ani o detekciu viacerých stĺpcov: dvojstĺpcová strana sa vypíše prekladaná v poradí toku obsahu, čo je zvyčajne, ale nie vždy, vizuálne poradie

Kedy by ste mali namiesto toho použiť extrakciu so zachovaním rozloženia?

Metóda ExtractLoadedPageTextLayout je správnym volaním všade tam, kde má pozícia svoj význam: tabuľky, formuláre, výpisy kódu, čokoľvek, čo plánujete porovnávať (diff), vyhľadávať (grep) alebo analyzovať po stĺpcoch. Namiesto sploštenia glyfov do jedného toku ich zoskupuje do základných línií (baselines), triedi každú líniu podľa súradnice X a reprodukuje horizontálne a vertikálne biele znaky na mriežke znakov s pevnou šírkou (monospaced grid), ktorej veľkosť je odvodená od mediánu posunu glyfov a veľkosti písma. Široké medzery medzi behmi textu na rovnakej línii sa menia na sériu medzier; veľké medzery medzi líniami sa stávajú prázdnymi riadkami. Výsledok sa číta tak, ako strana vyzerá

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;

Oba režimy zdieľajú každý bajt dekódovacieho aparátu a líšia sa iba v tom, ako usporiadajú dekódované glyfy, takže voľba režimu nestojí nič z hľadiska vernosti údajov. Vyberte ExtractLoadedPageText, keď záleží len na slovách, a ExtractLoadedPageTextLayout, keď záleží na usporiadaní. Detekcia poradia čítania vo viacerých stĺpcoch zostáva mimo rozsahu pre oba režimy — mriežkové vykreslenie dvojstĺpcovej strany zobrazuje oba stĺpce vedľa seba, čo je pre porovnávanie (diffing) úplne správne, ale pre plynulý text to vhodné nie je

Ako HotPDF dekóduje kódy znakov na Unicode?

Komponent HotPDF Component vyhodnocuje každý kód znaku prostredníctvom prioritne zoradeného záložného reťazca: najprv vložená tabuľka /ToUnicode CMap písma, potom záznam /Encoding (prúd alebo pomenovaný CMap), potom — pre zložené písma — štandardné súbory Adobe CMap pre sady znakov ako Adobe-GB1, Adobe-CNS1, Adobe-Japan1 a Adobe-KR, a nakoniec vstavané tabuľky WinAnsi a MacRoman pre jednoduché písma. Stratégia, ktorá nedokáže priniesť výsledok, ticho prejde na ďalšiu v poradí bez vyvolania výnimky, a kód, ktorý vyčerpá celý reťazec, sa vyhodnotí ako 0, takže volajúci môže počítať chyby namiesto hádania

Tabuľka /ToUnicode CMap (ISO 32000-1 §9.10.3) je na prvom mieste, pretože ide o mapovanie, ktoré tvorca zapísal špeciálne pre účely extrakcie. Cesta k štandardným súborom Adobe CMap je dôležitá pre dokumenty CJK, ktoré používajú vopred definované CMapy, ako napríklad UniGB-UTF16-H, namiesto toho, aby čokoľvek vkladali: HotPDF dodáva súbory sád vo svojom adresári resources\CMap, vyhľadáva ich za behu relatívne k spustiteľnému súboru a ukladá každú spracovanú mapu do vyrovnávacej pamäte pre daný proces — čo je užitočné vedieť, pretože najväčšia z nich, mapa Adobe-GB1, má približne 2 MB zdrojového textu, ktorý nechcete analyzovať znova pre každú stranu. Ak adresár chýba, dekodér jednoducho vynechá CMapy na disku a pracuje s vloženými tabuľkami plus so vstavanými kódovaniami. Ide o zrkadlový obraz problému tvarovania na strane čítania, o ktorom sa píše v tvarovaní textu pre zložité písma pomocou HotPDF, kde sa rovnakému rozdielu medzi kódom a glyfom čelí pri zápise

Dve pasce syntaxe CMap, ktoré stojí za to poznať

Súbory CMap vyzerajú triviálne analyzovateľné, no nie je to tak, pričom dva detaily sú zodpovedné za väčšinu zlyhaní analyzátorov pri prvých pokusoch. Prvým je, že počet záznamov prichádza pred kľúčovým slovom sekcie: sekcia sa číta ako 2 beginbfchar, a nie beginbfchar 2. Analyzátor, ktorý očakáva počet za kľúčovým slovom, skonzumuje číslo ako zatúlaný token, a potom nájde nula položiek v každej sekcii. Robustný prístup — ten, pre ktorý sa rozhodol čítač v HotPDF — je úplne ignorovať počet a cykliť, kým nenarazí na zodpovedajúce kľúčové slovo endbfchar / endbfrange, čo má navyše výhodu tolerovania reálnych súborov, ktorých počty sú jednoducho nesprávne

Druhou pascou je, že ciele bfchar a bfrange sú reťazce UTF-16BE a nie celé čísla. Cieľ <D83DDE00> znamená U+1F600 — náhradný pár (surrogate pair), ktorý sa musí znova skombinovať do jedného bodu kódu — and reading those four bytes as a big-endian integer produces a meaningless value on every code point outside the Basic Multilingual Plane. Emoji v PDF už nie sú exotikou, takže dekodér, ktorý vynecháva rekombináciu náhradných párov, zlyhá na súboroch, ktoré vaši používatelia reálne majú. HotPDF najprv analyzuje hexadecimálny literál na surové bajty, a potom kombinuje kódové jednotky UTF-16BE, čo pokrýva aj viacznakové ciele, ktoré vytvárajú mapovania ligatúr

Prechod na úroveň glyfov pomocou ExtractLoadedPageGlyphs

Oba textové hovory sú postavené na ExtractLoadedPageGlyphs, a základné pole THPDFGlyphArray je k dispozícii aj pre váš kód. Každý THPDFGlyphRecord nesie vyriešený bod kódu Unicode spolu so surovým kódom znaku, bajtovou šírkou kódu (1, 2 alebo 4, určenou podľa codespacerange v CMap), kľúčom a veľkosťou aktívneho prostriedku písma, počiatkom X a Y v používateľskom priestore a horizontálnym posunom. To stačí na vytvorenie detekcie hraníc slov, polohovaného zvýrazňovania alebo vlastného algoritmu rozloženia bez toho, aby ste sa museli sami dotknúť toku obsahu

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;

Počítanie záznamov Unicode = 0, ako je uvedené vyššie, je korektný spôsob, ako zmerať kvalitu extrakcie v danom dokumente predtým, ako dôverujete textu v ďalšom kroku spracovania. Záznamy glyfov tiež ukotvujú každý znak k zdrojovému operandu v toku obsahu, čo umožňuje vyhľadávanie a nahrádzanie textu v načítanom dokumente HotPDF na rovnakom základe

Ktoré PDF neposkytnú svoj text?

Niektoré súbory prekonajú akýkoľvek extraktor a je lepšie ich detegovať než odosielať ich výstup. Naskenované dokumenty sú najjasnejším prípadom: strana, ktorá je jedným veľkým obrázkom, neobsahuje vôbec žiadne textové operátory, takže extrakcia správne vracia prázdny reťazec — riešením je OCR, pričom extrahovanie obrázkov strán z načítaného PDF je prvým krokom tohto spracovania. Písma s podmnožinou bez tabuľky /ToUnicode sú zložitejším prípadom: ak cesta /Encoding aj štandardné CMapy ostanú prázdne, tieto glyfy sa vyhodnotia ako 0 a v textových volaniach sa prejavia ako medzery. Šifrované dokumenty sa extrahujú normálne za predpokladu, že ich načítate s ich heslom prostredníctvom preťaženej verzie LoadFromFile, takže toky sú dešifrované ešte predtým, ako ich interpretátor uvidí

Jeden užší limit stojí za to uviesť jasne: dekódovací reťazec číta CMap a toky obsahu prostredníctvom Flate cesty HotPDF, takže písmo, ktorého prúd ToUnicode používa neobvyklý filter, prejde na ďalšiu stratégiu namiesto zlyhania celej strany. V praxi FlateDecode pokrýva takmer všetko vyrobené v posledných dvoch desaťročiach a toto prechádzanie na záložné riešenia je zámerne tiché — získate najlepší text, aký súbor umožňuje, namiesto výnimky. Rovnaké mechanizmy čítania objektov na strane čítania, ktoré tu vyhodnocujú slovníky písiem, poháňajú aj úpravu metadát v načítaných dokumentoch, so a document intake pipeline can extract, inspect, and annotate in one pass

Extrakcia textu, vykresľovanie so zachovaním rozloženia, prístup na úrovni glyfov a na nich postavené funkcie vyhľadávania a nahrádzania sú súčasťou štandardného komponentu HotPDF Component pre Delphi a C++Builder — no external DLLs, no OS text services, just Object Pascal you can step through when a strange file lands in your queue