Az oldal szövegének kinyerése a probléma könnyebbik fele. Abban a pillanatban, amikor a felhasználó beír egy szót a keresőmezőbe, és elvárja, hogy a megjelenítő odaugorjon és sárga kerettel jelölje meg, szükség van valamire, amit a sík szöveg-karakterlánc nem tud megadni: az oldalra, ahol a találat található, és a téglalapra, amelyet a PDF koordináták szerint elfoglal. Az oldalon összefűzött karakterlánc elvesztette ezt a geometriát. Megtalálhatja a részkarakterláncot (substring), de nem tud rámutatni
A PDFlibPas egy natív Object Pascal PDF-könyvtár Delphihez and C++Builderhez, és a v3.78.0 verziótól kezdve pontosan erre a kérdésre válaszol. Három lekérdező API épül a meglévő szövegblokk-kinyerő tetejére: a SearchText végigjár egy oldaltartományt, és minden találatot visszaad az oldalával és a tengelyhez igazított téglalapjával együtt, az EnumPageElements felsorol mindent egy oldalon (szövegblokkokat és beágyazott képeket egyaránt), a GetTextInAreaEx pedig egy régióban lévő minden blokk téglalapját jelenti ahelyett, hogy karakterlánc-listává lapítaná őket. Egyikük sem érinti az írási útvonalat; tiszta olvasó-oldali bővítmények a könyvtár meglévő mechanizmusa felett
Miért a szövegblokk-listában lakik a geometria, nem pedig a tölcsérben
A természetes ösztön az lenne, hogy újra felhasználjuk azt, amit a GetPageText belsőleg futtat. Ez az útvonal egy átmeneti kinyerési „tölcséren” (funnel) halad át, amely előállítja az oldal karakterláncát, majd felszabadítja magát, mielőtt a hívás visszatérne. Mire az eredményt kézbe kapja, a blokkonkénti koordináták már nincsenek meg. Soha nem voltak az Önéi, hogy megtartsa őket
A koordináták azonban megmaradnak egy another structure. Az ExtractPageTextBlocks(3) egy szövegblokk-lista azonosítót ad vissza, amelynek minden egyes eleme egy nyolc double értékből álló határoló négyszöget (bounding quad), betűtípust, betűméretet és a blokk szövegét hordozza. Ez az azonosító az egyetlen hely, ahol a geometria megőrződik a kinyerés után, ezért épül az összes új lekérdező API erre a tölcsér helyett. A blokklista újrafelhasználása azt jelenti, hogy a keresés, a felsorolás és a régiólekérdezések mind egyetlen kinyerési meneten és a blokk helyének egyetlen meghatározásán osztoznak
A visszaadott találat egy kis rekord:
type
TPDFlibSearchHit = record
Page: Integer; // a találat 1-alapú oldala
Left, Top, Right, Bottom: Double; // tengelyhez igazított találati téglalap
MatchText: WideString; // a lekérdezést tartalmazó blokk szövege
end;
A határoló tömb X/Y váltakozó (interleaved), nem pedig a négy sarok
Ez az a részlet, amely először fejt ki nehézséget. A GetTextBlockBound(ListID, Index, BoundIndex) egy 1 és 8 közötti BoundIndex-et vár, és ez a nyolc érték nem a „1. sarok, 2. sarok, 3. sarok, 4. sarok” párosítás, két mezővel csoportosítva, ahogyan azt sejteni lehetne. Ezek valójában X, Y, X, Y, X, Y, X, Y: a páratlan indexek az X koordináták, a páros indexek a Y koordináták, összesen négy pont. Ha rossz párosításban olvassa be őket, a téglalap értelmetlen lesz
A négyszög (quad) létezésének oka a sima téglalap helyett az elforgatás (rotation). Egy ferdén elhelyezett szövegblokknak valódi három- vagy négypontos határoló sokszöge van, és a nyolc double érték hűen leírja azt. A kiemelés-és-ugrás (highlight-and-jump) használati esetnél szinte mindig egy álló dobozt szeretne helyette, így a könyvtár a négyszöget tengelyhez igazított téglalappá csökkenti a négy pont minimális és maximális X és Y értékeinek pásztázásával. Az elforgatott szöveg leegyszerűsödik az azt körülvevő álló dobozzá, amire a kiemelő fedvénynek (highlight overlay) szüksége van:
var
Pdf: TPDFlib;
Hits: array[0..255] of TPDFlibSearchHit;
Found, I: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.LoadFromFile('contract.pdf', '');
// Keresés az 1-10. oldalakon, kis- és nagybetűkre nem érzékeny, részkarakterlánc-egyezés.
Found := Pdf.SearchText('indemnity', [], '1-10', Hits);
for I := 0 to Found - 1 do
if I <= High(Hits) then
WriteLn(Format('p%d: [%.1f %.1f %.1f %.1f] %s',
[Hits[I].Page, Hits[I].Left, Hits[I].Top,
Hits[I].Right, Hits[I].Bottom, Hits[I].MatchText]));
finally
Pdf.Free;
end;
end;
Vegye figyelembe, hogy a téglalap PDF felhasználói térbeli pontokban van megadva, ahol az origó a lap bal alsó sarkában található, ugyanabban a koordináta-rendszerben, amelyet a rajzolási és annotációs hívásoknak ad át. Ez szándékos: a keresési találatból visszakapott téglalap az a téglalap, amelyet közvetlenül átadhat egy kiemelő annotációnak vagy egy „görgess ide” parancsnak anélkül, hogy bármit is át kellene konvertálnia
Kis- és nagybetűérzékenység, teljes szavak, és ahol a CJK eltér
A második paraméter egy TPDFlibSearchOptions készlet, amely a soCaseSensitive és soWholeWord elemekből áll. Az üres készlet [] a gyakori eset: a kis- és nagybetűkre nem érzékeny részkarakterlánc-keresés. Adja hozzá a soCaseSensitive opciót, hogy az Indemnity és indemnity szavak megkülönböztethetők legyenek, adja hozzá a soWholeWord opciót, hogy megakadályozza a sign szó egyezését a signature szón belül, vagy kombinálja a kettőt
A teljes szavas egyezéshez meg kell határozni, hogy mi minősül szóhatárnak, és itt érdemes egyértelműen kimondani a szabályt, mert az tervezéséből adódóan ASCII-központú. Egy karakter akkor számít a szó részének, ha az ASCII betű, ASCII számjegy vagy aláhúzás: az azonosító szabályokból ismerős [A-Za-z0-9_] osztály. Egy találat csak akkor minősül teljes szónak, ha a közvetlenül előtte és utána lévő karakterek not szókarakterek (vagy a találat a blokk szélén helyezkedik el)
A nem-latin írásrendszerekre gyakorolt következményt érdemes tudni, mielőtt többnyelvű keresőmezőt vezetne be. Mivel a kínai (Han) karakterek, a kana és más nem-ASCII betűk kívül esnek ezen az osztályon, a mellettük lévő minden határ nem-szó élnek minősül. A gyakorlatban ez azt jelenti, hogy a CJK szövegeken végzett teljes szavas keresés úgy viselkedik, mintha minden pozíció érvényes szóhatár lenne, így a jelző gyakorlatilag részkarakterlánc-egyezéssé fokozódik le. Ez egy dokumentált korlátozás, nem pedig hiba, és megegyezik azzal a viselkedéssel, amely alapján a funkciót modellezték. Ha a dokumentumai elsősorban CJK nyelvűek, a teljes szavas mód nem fogja megadni azt a szegmentálást, amit egy dedikált tokenizáló nyújtana; tervezzen ezzel együtt ahelyett, hogy ráhagyatkozna
Egy megvalósítási lábjegyzet, amely megmagyarázza a máshol előforduló finom hibák egyik osztályát: a kis- és nagybetűket figyelmen kívül hagyó összehasonlítás a UpperCase-t használja a WideString-en, nem pedig az AnsiUpperCase-t. Az Ansi változat egy AnsiString-et ad vissza, ami nem egyezne meg a WideString-el, amelyet az útvonal többi része használ, és a kettő keverése típuseltéréseket, sőt, az aktív kódlapon kívüli karakterek veszteséges leképezését eredményezi. Unicode bemenet, Unicode kimenet, végig a folyamatban
Egyetlen oldaltartomány-értelmező a teljes könyvtárhoz
A harmadik paraméter egy oldaltartomány karakterlánc, például „1,3,5-9”. Az értelmezése nem egyedi: ugyanaz a PLParsePageRangeList kezeli itt is, amely a PrintPages-t és a lapmásoló eljárásokat támogatja, így a helyesen nyomtatott tartomány helyesen is keres. Az üres tartomány karakterlánc a „minden oldal” jelzése, amely esetben a SearchText maga állítja elő a teljes listát
A hatókör számít a költségek szempontjából. Egy ezer oldalas dokumentum tízoldalas szeletének keresése tíz oldalhoz nyer ki blokkokat, nem ezerhez, mert a ciklus csak a tartomány által megnevezett oldalakat választja ki és nyeri ki. Ha már tudja, hogy egy záradék a mellékletben található, adja meg azt a tartományban, és hagyja ki a fájl többi részét
Belsőleg a keresés és a felsorolás egyaránt megváltoztatja a kiválasztott oldalt az iteráció során, így mindegyik elmenti a hívó kiválasztott oldalát a belépéskor, és visszaállítja azt egy finally blokkban. Ha a lapépítés közepén hívja meg a SearchText-et, a kiválasztás pontosan ott marad, ahol a hívás visszatérésekor hagyta. Ez a mentési-és-visszaállítási szerződés olyasmi, amit csak akkor vesz észre az ember, ha hiányzik, és éppen ezért van ott
Egy teljes oldal felsorolása: szövegek és képek egyetlen listában
A keresés arra válaszol, hogy „hol van ez a szó”. A vizsgálat másik fele az, hogy „mi van egyáltalán ezen az oldalon”, és ez az EnumPageElements. Egyetlen egységes listát ad vissza, ahol minden elem vagy szövegblokk, vagy beágyazott kép, amelyeket a Kind mező különböztet meg:
type
TPDFlibPageElementKind = (ekText, ekImage);
TPDFlibPageElement = record
Kind: TPDFlibPageElementKind;
Page: Integer;
Left, Top, Right, Bottom: Double;
Text: WideString; // ekText
FontName: WideString; // ekText
FontSize: Double; // ekText
ImageID: Integer; // ekImage; használható a SelectImage / GetImageID metódusokkal
end;
A szöveges elemek ugyanabból a ExtractPageTextBlocks menetből származnak, így mindegyik a téglalapjával, a betűtípus nevével és méretével érkezik, amelyek már ki vannak töltve. A képelemek a lap beágyazott képlistájából származnak a FindImages és a GetImageID segítségével; az általuk hordozott ImageID az az azonosító, amelyet átad a SelectImage-nek a kép további vizsgálatához. A két típus egyetlen tömbben landol, így egyetlen oldalbejárással minden láthatóvá válik rajta
var
Pdf: TPDFlib;
Elems: array[0..511] of TPDFlibPageElement;
Total, I: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.LoadFromFile('report.pdf', '');
Total := Pdf.EnumPageElements(1, Elems);
for I := 0 to Total - 1 do
if I <= High(Elems) then
if Elems[I].Kind = ekText then
WriteLn(Format('text %s/%.1f "%s"',
[Elems[I].FontName, Elems[I].FontSize, Elems[I].Text]))
else
WriteLn(Format('image id=%d', [Elems[I].ImageID]));
finally
Pdf.Free;
end;
end;
Létezik itt egy számlálási konvenció, amely követi a könyvtár többi részét, és amelyet tiszteletben kell tartania, különben nem inicializált memóriát fog olvasni. A visszatérési érték a teljes elemszám, amely nagyobb lehet, mint a bemenetként átadott tömb. A függvény csak annyi helyet tölt meg, amennyi belefér, és folytatja a többi számlálását, pontosan úgy, ahogyan az aláírás-felsorolás működik. Így a védelem mindig ugyanaz: korlátozza a ciklust a visszaadott szám és a High(array) közül a kisebbikre, soha ne fusson végig vakon a számon. A fenti példák ezért mutatják a I <= High(...) ellenőrzést. Ha a visszatérési érték meghaladja a puffert, méretezzen nagyobb tömböt és hívja meg újra
Ha használta már a könyvtár alacsonyabb szintű szövegblokk-hívásait, ez a típusos, geometria-tudatos réteg épül föléjük; a mögöttes kinyerés megegyezik a Delphi PDF szöveg, kép és betűtípus kinyerése a PDFlibPas segítségével című cikkben leírttal. És amikor a cél nem az, hogy „hol van ez a szöveg”, hanem az, hogy „hogyan épül fel ez a dokumentum a kisegítő technológiák számára”, a párhuzamos olvasó-oldali történet a tagged-PDF struktúrafa, amely a fizikai blokkelrendezés helyett a logikai olvasási sorrendet mutatja be
Régiólekérdezések, amikor már tudja, hol keresse
Néha egyáltalán nincs keresőszava; csak egy téglalapja van. Egy űrlapsablon mindig a jobb felső sarokba helyezi a számlaszámot, vagy egy szkennelt elrendezés rögzített sávot tart fenn egy táblázatnak. Ezt a célt szolgálja a GetTextInAreaEx. Ez a GetTextInArea határokat hordozó megfelelője: míg a régebbi hívás egy lapos karakterlánc-listát ad vissza egy régióhoz, az új visszaadja a megmaradt blokkok téglalapját a szöveggel együtt, így nemcsak azt tudja meg, hogy mi van a dobozban, hanem azt is, hogy az egyes sorok hol helyezkednek el azon belül
var
Pdf: TPDFlib;
Hits: array[0..63] of TPDFlibSearchHit;
Found, I: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.LoadFromFile('invoice.pdf', '');
Pdf.SelectPage(1);
// Bal, Fent, Szélesség, Magasság PDF pontokban a kiválasztott oldalon
Found := Pdf.GetTextInAreaEx(360, 720, 180, 60, Hits);
for I := 0 to Found - 1 do
if I <= High(Hits) then
WriteLn(Hits[I].MatchText);
finally
Pdf.Free;
end;
end;
Két dolgot kell észben tartani. A GetTextInAreaEx az aktuálisan kiválasztott oldalon működik, ezért először a SelectPage-et kell meghívni; a SearchText metódussal ellentétben nem fogad el tartományt. És a blokk megmarad, ha metszi a lekérdezési téglalapot, nemcsak akkor, ha teljesen benne van, így a határt átszelő sor is átkerül. Ez általában az, amit egy kézzel rajzolt kijelölő doboznál szeretne, de ha szigorú befoglalásra van szüksége, maga is szűrheti a visszaadott téglalapokat, mivel azok most már rendelkezésre állnak
Munkára fogva
Mindhárom hívás közös vonása, hogy a geometria már nem olyasmi, amit utólag kell rekonstruálni. A keresési találat ismeri az oldalát és a dobozát. A lap-elem ismeri a téglalapját és szöveg esetén a betűtípusát. A régiólekérdezés jelenti az egyes sorok elhelyezkedését. Ez elegendő egy valódi kereső-és-kiemelő funkció, egy kattintásra-helyező index vagy egy elrendezés-tudatos kinyerő felépítéséhez anélkül, hogy a nyilvános API alá süllyedne, vagy kézzel építené újjá a szövegkinyerő folyamatot
Ezek a lekérdező API-k a PDFlibPas Delphi PDF Library részeként kerülnek szállításra, a mögöttük álló teljes szövegblokk-kinyerési réteggel és a Delphihez, valamint C++Builderhez készült többi olvasó-oldali vizsgálati felülettel együtt