A HotPDF Component két hívással nyeri ki a Unicode szöveget bármely betöltött PDF-ből Delphi-ben: az ExtractLoadedPageText visszaadja az oldal olvasási folyamatának szövegét, az ExtractLoadedPageTextLayout (amely a v2.263.0 verzióban jelent meg) pedig rekonstruálja az oldal vizuális elrendezését egyszerű szövegként, így a hasábok, a behúzások és a táblázatok igazítása megmarad a kimenetben; mindkettő működik olyan dokumentumokon is, amelyeket nem a HotPDF hozott létre, ami a gyakorlatban a legfontosabb eset: az ügyfél által e-mailben küldött számlán, a szkennelő iroda által szállított jelentésen, vagy az olyan szoftverrel előállított szerződésen, amelynek nevét már senki sem tudja megnevezni
Ez a folyamat több mechanizmust igényelt, mint amennyit a két metódus sugall, mert a PDF nem úgy tárolja a szöveget, mint egy szöveges fájl; ez a cikk végigvezeti Önt mindkét kinyerési módon, majd bemutatja a három mögöttes elemet — a CMap-olvasót, a tartalomfolyam-értelmezőt és a betűtípus-dekódolási tartalékláncot —, mert a leképezés működésének ismerete jelenti a különbséget a hibás kimenet miatti vállvonogatás és a hiba diagnosztizálása között
Miért nehezebb a szövegkinyerés, mint a karakterláncok kiolvasása a fájlból?
A PDF tartalomfolyam karakterkódokat rögzít, nem karaktereket; a Tj és TJ operátorok (ISO 32000-1 §9.4.3) olyan bájtokból álló karakterláncokat hordoznak, amelyek jelentése teljesen a megelőző Tf által kiválasztott betűtípustól függ: a 0x41 bájt lehet az A betű a WinAnsi kódolás alatt, egy tetszőleges karakter egy részhalmaz-betűtípusban, vagy egy kétbájtos CID fele egy összetett CJK betűtípusban; az ISO 32000-1 §9.10 a szövegkinyerést pontosan ezzel a dekódolási problémával azonosítja — minden kód leképezését a Unicode-ra a betűtípus-szótár által biztosított információk felhasználásával —, és a szabvány egyértelműen kimondja, hogy a megfelelő fájlnak nem kötelező elegendő információt szolgáltatnia ehhez
Ez az utolsó záradék magyarázza az összes olyan „miért eredményez zagyvaságot a másolás-beillesztés ebből a PDF-ből” típusú hibajelentést, amelyet valaha látott; az az előállító, amely beágyaz egy részhalmaz-betűtípust /ToUnicode tábla nélkül, olyan fájlt hoz létre, amely tökéletesen renderelődik, de zagyvaságként nyerhető ki, mivel a kód-karakter leképezés létezik, de a kód-Unicode leképezést soha nem szállították le; minden korrekt kinyerési API ezért a lehetőségek szerinti legjobb tartaléklánc, és a hasznos kérdés az, hogy ez a lánc milyen mélyre nyúlik vissza
Olvasási folyamat kinyerése az ExtractLoadedPageText segítségével
Keresési indexeléshez, kulcsszó-illesztéshez vagy a szöveg elemzési feldolgozóhoz továbbításához az ExtractLoadedPageText a megfelelő hívás; a szignatúra a következő: function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean — az oldalindexek nulla alapúak, az eredmény natív Delphi UnicodeString-ként érkezik, és a függvény False értéket ad vissza, ha az oldal nem rendelkezik olvasható tartalomfolyammal, ahelyett, hogy kivételt dobna
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;
// Az AllText most a dokumentum olvasási folyamatának szövegét tartalmazza
finally
Pdf.Free;
end;
end;
A kimeneti sortörések egy szándékosan egyszerű heurisztikából származnak: amikor egy karakter függőleges kezdőpontja a jelenlegi betűméret felénél nagyobb mértékben elmozdul — ami a tartalomfolyamban egy Td vagy T* lépés jele —, egy újsor karakter kerül beillesztésre; a dekóder által nem feloldható karakterek szóközökké válnak ahelyett, hogy eltűnnének, így a szavak határai megmaradnak akkor is, ha az egyes karakterek nem; ez a mód nem próbálkozik az olvasási sorrend csoportosításával vagy a többhasábos észleléssel: a két hasábos oldal a tartalomfolyam sorrendjében összefonódva jelenik meg, ami általában, de nem mindig a vizuális sorrend
Mikor érdemes inkább elrendezést megőrző kinyerést használni?
Az ExtractLoadedPageTextLayout a megfelelő hívás minden olyan esetben, amikor a pozíciónak jelentése van: táblázatok, űrlapok, kódlisták, vagy bármi, amit hasábok szerint szeretne összehasonlítani, szűrni vagy értelmezni; ahelyett, hogy a karaktereket egy adatfolyammá lapítaná, alapvonalakba csoportosítja őket, az egyes alapvonalakat X szerint rendezi, és reprodukálja a vízszintes és függőleges üres helyeket egy fix szélességű karakterrácson, amelynek méretét az átlagos karakterelmozdulás és a betűméret alapján határozza meg; az ugyanazon az alapvonalon lévő futások közötti széles hézagok szóközökké válnak, a bázisvonalak közötti nagy hézagok pedig üres sorokká; az eredmény úgy olvasható, ahogy az oldal kinéz
var
Grid: UnicodeString;
begin
if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
TFile.WriteAllText('page1.txt', Grid, TEncoding.UTF8);
// A hasábok, a behúzások és a táblázatok igazítása megmarad
// szóközökként és üres sorokként a karakterrácson
end;
A két mód a dekódoló mechanizmus minden bájtján osztozik, és csak a dekódolt karakterek elrendezésében különböznek, így a választás nem jár pontosságbeli veszteséggel; válassza az ExtractLoadedPageText metódust, ha csak a szavak számítanak, és az ExtractLoadedPageTextLayout-ot, ha az elrendezés is; a többhasábos olvasási sorrend észlelése mindkettőnél kívül esik a hatókörön — a két hasábos oldal rácsos renderelése mindkét hasábot egymás mellett mutatja, ami az összehasonlításhoz pontosan megfelelő, de a folyószöveg újrarendezéséhez nem
Hogyan dekódolja a HotPDF a karakterkódokat Unicode-ra?
A HotPDF Component minden egyes karakterkódot egy prioritás szerint rendezett tartalékláncon keresztül old fel: először a betűtípus beágyazott /ToUnicode CMap-jét használja, majd az /Encoding bejegyzést (adatfolyam vagy nevesített CMap), ezután — összetett betűtípusok esetén — az Adobe szabványos CMap fájljait olyan karaktergyűjteményekhez, mint az Adobe-GB1, Adobe-CNS1, Adobe-Japan1 és Adobe-KR, végül pedig a beépített WinAnsi és MacRoman táblákat az egyszerű betűtípusokhoz; ha egy stratégia nem tud választ adni, csendben továbblép a következőre ahelyett, hogy hibát jelezne, és a teljes láncot kimerítő kód 0-ra oldódik fel, így a hívó számolni tudja a sikertelen kísérleteket a találgatás helyett
A /ToUnicode CMap (ISO 32000-1 §9.10.3) áll az első helyen, mivel ezt a leképezést az előállító kifejezetten a kinyeréshez írta; az Adobe szabványos CMap útvonala olyan CJK dokumentumoknál számít, amelyek előre meghatározott CMap-eket használnak, mint például a UniGB-UTF16-H ahelyett, hogy bármit is beágyaznának: a HotPDF a gyűjteményfájlokat a resources\CMap könyvtárában szállítja, futásidőben az indítófájlhoz képest keresi meg őket, és minden értelmezett térképet folyamatonként gyorsítótáraz — ezt érdemes tudni, mert a legnagyobb közülük, az Adobe-GB1 térkép, nagyjából 2 MB forrásszöveg, amelyet nem szeretne oldalonként újraértelmezni; ha a könyvtár hiányzik, a dekóder egyszerűen kihagyja a lemezalapú CMap-eket, és beágyazott táblákkal, valamint beépített kódolásokkal dolgozik; ez a szövegkinyerési oldala a komplex írásrendszerű szövegek HotPDF segítségével történő alakításáról szóló cikkben tárgyalt problémának, ahol az írás során ugyanezzel a kód-karakter különbséggel szembesülünk
Két CMap szintaktikai csapda, amit érdemes ismerni
A CMap fájlok egyszerűen értelmezhetőnek tűnnek, de nem azok, és két részlet okozza a legtöbb első próbálkozásos értelmezési hibát; az első az, hogy a rekordok száma a szakasz kulcsszava előtt szerepel: egy szakasz így néz ki: 2 beginbfchar, nem pedig beginbfchar 2; az az értelmező, amely a számot a kulcsszó után várja, a számot felesleges elemként kezeli, majd minden szakaszben nulla bejegyzést talál; a robusztus megközelítés — amely mellett a HotPDF olvasója is döntött — az, hogy teljesen figyelmen kívül hagyja a számot, és addig ismétli a ciklust, amíg meg nem találja a megfelelő endbfchar / endbfrange kulcsszót, aminek az az előnye is megvan, hogy tolerálja azokat a valós fájlokat, amelyekben a számok egyszerűen hibásak
A második csapda az, hogy a bfchar és bfrange célok UTF-16BE karakterláncok, nem pedig egészek; a <D83DDE00> cél az U+1F600 értéket jelenti — egy pót-párt (surrogate pair), amelyet egyetlen kódponttá kell összevonni —, és ezen négy bájt nagy-endian egészként történő beolvasása értelmetlen értéket eredményez minden olyan kódpontnál, amely a Basic Multilingual Plane-en kívül esik; a PDF-ekben lévő hangulatjelek már nem ritkák, így az a dekódoló, amely kihagyja a pót-párok összevonását, elbukik azokon a fájlokon, amelyekkel a felhasználók valóban rendelkeznek; a HotPDF először nyers bájtokká értelmezi a hexadecimális literált, majd összevonja az UTF-16BE kódegységeket, ami kiterjed a ligatúra-leképezések által generált többkarakteres célokra is
Karakter szintig történő lebontás az ExtractLoadedPageGlyphs segítségével
Mindkét szöveges hívás az ExtractLoadedPageGlyphs metódusra épül, és a mögöttes THPDFGlyphArray is elérhető a kódjában; minden egyes THPDFGlyphRecord hordozza a feloldott Unicode kódpontot a nyers karakterkód mellett, a kód bájtszélességét (1, 2 vagy 4, a CMap codespacerange-je alapján), az aktív betűtípus erőforráskulcsát és méretét, a felhasználói tér X és Y kezdőpontját, valamint a vízszintes elmozdulást; ez elegendő ahhoz, hogy szóhatár-észlelést, pozicionált kiemelést vagy egyedi elrendezési algoritmust építsen fel anélkül, hogy magához a tartalomfolyamhoz nyúlna
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;
A Unicode = 0 rekordok számlálása a fenti módon a korrekt út a kinyerési minőség ellenőrzésére egy adott dokumentumnál, mielőtt megbízna a kapott szövegben; a karakterrekordok a tartalomfolyamban lévő forrásoperandushoz is rögzítik az egyes karaktereket, ami lehetővé teszi a HotPDF betöltött dokumentum szövegének keresését és cseréjét ugyanezen az alapon
Mely PDF-ekből nem nyerhető ki szöveg?
Egyes fájlok kifognak minden kinyerőn, és jobb ezeket észlelni, mintsem kiadni a kimenetüket; a szkennelt dokumentumok a legegyszerűbb esetek: az olyan oldal, amely egyetlen nagy kép, egyáltalán nem tartalmaz szöveges operátorokat, így a kinyerés helyesen üres karakterláncot ad vissza — a megoldás az OCR, a lapképek kinyerése a betöltött PDF-ből pedig ennek a folyamatnak az első lépése; a /ToUnicode tábla nélküli részhalmaz-betűtípusok jelentik a nehezebb esetet: ha az /Encoding útvonal és a szabványos CMap-ek is üresen térnek vissza, a karakterek 0-ra oldódnak fel, és szóközökként jelennek meg a szöveges hívásokban; a titkosított dokumentumok normál módon kinyerhetők, feltéve, hogy a jelszóval együtt tölti be őket a LoadFromFile túlterhelt verziójával, így az adatfolyamok még azelőtt dekódolódnak, hogy az értelmező látná őket
Egy szűkebb korlátot érdemes egyértelműen kimondani: a dekódolási lánc a CMap-et és a tartalomfolyamokat a HotPDF Flate útvonalán keresztül olvassa, így az a betűtípus, amelynek ToUnicode adatfolyama szokatlan szűrőt használ, a következő stratégiára lép tovább az oldal hibára futása helyett; a gyakorlatban a FlateDecode szinte mindent lefed, ami az elmúlt két évtizedben készült, a minőségromlás pedig szándékosan csendes — a lehető legjobb szöveget kapja, amit a fájl lehetővé tesz, nem pedig kivételt; ugyanaz az olvasó oldali objektum-mechanizmus, amely itt feloldja a betűtípus-szótárakak, hajtja meg a betöltött dokumentumok metaadatainak szerkesztését is, így a dokumentumbeviteli folyamat egyetlen lépésben tud kinyerni, ellenőrizni és jegyzetelni
A szövegkinyerés, az elrendezést megőrző renderelés, a karakterszintű hozzáférés, valamint a rájuk épülő keresési és cserefunkciók mind részei a Delphi és C++Builder rendszerekhez készült szabványos HotPDF Component terméknek — külső DLL-ek és operációs rendszerbeli szöveges szolgáltatások nélkül, pusztán Object Pascal kódban, amelyet lépésről lépésre követhet, amikor egy szokatlan fájl érkezik a feldolgozósorba