Teknisk artikkel

Tekst-, bilde- og skriftuttrekking fra PDF i Delphi med PDF Library for Delphi

Å trekke tekst, bilder og skrifter ut av en eksisterende PDF høres ut som et løst problem helt til du kjører et virkelig korpus gjennom det. Rett en søkeindekserer mot førti tusen kundefiler, og bruddene sorterer seg i noen få gjenkjennelige hauger. Ord løper sammen fordi ingen fortalte uttrekkeren hvor bredt et mellomrom må være for å telle som et ordskille. Andre sider kommer tilbake som volapyk fordi en delmengdeskrift ikke bærer noe kart fra glyffkodene sine til faktiske tegn. Og «firmalogoen» viser seg å være ni separate bildeobjekter stablet bak en myk maske. Ingenting av dette er en feil i biblioteket. Det er forskjellen på å kalle en uttrekksfunksjon og å forstå hva funksjonen kan og ikke kan gjenvinne fra bytene på disk

losLab PDF Library, Pascal-utgaven, gir Delphi- og C++Builder-kode mer enn én måte å lese hver av disse tre strømmene på, og nivåene skiller seg i hva de garanterer. Kunsten er å tilpasse nivået til oppgaven: en søkeindeks, en gjennomgang av sladding og en PDF/A-preflight vil alle ha forskjellige ting ut av den samme siden, og å gripe etter feil kall sløser med innsats eller produserer utdata du ikke kan stole på

Nivåer for tekstuttrekk og hva hvert av dem lover

GetPageText tar en opsjonsverdi fra 0 til 8, og det tallet velger en motor snarere enn et format. Verdiene 0 til 2 kjører en lett runde som holder for en rask forhåndsvisning. Verdiene 3 til 8 går gjennom den oppsettsbevisste motoren, som bygger linjer og avstander opp igjen fra der glyffene faktisk ligger på siden. Innenfor det spennet betyr variasjonene noe: 4 og 6 deler utdataene i ord, 5 og 6 sender ut bredder per glyff, og 7 returnerer ren tekst der skrift, farge og blokkmetadata bevisst er droppet. Opsjon 7 er den du mater en søkeindeks med, siden indeksen vil ha ord og ingenting annet

Ingen opsjonsinnstilling kan redde et dokument som aldri bar informasjonen i utgangspunktet. PDF avbilder tegnkoder på glyffformer, og det eneste som avbilder de kodene tilbake til lesbar tekst, er en skrifts ToUnicode-CMap (ISO 32000-1 §9.10). Når en delmengdeskrift leveres uten en, står enhver uttrekker fast. Dette biblioteket, kopier-og-lim i en leser, en konkurrerende verktøykasse: alle er redusert til å gjette ut fra glyffnavn eller returnere ingenting. Den praktiske responsen er deteksjon, ikke heltedåd. Gi siden lav konfidens og send den til OCR, for å indeksere søppelet stilltiende er verre enn å innrømme at du ikke kan lese det

Diagram over Delphi PDF-tekstuttrekksnivåer: GetPageText alternativene 0 til 8 rutes til et lettvektspass eller den layoutbevisste motoren, og sider hvis subsettede skrifter mangler en ToUnicode CMap faller tilbake til OCR
GetPageText-alternativverdier 0 til 8 velger mellom et lettvekts forhåndsvisningsgjennomløp og den layoutbevisste motoren, med alternativ 7 reservert for søkeindeksering og manglende ToUnicode CMaps omdirigert til OCR

For de tilfellene de flate opsjonene ikke dekker, egendefinert tokenisering, etterforskning i innholdsstrømmen, en teksttrakt bygd etter dine egne regler, er dekoderen tilgjengelig ett lag ned. TPDFExtractor konstrueres over en sides ressursordbok og skriftsamling. Metoden ExtractTextW kjører rå tekstoperasjoner fra innholdsstrømmen tilbake gjennom det samme skriftmaskineriet for å gjenvinne Unicode, og hendelsen OnFindObject gir deg hvert objekt etter hvert som det strømmer forbi. De fleste koder trenger aldri å nå så dypt. Applikasjonene som gjør det, er de som er glade for at laget er offentlig framfor begravd

Plasserte blokker: enheten for søketreff og gjennomgang av sladding

