Teknisk artikel

Tekst-, billed- og skrifttypeudtrækning fra PDF i Delphi med PDF Library for Delphi

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

Diagram over Delphi PDF-tekstudtrækningsniveauer: GetPageText options 0 til 8 ruter til et letvægtsforløb eller den layoutbevidste motor, og sider, hvis subsettede skrifttyper mangler et ToUnicode CMap, falder tilbage til OCR
GetPageText-tilvalgsværdier 0 til 8 vælger mellem et letvægts preview-forløb og den layoutbevidste motor, hvor tilvalg 7 er reserveret til søgeindeksering, og manglende ToUnicode CMaps omdirigeres til OCR

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

Diagram, der viser dokumentniveau PDF-udtrækningstilstand i Delphi vedvarende på tværs af kald på ét handle indtil reset, hvilket forhindrer lydløs afkortning af senere udtrækninger
Ekstraktionsområde, ordmellemrum og tilvalgsindstillinger vedvarer på dokumenthandleet, så et område afgrænset til én funktion i stilhed afkorter hver senere ekstraktion, til tilstanden nulstilles eller handleet byttes ud

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

Sammenligning af at rendere en Delphi PDF-side for at fange billeder versus at udtrække originale billedstrømme med GetPageImageList, inklusive softmaske-, duplikat-XObject- og CMYK-forbehold
Gengivelse og beskæring genresampler pixels og kasserer de originale billeddata, mens GetPageImageList optæller de gemte billedressourcer med deres egenskaber og uforstyrrede streams

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