At trække tekst, billeder og skrifttyper ud af en eksisterende PDF lyder som et løst problem, indtil du kører et rigtigt korpus igennem. Peg en søgeindekserer mod fyrre tusind kundefiler, og bruddene sorterer sig i nogle få genkendelige bunker. Ord løber sammen, fordi ingen fortalte udtrækkeren, hvor bredt et mellemrum skal være for at tælle som et blanktegn. Andre sider kommer tilbage som volapyk, fordi en subsettet skrifttype ikke bærer nogen afbildning fra sine glyfkoder til faktiske tegn. Og "firmalogoet" viser sig at være ni separate billedobjekter stablet bag en soft mask. Intet af det er en fejl i biblioteket. Det er forskellen på at kalde en udtræksfunktion og på at forstå, hvad funktionen kan og ikke kan genskabe ud fra de bytes, der ligger på disken
losLab PDF Library, Pascal-udgaven, giver Delphi- og C++Builder-kode mere end én måde at læse hver af de tre strømme på, og niveauerne adskiller sig i, hvad de garanterer. Kunsten er at matche niveauet til opgaven: et søgeindeks, en gennemgang af redaktioner og en PDF/A-preflight vil hver især have noget forskelligt ud af den samme side, og at gribe efter det forkerte kald spilder arbejde eller producerer output, du ikke kan stole på
Niveauer for tekstudtræk, og hvad hvert enkelt lover
GetPageText tager en options-værdi fra 0 til 8, og det tal vælger en motor snarere end et format. Værdierne 0 til 2 kører et let gennemløb, der er fint til en hurtig forhåndsvisning. Værdierne 3 til 8 går gennem den layoutbevidste motor, som genopbygger linjer og mellemrum ud fra, hvor glyfferne faktisk sidder på siden. Inden for det interval betyder varianterne noget: 4 og 6 deler outputtet op i ord, 5 og 6 udsender bredder per glyf, og 7 returnerer ren tekst, hvor skrifttype-, farve- og blokmetadata bevidst er kasseret. Option 7 er den, man fodrer et søgeindeks med, eftersom indekset kun vil have ord og intet andet
Ingen options-indstilling kan redde et dokument, der aldrig bar informationen til at begynde med. PDF afbilder tegnkoder på glyfformer, og det eneste, der afbilder de koder tilbage til læsbar tekst, er en skrifttypes ToUnicode-CMap (ISO 32000-1 §9.10). Når en subsettet skrifttype leveres uden en, står enhver udtrækker fast. Dette bibliotek, kopiér-og-indsæt i en fremviser, et konkurrerende toolkit: alle er reduceret til at gætte ud fra glyfnavne eller til at returnere ingenting. Det praktiske svar er detektion, ikke heltemod. Giv siden en lav tillidsscore, og send den til OCR, for at indeksere volapykken i stilhed er værre end at indrømme, at du ikke kan læse den
Til de tilfælde, de flade options ikke dækker, altså egen tokenisering, forensik på indholdsstrømme, en teksttragt bygget efter dine egne regler, er dekoderen tilgængelig ét lag længere nede. TPDFExtractor konstrueres oven på en sides ressourceordbog og skrifttypesamling. Dens metode ExtractTextW kører rå tekstoperationer fra indholdsstrømmen tilbage gennem det samme skrifttypemaskineri for at genskabe Unicode, og dens hændelse OnFindObject rækker dig hvert objekt, efterhånden som det strømmer forbi. Det meste kode har aldrig brug for at nå så dybt. De programmer, der har, er dem, der er glade for, at laget er offentligt frem for begravet
Positionerede blokke: enheden bag søgetræffere og redaktionsgennemgang
Ren tekst fortæller dig, hvad siden siger. Før eller siden har et produkt også brug for at vide, hvor den siger det, så det kan fremhæve en søgetræffer, tegne en kasse om en redaktionskandidat eller forankre en annotation det rigtige sted. ExtractPageTextBlocks returnerer et handle til en liste af tekststrøg, og hvert strøg bærer sin tekst, sin afgrænsningsramme og det skrifttypenavn og den størrelse, det blev sat i:
var
Pdf: TPDFlib;
Blocks, I: Integer;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('contract.pdf', '') <> 1 then
raise Exception.Create('load failed');
Pdf.SelectPage(1);
Blocks := Pdf.ExtractPageTextBlocks(0);
for I := 0 to Pdf.GetTextBlockCount(Blocks) - 1 do
Writeln(Format('%s [%s %.1f pt at %.0f,%.0f]',
[Pdf.GetTextBlockText(Blocks, I),
Pdf.GetTextBlockFontName(Blocks, I),
Pdf.GetTextBlockFontSize(Blocks, I),
Pdf.GetTextBlockBound(Blocks, I, 0),
Pdf.GetTextBlockBound(Blocks, I, 1)]));
Pdf.ReleaseTextBlocks(Blocks);
finally
Pdf.Free;
end;
end;
Én detalje på dette område spænder ben for integrationer oftere end nogen anden. SetTextExtractionArea, SetTextExtractionWordGap og SetTextExtractionOptions er tilstand på dokumentniveau, der bliver hængende, ikke argumenter du sender per kald. Konfigurér en områdebegrænsning til én funktion, lad os sige at læse kun sidehovedbåndet for at klassificere et dokument, og den afkorter stiltiende ethvert udtræk, der følger på det samme handle, inklusive de layoutbevidste GetPageText-niveauer, du griber efter senere. Nulstil enten udtrækstilstanden mellem logiske opgaver, eller giv hver opgave sit eget dokumenthandle
Tærsklen for ordmellemrum er håndtaget til den første bunke af fejl, de ord der løber sammen. SetTextExtractionWordGap fortæller layoutmotoren, hvor meget vandret plads, målt op mod sidens egen glyfafstand, der skiller ét ord fra det næste. En tæt tabel vil have et mindre mellemrum end en luftigt sat markedsføringsside, så en tærskel afstemt per dokumentklasse slår én global konstant. Den bliver hængende på dokumentet ligesom resten af udtrækstilstanden, så planlæg at sætte den bevidst frem for én gang for alle at glemme den
Billeder: originale strømme, ikke skærmbilleder
Den forkerte måde at få billeder ud af en PDF på er at gengive siden og beskære den. Det gensampler pixels, brænder enhver rotation ind og smider væk, hvad originalen end var. GetPageImageList optæller i stedet de faktiske billedressourcer, siden refererer til, og hvert element rækker sine egenskaber og sine originale, uforstyrrede data tilbage:
var
ImgList, I: Integer;
begin
Pdf.SelectPage(1);
ImgList := Pdf.GetPageImageList(0);
for I := 0 to Pdf.GetImageListCount(ImgList) - 1 do
begin
Writeln(Pdf.GetImageListItemFormatDesc(ImgList, I, 0));
Pdf.SaveImageListItemDataToFile(ImgList, I, 0,
Format('page1-img%.2d.bin', [I]));
end;
Pdf.ReleaseImageList(ImgList);
end;
Tjek GetImageListItemFormatDesc, før du antager noget om et element, for det, en side refererer til, er sjældent ét pænt billede per synligt billede. En soft mask optræder som sin egen separate post. Den samme XObject gentager sig ofte på tværs af mange sider, så fjern dubletter efter indholdshash, før du arkiverer en eksport med "alle billeder", ellers skriver du det samme logo hundrede gange. CMYK-JPEG'er kræver farvestyring længere nede i kæden, ellers gengives de inverteret i fremvisere, der tager kanalerne for pålydende. Vil du have en optælling for hele dokumentet frem for én side ad gangen, scanner FindImages sammen med SetFindImagesMode hele filen i ét gennemløb
Der er én grænse, det er værd at rejse over for interessenterne, før nogen skriver acceptkriterier: billedudtræk returnerer kun rasterressourcer. Et logo eller en graf tegnet som vektorbaner er ikke et billede i ressourceforstand og vil aldrig dukke op i nogen billedliste, uanset hvor tydeligt det læses som et billede på skærmen. Når kravet i virkeligheden er at levere den graf som en fil, er den ærlige fremgangsmåde at gengive sideregionen til en bitmap, hvilket er en anden operation med en anden troværdighed. De to slags output hører ikke hjemme i samme eksportmappe uden en mærkat, der siger, hvad der er hvad
Skrifttyper: en revisionsflade, ikke en eksportfunktion
Skrifttype-API'et besvarer spørgsmål om skrifttyper. Det rækker dig ikke selve skrifttypefilerne, og den skelnen former alt, hvad du kan bygge oven på det. Efter at FindFonts har scannet dokumentet, gennemløber optællingen skrifttyperne efter ID, og egenskabskaldene melder om den skrifttype, der aktuelt er valgt:
var
I: Integer;
begin
Pdf.FindFonts;
for I := 1 to Pdf.FontCount do // skrifttypeindekser starter ved 1, ikke 0
if Pdf.SelectFont(Pdf.GetFontID(I)) = 1 then
Writeln(Format('%s type=%d embedded=%d subset=%d',
[Pdf.FontName, Pdf.FontType,
Pdf.GetFontIsEmbedded, Pdf.GetFontIsSubsetted]));
end;
Hold øje med løkkens grænser. Skrifttypeindekser går fra 1 til FontCount, mens indekserne for tekstblokke og billedlister nogle afsnit længere oppe tæller fra nul. Bær den ene konvention over i den anden, og du får en off-by-one, der enten springer den første skrifttype over eller løber ud over enden, og den vil bestå en overfladisk test, fordi de fleste dokumenter har flere skrifttyper, og den forkerte ser stadig plausibel ud. Vær også klar omkring omfanget. Dette API har ingen skrifttypeeksport på byteniveau. Intet kald returnerer det indlejrede skrifttypeprogram som en TTF- eller OTF-fil, og optælling plus inspektion af metadata er hele den tiltænkte model. Den model dækker stadig det, produktionsarbejde faktisk beder skrifttyper om: subset-detektion efter navnemønster, revision af indlejring før en arkivkonvertering (en ikke-indlejret skrifttype er en hård PDF/A-blokering, som PDF/A- og PDF/UA-preflight i Delphi går i dybden med) og kodningsdiagnostik til, når tilliden til udtrækket falder. Der er også en licensmæssig grund til, at grænsen ligger her. Et subset-skrifttypeprogram er licensbelagt materiale og er, uden de fleste af sine glyffer, alligevel ubrugeligt som en skrifttype, man kan installere. At behandle det som revisionsmetadata frem for et udtrækbart aktiv er den position, du kan forsvare
Det sidste kald bærer sin vægt i triagen. Kør GetFontEncoding på hver skrifttype, læs det sammen med subset-flaget, og du kan forudsige udtrækskvaliteten, før du henter et eneste tegn. En side, hvis skrifttyper alle er subsettede med ikke-standardiserede kodninger, er en OCR-kandidat alene ved eftersyn, hvilket lader en batch-pipeline dirigere den rigtigt uden først at spilde et mislykket udtræksgennemløb på den
Udtræk i stor skala uden at indlæse dokumenter
I en batch-pipeline er det spildt I/O at indlæse et helt dokument bare for at læse én side, og det løber hurtigt op på tværs af et korpus. Enkeltkaldsvarianterne, ExtractFilePageText og ExtractFilePageTextBlocks, tager et filnavn, en adgangskode og et sidetal direkte og springer den fulde indlæsning over. Til filer i gigabyteklassen findes der et endnu lavere gear. Direct access-stien åbner en fil gennem streaming-læsninger af xref, så DAOpenFileReadOnly efterfulgt af DAExtractPageText kun rører de objekter, netop den ene side har brug for. Det kommer med et konventionsskifte, det er værd at lære udenad: DA-funktionerne adresserer sider via PageRef, et objektreference-handle du får fra DAFindPage, aldrig via råt sidetal. Send tallet, hvor handlet hører til, og kaldet arbejder på det forkerte objekt uden at rejse en fejl, hvilket er den værste slags fejl at fejlsøge. Resten af direct access-værktøjskassen er lagt frem i fletning, opdeling og direct access for store PDF'er
Er der én vane, der skiller udtrækskode, som overlever et rigtigt korpus, fra kode der halter, er det at behandle siden som upålideligt input frem for som en ren datakilde. Tekst, der er uenig med det, fremviseren gengiver, er næsten altid et kodningsproblem, en ligatur der falder sammen til én glyf eller en subset-skrifttype uden sine ToUnicode-poster, og løsningen er at måle tilliden og dirigere de dårlige sider til OCR, ikke at slås med de bytes. Skrifttype-API'et vil aldrig producere en TTF eller OTF, sådan er det designet, så byg arbejdsgange om skrifttyper op omkring revisionsspørgsmål. Og den vedvarende udtrækstilstand, områderektanglet ikke mindst, er en indstilling, du ejer, så længe et dokumenthandle lever, ikke en parameter du glemmer efter ét kald. Få de tre reflekser rigtige, og resten af API'et opfører sig
Evalueringsbuilds, demoprojekter og den komplette API-reference for udtræk findes på produktsiden for losLab PDF Library til Delphi