Ren tekst forteller deg hva siden sier. Før eller siden trenger et produkt også å vite hvor den sier det, slik at det kan framheve et søketreff, tegne en boks rundt en sladdingskandidat eller forankre en annotasjon på rett sted. ExtractPageTextBlocks returnerer et håndtak til en liste over tekstrekker, og hver rekke bærer teksten sin, sin omsluttende ramme og skriftnavnet og størrelsen den ble satt 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 detalj på dette området feller integrasjoner mer enn noen annen. SetTextExtractionArea, SetTextExtractionWordGap og SetTextExtractionOptions er tilstand på dokumentnivå som vedvarer, ikke argumenter du sender per kall. Konfigurer en områdebegrensning for én funksjon, si å lese bare topptekstbåndet for å klassifisere et dokument, og den avkorter stilltiende hvert uttrekk som følger på det samme håndtaket, inkludert de oppsettsbevisste GetPageText-nivåene du griper til senere. Enten nullstill uttrekkstilstanden mellom logiske oppgaver, eller gi hver oppgave sitt eget dokumenthåndtak

Terskelen for ordmellomrom er spaken for den første feilhaugen, ordene som løper sammen. SetTextExtractionWordGap forteller oppsettsmotoren hvor mye vannrett plass, målt mot sidens egen glyffavstand, som skiller ett ord fra det neste. En tett tabell vil ha et mindre mellomrom enn en luftig satt markedsføringsside, så en terskel justert per dokumentklasse slår én global konstant. Den vedvarer på dokumentet som resten av uttrekkstilstanden, så planlegg å sette den bevisst framfor én gang og glemme den

Diagram som viser dokumentnivå PDF-uttrekkstilstand i Delphi som varer på tvers av kall på ett handle til nullstilling, noe som hindrer stille avkorting av senere uttrekk
Uttreksområde, ordmellomrom og alternativinnstillinger vedvarer på dokumenthåndtaket, så et område avgrenset for én funksjon forkorter i stillhet alle senere uttrekk til tilstanden nullstilles eller håndtaket byttes

Bilder: originale strømmer, ikke skjermbilder

Den gale måten å hente bilder ut av en PDF på er å rendre siden og beskjære den. Det resampler pikslene, brenner inn enhver rotasjon og kaster bort det originalen var. GetPageImageList teller i stedet opp de faktiske bilderessursene siden refererer til, og hvert element gir tilbake egenskapene sine og sine originale, uberørte data:

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;

Sjekk GetImageListItemFormatDesc før du antar noe om et element, for det en side refererer til, er sjelden ett ryddig bilde per synlige illustrasjon. En myk maske dukker opp som sin egen separate oppføring. Den samme XObject-en gjentar seg ofte over mange sider, så fjern duplikater ved innholdshash før du arkiverer en eksport med «alle bilder», ellers skriver du den samme logoen hundre ganger. CMYK-JPEG-er trenger fargestyring anvendt nedstrøms, ellers gjengis de invertert i lesere som tar kanalene for pålydende. Vil du ha en inventarliste for hele dokumentet framfor én side om gangen, skanner FindImages sammen med SetFindImagesMode hele filen i én runde

Det er én grense verdt å ta opp med interessentene før noen skriver aksepteringskriterier: bildeuttrekk returnerer kun rasterressurser. En logo eller et diagram tegnet som vektorbaner er ikke et bilde i ressursforstand og vil aldri dukke opp i noen bildeliste, uansett hvor tydelig det leses som en illustrasjon på skjermen. Når kravet virkelig er å levere det diagrammet som en fil, er den ærlige tilnærmingen å rendre sideområdet til en punktgrafikk, som er en annen operasjon med en annen troverdighet. De to slagene utdata hører ikke hjemme i samme eksportmappe uten en merkelapp som sier hva som er hva

Sammenligning av å rendere en Delphi PDF-side for å kapre bilder mot å trekke ut originale bildestrømmer med GetPageImageList, inkludert forbehold om myk maske, duplisert XObject og CMYK
Rendring og beskjæring resampler piksler og kaster original bildedata, mens GetPageImageList oppregner de lagrede bildressursene med sine egenskaper og uforstyrrede strømmer

Skrifter: en granskingsflate, ikke en eksportfunksjon

Skrift-API-et besvarer spørsmål om skrifter. Det gir deg ikke selve skriftfilene, og det skillet former alt du kan bygge på det. Etter at FindFonts har skannet dokumentet, går opptellingen gjennom skriftene etter ID, og egenskapskallene melder om den skriften som er valgt akkurat da:

