Ekstrakcija teksta stranice je laka polovica problema. U trenutku kada korisnik upiše riječ u polje za pretraživanje i očekuje da preskoči do nje i iscrta žuti okvir oko nje, treba vam nešto što vam ravni tekst ne može dati: stranicu na kojoj je svaki pogodak, i pravokutnik koji zauzima u PDF koordinatama. Niz spojen preko stranice izgubio je tu geometriju. Substring možete pronaći, ali ne možete pokazati na njega
PDFlibPas je nativna Object Pascal PDF biblioteka za Delphi i C++Builder, a od v3.78.0 odgovara upravo na to pitanje. Tri query API-ja sjede nad postojećim extractorom tekstualnih blokova: SearchTextSearchText prolazi raspon stranica i vraća svaki pogodak s njegovom stranicom i osi poravnatim pravokutnikom, EnumPageElementsEnumPageElements nabraja sve na jednoj stranici (tekstualne blokove i ugrađene slike podjednako), a GetTextInAreaExGetTextInAreaEx prijavljuje pravokutnik svakog bloka unutar regije umjesto da ih spljoštava u listu stringova. Nijedan ne dira write path; to su čista read-side proširenja nad mehanikom koju biblioteka već ima
Zašto geometrija živi u popisu tekstualnih blokova, a ne u funnelu
Prirodni je instinkt ponovno upotrijebiti što god GetPageTextGetPageText radi interno. Taj put prolazi kroz prijelazni extraction "funnel" koji proizvodi string stranice i zatim se sam oslobađa prije nego se poziv vrati. Do trenutka kad držite rezultat, koordinate po bloku su nestale. Nikad nisu ni bile vaše za zadržati
Koordinate ipak opstaju u drugoj strukturi. ExtractPageTextBlocks(3)ExtractPageTextBlocks(3) vraća handle liste tekstualnih blokova čiji elementi svaki nose osmerostruki bounding quad, ime fonta, veličinu fonta i tekst bloka. Taj handle je jedino mjesto gdje je geometrija zadržana nakon ekstrakcije, zbog čega je svaki od novih query API-ja izgrađen na njemu, a ne na funnelu. Ponovna uporaba block liste znači da pretraga, enumeracija i upiti po području dijele jedan extraction pass i jednu definiciju gdje je blok
Dakle, oblik SearchText slijedi iz tog ograničenja. Za svaku stranicu u rasponu izvlači listu blokova, čita tekst svakog bloka s SearchTextGetTextBlockTextGetTextBlockText, testira ga prema upitu i za blokove koji se podudaraju reducira quad u pravokutnik. Pogodak koji vraća mali je zapis:
type
TPDFlibSearchHit = record
Page: Integer; // 1-based page of the match
Left, Top, Right, Bottom: Double; // axis-aligned hit rectangle
MatchText: WideString; // the block text that contained the query
end;
Vezani niz je X/Y isprepleten, a ne četiri kuta
Ovo je detalj koji prvi ugrize. GetTextBlockBound(ListID, Index, BoundIndex)GetTextBlockBoundBoundIndex uzima BoundIndex od 1 do 8, a tih osam vrijednosti nisu "kut 1, kut 2, kut 3, kut 4" s po dva polja grupirana zajedno kako biste možda pretpostavili. One su X, Y, X, Y, X, Y, X, Y: neparni indeksi su X koordinate, parni indeksi su Y koordinate, ukupno četiri točke. Pročitate li ih u pogrešnom sparivanju, vaš je pravokutnik besmislica
Razlog zašto quad uopće postoji, umjesto običnog pravokutnika, jest rotacija. Tekstualni blok postavljen pod kutom ima pravi četverotočkasti poligon obuhvata, a osam double vrijednosti to vjerno opisuju. Za slučaj isticanja i skoka gotovo uvijek želite uspravan okvir, pa biblioteka reducira quad na osi poravnat pravokutnik tako da pregleda četiri točke za njihov minimum i maksimum X i Y. Rotirani tekst kolabira u uspravni okvir koji ga obuhvaća, a to je upravo ono što treba highlight overlayu:
var
Pdf: TPDFlib;
Hits: array[0..255] of TPDFlibSearchHit;
Found, I: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.LoadFromFile('contract.pdf', '');
// Search pages 1 to 10, case-insensitive, substring match.
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;
Imajte na umu da je pravokutnik u PDF user-space točkama s ishodištem u donjem lijevom kutu stranice, istim koordinatnim sustavom koji predajete crtanju i anotacijskim pozivima. To je namjerno: pravokutnik koji dobijete natrag iz search pogotka pravokutnik je koji možete predati izravno highlight anotaciji ili naredbi "scroll here" bez ikakve pretvorbe
Osjetljivost na velika slova, cijele riječi i gdje CJK odstupa
Drugi parametar je TPDFlibSearchOptions set nacrtan iz soCaseSensitive i soWholeWord. Prazan skup [] je uobičajen slučaj: case-insensitive substring pretraga. Dodajte soCaseSensitive da bi Indemnity i indemnity bili različiti, dodajte soWholeWord da biste zaustavili sign da se podudara unutar signature, ili kombinirajte oboje
Whole-word podudaranje traži definiciju što je granica riječi, a ovdje vrijedi pravilo koje treba izričito reći jer je po dizajnu ASCII centričan. Znak se računa kao dio riječi kad je ASCII slovo, ASCII znamenka ili podvlaka: [A-Za-z0-9_] klasa poznata iz pravila identifikatora. Pogodak se kvalificira kao whole-word samo kada znakovi neposredno prije i poslije njega nisu word znakovi (ili se pogodak nalazi na rubu bloka).ne word znakovi (ili se pogodak nalazi na rubu bloka)
Posljedica za nelatinične skripte vrijedna je znati prije nego što isporučite višejezični okvir za pretraživanje. Budući da Han znakovi, kana i druga ne-ASCII slova padaju izvan te klase, svaka granica uz njih čita se kao ne-word rub. U praksi to znači da whole-word pretraga nad CJK tekstom ponaša se kao da je svaka pozicija valjana granica riječi, pa se zastavica tamo zapravo svodi na substring matching. To je dokumentirano ograničenje, ne bug, i odgovara ponašanju na kojem je značajka modelirana. Ako vam je korpus pretežno CJK, whole-word mode neće dati segmentaciju koju bi dao namjenski tokenizer; planirajte oko toga umjesto da se oslanjate na njega
Jedna implementacijska fusnota koja objašnjava klasu suptilnih kvarova drugdje: case-insensitive usporedba koristi UpperCase na WideString, a ne AnsiUpperCase. Ansi varijanta vraća AnsiString, koja se ne bi poravnala s WideString koje ostatak puta koristi, a miješanje te dvije stvara tip mismatch i, još gore, gubitničko skupljanje znakova izvan aktivne code page. Unicode unutra, Unicode van, cijelim putem
Jedan parser page rangea za cijelu biblioteku
Treći parametar je page range string kao "1,3,5-9". Tu nema ničega prilagođenog u načinu parsiranja: isti PLParsePageRangeList koji pokreće PrintPages i rutine za kopiranje stranica to obrađuje i ovdje, pa raspon koji se ispravno ispisuje i ispravno pretražuje. Prazan string raspona je sentinel za "svaka stranica", u kojem slučaju SearchText gradi puni popis sam
Opseg je važan za trošak. Pretraživanje desetstraničnog isječka tisućustraničnog dokumenta izvlači blokove za deset stranica, a ne tisuću, jer petlja bira i izvlači samo stranice koje raspon imenuje. Kad već znate da se članak nalazi u prilogu, recite to u rasponu i preskočite ostatak datoteke
Interno, pretraga i enumeracija mijenjaju odabranu stranicu dok iteriraju, pa svaka spremi trenutno odabranu stranicu pri ulasku i vraća je u finally block. Pozovete li SearchText usred gradnje stranice, vaš odabir je točno ondje gdje ste ga ostavili kada se poziv vrati. Taj ugovor save-and-restore je stvar koju primijetite tek kada nedostaje, a upravo zato postoji
Nabrajanje cijele stranice: tekst i slike na jednom popisu
Search odgovara na "gdje je ova riječ". Druga polovica introspekcije je "što se na ovoj stranici uopće nalazi", i to je EnumPageElements. Vraća jednu objedinjenu listu u kojoj je svaki element ili tekstualni blok ili ugrađena slika, razlikovani po Kind polju:
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; usable with SelectImage / GetImageID
end;
Tekstualni elementi dolaze iz istog ExtractPageTextBlocks prolaza, pa svaki dolazi s pravokutnikom, imenom fonta i veličinom već popunjenim. Elementi slika dolaze iz popisa ugrađenih slika stranice putem FindImages i GetImageID; ImageID koje nose je handle koji gurnete u SelectImage da biste sliku dalje pregledali. Dvije vrste završavaju u jednom nizu tako da jedan prolaz po stranici vidi sve što je na njoj
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;
Tu postoji brojilačka konvencija koja slijedi ostatak biblioteke i koju morate poštovati ili ćete čitati neinicijaliziranu memoriju. Povratna vrijednost je ukupni broj elemenata, koji može biti veći od niza koji ste predali. Funkcija popunjava samo onoliko slotova koliko stane i nastavlja brojati ostatak, točno kao što radi enumeracija potpisa. Zato je zaštita uvijek ista: ograničite petlju na manju od vraćenog broja i High(array), nikad ne iterirajte do counta naslijepo. Primjeri gore pokazuju I <= High(...) provjeru iz tog razloga. Ako povratna vrijednost prelazi vaš buffer, dodijelite veći niz i pozovite ponovno
Ako ste koristili niže razine text-block poziva, ovo je tipizirani sloj s geometrijskom sviješću iznad njih; underlying extraction je isti onaj opisan u Delphi PDF text, image, and font extraction with PDFlibPas. A kad cilj nije "gdje je ovaj tekst" nego "kako je ovaj dokument strukturiran za asistivnu tehnologiju", paralelna read-side priča je tagged-PDF structure tree, koji izlaže logički redoslijed čitanja umjesto fizičkog rasporeda blokova
Upiti po području kad već znate gdje gledati
Ponekad uopće nemate pojavni pojam; imate pravokutnik. Predložak obrasca uvijek stavlja broj računa u gornji desni kut, ili skenirani layout rezervira fiksnu traku za tablicu. GetTextInAreaExGetTextInAreaEx serves that case. It is the bounds-carrying counterpart of GetTextInArea: where the older call hands back a flat list of strings for a region, the new one returns each retained block's rectangle alongside its text, so you learn not just what is in the box but where inside it each line sits
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);
// Left, Top, Width, Height in PDF points on the selected page.
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;
Dvije stvari držite ravno. GetTextInAreaExGetTextInAreaEx works on the currently selected page, so call SelectPage first; unlike SearchText, it does not take a range. And a block is kept when it intersects the query rectangle, not only when it is fully contained, so a line that straddles boundary still comes through. That is usually what you want for a hand-drawn selection box, but if you need strict containment you can filter the returned rectangles yourself, since you now have them
Staviti u rad
Zajednička nit kroz sva tri poziva jest da geometrija više nije nešto što rekonstruirate naknadno. Search hit zna svoju stranicu i okvir. Page element zna svoj pravokutnik i, za tekst, font. Region query prijavljuje gdje svaka linija pada. To je dovoljno da se izgradi pravi find-and-highlight feature, click-to-locate indeks ili layout-aware extractor bez spuštanja ispod javnog API-ja ili ručne izgradnje pipelinea za ekstrakciju teksta
Ovi query API-ji dolaze kao dio PDFlibPas Delphi PDF Library, uz puni sloj ekstrakcije tekstualnih blokova na kojem se temelje i ostatak read-side introspection surface za Delphi i C++Builder