Å trekke ut teksten på en side er den enkle halvdelen av problemet. I det øyeblikket en bruker skriver et ord i et søkefelt og forventer at viseren hopper dit og tegner en gul ramme rundt det, trenger du noe den flate tekststrengen ikke kan gi deg: siden hvert treff ligger på, og rektangelet det opptar i PDF-koordinater. En streng som er satt sammen over en side har mistet den geometrien. Du kan finne delstrengen, men du kan ikke peke på den
PDFlibPas er et innebygd Object Pascal PDF-bibliotek for Delphi og C++Builder, og fra v3.78.0 svarer det akkurat på det spørsmålet. Tre spørre-API-er ligger oppå den eksisterende tekstblokk-uttrekkeren: SearchText går gjennom et sideområde og returnerer hvert treff med siden og et aksejustert rektangel, EnumPageElements lister alt på én side, både tekstblokker og innebygde bilder, og GetTextInAreaEx rapporterer rektangelet for hver blokk innenfor et område i stedet for å flate dem ut til en strengliste. Ingen av dem berører skrivebanen, de er rene leseutvidelser over mekanikken biblioteket allerede hadde
Hvorfor geometrien ligger i tekstblokklisten, ikke i det midlertidige ekstraksjonssteget
Den naturlige fristelsen er å gjenbruke det GetPageText kjører internt. Den banen går gjennom et midlertidig ekstraksjonstrinn som lager sidens streng og deretter frigjør seg før kallet returnerer. I det øyeblikket du sitter med resultatet, er koordinatene per blokk borte. De var aldri dine å beholde
Koordinatene overlever i en annen struktur. ExtractPageTextBlocks(3) returnerer et håndtak til en tekstblokkliste der hvert element har et åtte-dobbelt avgrensningskvadrilateral, et skriftnavn, en skriftstørrelse og blokkens tekst. Det håndtaket er det eneste stedet geometrien bevares etter ekstraksjon, og det er derfor alle de nye spørre-API-ene er bygget på det i stedet for på ekstraksjonssteget. Når du gjenbruker blokklisten, deler søk, opplisting og områdespørringer én uttrekksrunde og én definisjon av hvor en blokk er
Så formen til SearchText følger av dette kravet. For hver side i området henter den ut blokklisten, leser teksten i hver blokk med GetTextBlockText, tester den mot spørringen, og for blokkene som matcher reduserer den kvadrilateralet til et rektangel. Treffet den returnerer, er en liten record:
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;
Bound-arrayet er X/Y-vekslet, ikke fire hjørner
Dette er detaljen som biter først. GetTextBlockBound(ListID, Index, BoundIndex) tar en BoundIndex fra 1 til 8, og de åtte verdiene er ikke «hjørne 1, hjørne 2, hjørne 3, hjørne 4» med to felter per hjørne slik du kanskje skulle tro. De er X, Y, X, Y, X, Y, X, Y: de oddetallsindeksene er X-koordinater, de partallsindeksene er Y-koordinater, fire punkter i alt. Leser du dem i feil paring, blir rektangelet meningsløst
Grunnen til at det i det hele tatt finnes et kvadrilateral i stedet for et vanlig rektangel, er rotasjon. En tekstblokk som står i vinkel har et ekte begrensningspolygon med fire punkter, og de åtte dobbeltverdiene beskriver det trofast. For brukstilfellet med markering og hopp vil du nesten alltid ha en oppreist boks i stedet, så biblioteket reduserer kvadrilateralet til et aksejustert rektangel ved å hente ut minimums- og maksimumsverdiene for X og Y på de fire punktene. Rotert tekst kollapser til den oppreiste boksen som omslutter den, og det er akkurat det et markeringslag trenger:
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;
Legg merke til at rektangelet er i PDF-brukerromspunkter med origo nederst til venstre på siden, det samme koordinatsystemet du sender til tegne- og annotasjonskall. Det er gjort med vilje: rektangelet du får tilbake fra et søketreff er rektangelet du kan sende rett til en markeringsannotasjon eller en «rull hit»-kommando uten å konvertere noe som helst
Store og små bokstaver, hele ord og hvor CJK skiller seg
Det andre parameteret er et TPDFlibSearchOptions-sett satt sammen av soCaseSensitive og soWholeWord. Det tomme settet [] er normaltilfellet: et søk etter delstrenger uten hensyn til store og små bokstaver. Legg til soCaseSensitive for å skille mellom Indemnity og indemnity, legg til soWholeWord for å hindre at sign matcher inne i signature, eller kombiner begge
Hele-ord-samsvar trenger en definisjon av hva en ordgrense er, og her er regelen verdt å si rett ut fordi den er bevisst ASCII-sentrisk. Et tegn regnes som del av et ord når det er en ASCII-bokstav, et ASCII-siffer eller understrek: klassen [A-Za-z0-9_] som er kjent fra identifikatorregler. Et treff kvalifiserer bare som hele ord når tegnene rett før og etter det ikke er ordtegn, eller når treffet ligger helt i kanten av blokken
Konsekvensen for ikke-latinske skript er noe du bør vite før du sender ut et flerspråklig søkefelt. Fordi Han-tegn, kana og andre ikke-ASCII-bokstaver faller utenfor den klassen, leses hver grense ved siden av dem som en ikke-ordgrense. I praksis betyr det at heltordsøk over CJK-tekst oppfører seg som om hver posisjon var en gyldig ordgrense, så flagget i praksis faller tilbake til delstrengssøk der. Det er en dokumentert begrensning, ikke en feil, og det samsvarer med oppførselen funksjonen ble modellert etter. Hvis korpuset ditt hovedsakelig er CJK, vil ikke heltordsmodus gi deg segmenteringen en egen tokenizer ville gjort; planlegg rundt den i stedet for å stole på den
En liten implementasjonsnote som forklarer en klasse subtile feil andre steder: den case-ufølsomme sammenligningen bruker UpperCase på WideString, ikke AnsiUpperCase. Ansi-varianten returnerer en AnsiString, som ikke ville stilt seg på linje med WideString resten av banen bruker, og å blande de to gir typefeil og, enda verre, tapende folding for tegn utenfor den aktive kode siden. Unicode inn, Unicode ut, hele veien
Én parser for sideområder i hele biblioteket
Det tredje parameteret er en sideområdestreng som "1,3,5-9". Det er ingenting spesialtilpasset i hvordan den parses: den samme PLParsePageRangeList som ligger under PrintPages og rutene for sidekopiering, håndterer dette også her, så et område som skriver ut riktig, søker riktig. En tom områdestreng er markøren for «hver side», og i så fall bygger SearchText hele listen selv
Omfang betyr noe for kostnaden. Å søke i et ti-siders utsnitt av et dokument på tusen sider henter ut blokker for ti sider, ikke tusen, fordi løkken bare velger og ekstraherer sidene området navngir. Når du allerede vet at en klausul ligger i appendikset, oppgi det i området og hopp over resten av filen
Internt endrer både søk og opplisting valgt side mens de itererer, så hver av dem lagrer den valgte siden ved inngangen og gjenoppretter den i en finally-blokk. Kaller du SearchText midt i å bygge en side, er valget ditt akkurat der du lot det være når kallet returnerer. Den lagre-og-gjenopprett-avtalen er den typen ting du bare legger merke til når den mangler, og nettopp derfor finnes den
Å enumerere en hel side: tekst og bilder i én liste
Søket svarer på «hvor er dette ordet». Den andre halvdelen av introspeksjonen er «hva finnes på denne siden i det hele tatt», og det er EnumPageElements. Den returnerer én samlet liste der hvert element enten er en tekstblokk eller et innebygd bilde, skilt av et Kind-felt:
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;
Tekstelementene kommer fra den samme ExtractPageTextBlocks-runden, så hvert av dem ankommer med rektangelet sitt, skriftnavnet sitt og størrelsen sin allerede fylt ut. Bildeelementene kommer fra sidens innebygde bildeliste via FindImages og GetImageID; ImageID-en de bærer er håndtaket du sender til SelectImage for å se nærmere på bildet. De to typene havner i én matrise, slik at en enkelt gjennomgang av en side ser alt som finnes på den
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;
Her følger en tellekonvensjon som er lik resten av biblioteket, og som du må respektere ellers leser du uinitialisert minne. Returverdien er det totale antallet elementer, som kan være større enn arrayet du sendte inn. Funksjonen fyller bare så mange plasser som får plass, og fortsetter å telle resten, nøyaktig slik signaturen for opplisting fungerer. Vernet er derfor alltid det samme: begrens løkken til det minste av returverdien og High(array), aldri iterer blindt til tellingen. Eksemplene over viser I <= High(...)-sjekken av den grunn. Hvis returverdien er større enn bufferen din, trenger du en større matrise og et nytt kall
Hvis du har brukt bibliotekets lavere nivå for tekstblokk-kall, er dette det typede, geometribevisste laget over dem, og den underliggende ekstraksjonen er den samme som er beskrevet i Delphi PDF-tekst-, bilde- og skrifttypeuttrekking med PDFlibPas. Og når målet ikke er «hvor er denne teksten» men «hvordan er dette dokumentet strukturert for hjelpeteknologi», er den parallelle lesehistorien det taggede PDF-strukturtreet, som viser den logiske leseordenen i stedet for den fysiske blokkoppsettet
Områdeoppgaver når du allerede vet hvor du skal se
Noen ganger har du ikke et søkeord i det hele tatt, du har et rektangel. En mal for skjemaer plasserer alltid fakturanummeret øverst til høyre, eller et skannet oppsett reserverer et fast felt til en tabell. GetTextInAreaEx dekker det tilfellet. Det er motstykket til GetTextInArea som bærer med seg grenser: der det eldre kallet gir deg en flat liste med strenger for et område, returnerer det nye rektangelet til hver bevart blokk sammen med teksten, slik at du ikke bare ser hva som er i boksen, men også hvor inne i den hver linje ligger
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;
To ting må holdes atskilt. GetTextInAreaEx virker på den valgte siden, så kall SelectPage først, og i motsetning til SearchText tar den ikke noe område. Og en blokk beholdes når den krysser spørringsrektangelet, ikke bare når den ligger helt inni det, så en linje som går over grensen kommer fortsatt med. Det er som regel det du vil ha for en manuelt tegnet markeringsboks, men hvis du trenger streng innkapsling kan du filtrere de returnerte rektanglene selv, siden du nå har dem
Å sette det i arbeid
Den røde tråden gjennom alle tre kallene er at geometrien ikke lenger er noe du må rekonstruere i etterkant. Et søketreff kjenner siden og boksen sin. Et sideelement kjenner rektangelet sitt og, for tekst, skrifttypen. En områdespørring rapporterer hvor hver linje faller. Det er nok til å bygge en ekte finn-og-marker-funksjon, et klikk-for-å-finne-indeksoppslag, eller en layoutbevisst uttrekker uten å gå under den offentlige API-en eller bygge tekstuttrekksrøret på nytt for hånd
Disse spørre-API-ene følger med som del av PDFlibPas Delphi PDF Library, sammen med hele tekstblokk-uttrekkslaget de bygger på og resten av leseinnsikten for Delphi og C++Builder