var
  I: Integer;
begin
  Pdf.FindFonts;
  for I := 1 to Pdf.FontCount do        // skriftindekser starter på 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;

Følg med på sløyfegrensene. Skriftindekser går fra 1 til FontCount, mens indeksene for tekstblokker og bildelister noen avsnitt lenger opp starter på null. Tar du den ene konvensjonen med inn i den andre, får du en av-med-én-feil som enten hopper over den første skriften eller løper forbi enden, og den vil bestå overfladisk testing fordi de fleste dokumenter har flere skrifter, og feil skrift ser fortsatt plausibel ut. Vær tydelig på omfanget også. Dette API-et har ingen skrifteksport på byte-nivå. Ingen kall returnerer det innebygde skriftprogrammet som en TTF- eller OTF-fil, og opptelling pluss inspeksjon av metadata er hele den tiltenkte modellen. Den modellen dekker likevel det produksjonsarbeid faktisk spør skrifter om: deteksjon av delmengder etter navnemønster, granskinger av innebygging før en arkivkonvertering (en ikke-innebygd skrift er en hard blokkering for PDF/A, som PDF/A- og PDF/UA-preflight i Delphi går inn på), og kodingsdiagnostikk for når uttrekkskonfidensen faller. Det finnes også en lisensgrunn til at grensen ligger her. Et delmengdeskriftprogram er lisensiert materiale og, siden det mangler de fleste glyffene sine, uansett ubrukelig som en installerbar skrift. Å behandle det som granskingsmetadata framfor et uttrekkbart aktivum er den posisjonen du kan forsvare

Det siste kallet gjør nytten sin i triagen. Kjør GetFontEncoding på hver skrift, les den sammen med delmengdeflagget, og du kan forutsi uttrekkskvaliteten før du henter ut ett eneste tegn. En side der alle skriftene er delmengder med ikke-standard koding, er en OCR-kandidat allerede ved inspeksjon, noe som lar en batchpipeline rute den riktig uten først å kaste bort en mislykket uttrekksrunde på den

Uttrekk i stor skala uten å laste dokumenter

I en batchpipeline er det å laste et helt dokument bare for å lese én side bortkastet I/O, og det hoper seg raskt opp over et korpus. Variantene med ett kall, ExtractFilePageText og ExtractFilePageTextBlocks, tar filnavn, passord og sidenummer direkte og hopper over den fulle lastingen. For filer i gigabyteklassen finnes det et enda lavere gir. Veien med direkte tilgang åpner en fil gjennom strømmende xref-lesinger, så DAOpenFileReadOnly etterfulgt av DAExtractPageText berører bare de objektene den ene siden faktisk trenger. Den kommer med et konvensjonsskifte verdt å legge på minnet: DA-funksjonene adresserer sider med PageRef, et objektreferansehåndtak du får fra DAFindPage, aldri med rått sidenummer. Send inn tallet der håndtaket hører hjemme, og kallet opererer på feil objekt uten å utløse en feil, som er den verste sorten tabbe å feilsøke. Resten av verktøykassen for direkte tilgang er lagt ut i sammenslåing, oppdeling og direkte tilgang for store PDF-er

Finnes det én vane som skiller uttrekkskode som overlever et virkelig korpus, fra kode som halter, er det å behandle siden som upålitelig inndata framfor en ren datakilde. Tekst som ikke stemmer med det leseren gjengir, er nesten alltid et kodingsproblem, en ligatur som kollapser til én glyff eller en delmengdeskrift som mangler ToUnicode-oppføringene sine, og løsningen er å måle konfidens og lede de dårlige sidene til OCR, ikke å slåss mot bytene. Skrift-API-et vil aldri produsere en TTF eller OTF, av design, så bygg skriftarbeidsflyter rundt granskingsspørsmål. Og den vedvarende uttrekkstilstanden, områderektangelet ikke minst, er en innstilling du eier gjennom hele levetiden til et dokumenthåndtak, ikke en parameter du glemmer etter ett kall. Får du de tre refleksene riktig, oppfører resten av API-et seg

Evalueringsbygg, demoprosjekter og den fullstendige API-referansen for uttrekk finnes på produktsiden for losLab PDF Library for Delphi