Teknisk artikel

Udtræk tekst fra en indlæst PDF i Delphi med HotPDF

HotPDF Component udtrækker Unicode-tekst fra enhver PDF, du indlæser i Delphi, via to kald: ExtractLoadedPageText returnerer læseflow-teksten på en side, og ExtractLoadedPageTextLayout (tilføjet i v2.263.0) rekonstruerer sidens visuelle layout som almindelig tekst, så kolonner, indrykning og tabeljustering bevares i outputtet. Begge fungerer på dokumenter, som HotPDF ikke har oprettet, hvilket er det tilfælde, der reelt betyder noget: Fakturaen en kunde sendte til dig, rapporten et scanningsbureau leverede, eller kontrakten genereret af software, som ingen længere kan huske navnet på

At nå dertil krævede mere maskineri, end de to signaturer antyder, fordi en PDF ikke gemmer tekst på samme måde som en tekstfil. Denne artikel gennemgår begge udtrækningsmetoder og kigger derefter under motorhjelmen på de tre underliggende dele — CMap-læseren, indholdsstrøm-fortolkeren og fallback-kæden til skrifttypeafkodning — for at vide, hvordan mappingen fungerer, er forskellen på at trække på skuldrene over ubrugeligt output eller at diagnosticere det

Hvorfor er tekstudtrækning sværere end at læse strenge direkte ud af filen?

En PDF-indholdsstrøm registrerer tegnkoder, ikke tegn. Operatorerne Tj og TJ (ISO 32000-1 §9.4.3) indeholder byte-strenge, hvis betydning afhænger helt af den skrifttype, der er valgt af den forudgående Tf: Byte 0x41 kan være bogstavet A under WinAnsi, et vilkårligt tegn (glyph) i en delmængdeskrifttype, eller halvdelen af en to-byte CID i a sammensat CJK-skrifttype. ISO 32000-1 §9.10 definerer tekstudtrækning som netop dette afkodningsproblem — at mappe hver kode tilbage til Unicode ved hjælp af de oplysninger, som skrifttypeordbogen leverer — og standarden fastslår eksplicit, at en overensstemmende fil ikke er forpligtet til at levere tilstrækkelige oplysninger til at gøre det

Denne sidste bestemmelse forklarer enhver fejlrapport af typen "hvorfor producerer kopiering og indsættelse fra denne PDF vrøvl", som du nogensinde har set. En producent, der integrerer en delmængdeskrifttype uden en /ToUnicode-tabel, har skrevet en fil, der renderer perfekt, men udtrækkes som sludder, fordi koden-til-tegn-mappingen eksisterer, men koden-til-Unicode-mappingen aldrig blev leveret. Ethvert ærligt udtræknings-API is derfor en fallback-kæde baseret på bedste evne, og det relevante spørgsmål er, hvor dyb denne kæde er

Læseflow-udtrækning med ExtractLoadedPageText

Til søgeindeksering, søgeordsmatchning eller levering af tekst til en analysepipeline er ExtractLoadedPageText det kald, du skal bruge. Signaturen er function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean — sideindeks er nulbaserede, resultatet leveres som en indfødt Delphi UnicodeString, og funktionen returnerer False i stedet for at udløse en fejl, når siden ikke har en læsbar indholdsstrøm

var
  Pdf: THotPDF;
  PageCount, I: Integer;
  PageText, AllText: UnicodeString;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('invoice.pdf');
    AllText := '';
    for I := 0 to PageCount - 1 do
      if Pdf.ExtractLoadedPageText(I, PageText) then
        AllText := AllText + PageText + #13#10;
    // AllText now holds the reading-flow text of the document
  finally
    Pdf.Free;
  end;
end;

Linjeskift i outputtet stammer fra en bevidst simpel heuristik: Når et tegns lodrette udgangspunkt flytter sig med mere end det halve af den aktuelle skrifttypestørrelse — hvilket er signaturen for et Td- eller T*-trin i indholdsstrømmen — indsættes et linjeskift. Tegn, som afkoderen ikke kan løse, bliver til mellemrum i stedet for at forsvinde, så ordgrænser overlever, selv når enkelte tegn (glyphs) ikke gør. Hvad denne tilstand ikke forsøger, er gruppering efter læserækkefølge eller detektering af flere kolonner: En to-spaltet side udlæses flettet i indholdsstrømmens rækkefølge, hvilket normalt, men ikke altid, er den visuelle rækkefølge

Hvornår skal man bruge layoutbevarende udtrækning i stedet?

ExtractLoadedPageTextLayout er det rigtige kald, hver gang placering har en betydning: Tabeller, formularer, kodelister, alt hvad du har til hensigt at sammenligne (diff), søge i (grep) eller fortolke efter kolonner. I stedet for at flade tegn ud i en strøm, grupperer den dem i bundlinjer (baselines), sorterer hver bundlinje efter X og gengiver vandret og lodret hvidt rum på et fastbredde-tegnnet baseret på den gennemsnitlige tegnfremrykning og skrifttypestørrelse. Brede huller mellem tekststykker på samme bundlinje bliver til serier af mellemrum; store huller mellem bundlinjer bliver til tomme linjer. Resultatet kan læses på samme måde, som siden ser ud

var
  Grid: UnicodeString;
begin
  if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
    TFile.WriteAllText('page1.txt', Grid, TEncoding.UTF8);
  // Columns, indentation and table alignment survive as
  // spaces and blank lines on a character grid
end;

De to tilstande deler hver eneste byte af afkodningsmaskineriet og adskiller sig kun i, hvordan de arrangerer de afkodede tegn, så valget koster intet på nøjagtigheden. Vælg ExtractLoadedPageText, når kun ordene betyder noget, og ExtractLoadedPageTextLayout, når opsætningen gør. Detektering af læserækkefølge for flere kolonner er fortsat uden for rammerne for begge — en netgengivelse af en tospaltet side viser dig trofast begge kolonner side om side, hvilket til sammenligning (diffing) er helt rigtigt, men til tekstombrydning ikke er det

Hvordan afkoder HotPDF tegnkoder til Unicode?

HotPDF Component løser hver tegnkode via en prioriteret fallback-kæde: Skrifttypens integrerede /ToUnicode CMap først, derefter /Encoding-posten (strøm eller navngiven CMap), derefter — for sammensatte skrifttyper — Adobes standard CMap-filer til tegnbunt-samlinger som f.eks. Adobe-GB1, Adobe-CNS1, Adobe-Japan1 og Adobe-KR, og endelig de indbyggede WinAnsi- og MacRoman-tabeller for simple skrifttyper. En strategi, der ikke kan levere et svar, nedgraderes stiltiende til den næste frem for at udløse en fejl, og en kode, der udtømmer hele kæden, løses til 0, så kalderen kan tælle mangler i stedet for at gætte

/ToUnicode CMappen (ISO 32000-1 §9.10.3) ligger først, fordi det er den mapping, producenten skrev specifikt til udtrækning. Adobes standard CMap-sti er vigtig for CJK-dokumenter, der bruger foruddefinerede CMaps som f.eks. UniGB-UTF16-H i stedet for at integrere noget: HotPDF leverer samlingsfilerne under sin resources\CMap-mappe, finder dem i forhold til den eksekverbare fil ved kørselstid og cacher hvert fortolket kort pr. proces — værd at vide, fordi det største af dem, Adobe-GB1-kortet, er ca. 2 MB kilde-tekst, som du ikke ønsker at fortolke igen pr. side. Hvis mappen mangler, springer afkoderen simpelthen disk-baserede CMaps over og arbejder med integrerede tabeller samt de indbyggede kodninger. Dette er læsesidens spejlbillede af formningsproblemet (shaping), der dækkes i formning af tekst til komplekse skrifter med HotPDF, hvor den samme forskel på kode versus tegn opstår ved skrivning

To syntaksfælder i CMap, der er værd at kende

CMap-filer ser ud til at være nemme at fortolke, men er det ikke, og to detaljer tegner sig for de fleste mislykkede forsøg på at bygge en fortolker. Den første er, at antallet af poster kommer før sektionsnøgleordet: En sektion lyder 2 beginbfchar, ikke beginbfchar 2. En fortolker, der forventer antallet efter nøgleordet, spiser tallet som et vildfarent token og finder derefter nul poster i hver sektion. Den robuste tilgang — den som HotPDF's læser valgte — er at ignorere antallet fuldstændigt og køre i løkke indtil det matchende nøgleord endbfchar / endbfrange, hvilket har den fordel, at det accepterer virkelige filer, hvor antallet simpelthen er forkert

Den anden fælde er, at bfchar- og bfrange-mål er UTF-16BE-strenge, ikke heltal. Destinationen <D83DDE00> betyder U+1F600 — et surrogatpar, der skal rekombineres til ét kodepunkt — og at læse disse fire bytes som et big-endian heltal giver en meningsløs værdi for ethvert kodepunkt uden for Basic Multilingual Plane. Emojier i PDF-filer er ikke længere eksotiske, så en afkoder, der springer surrogatrekombination over, fejler på filer, som dine brugere rent faktisk har. HotPDF fortolker hex-værdien til rå bytes først og rekombinerer derefter UTF-16BE-kodeenheder, hvilket også dækker de mål med flere tegn, som ligatur-mappings producerer

Nedadstigning to tegnniveau med ExtractLoadedPageGlyphs

Begge tekstkald er bygget på ExtractLoadedPageGlyphs, og det underliggende THPDFGlyphArray er også tilgængeligt for din kode. Hver THPDFGlyphRecord indeholder det fundne Unicode-kodepunkt sammen med den rå tegnkode, kodens bytebredde (1, 2 eller 4, bestemt af CMap-kørslens codespacerange), den aktive skrifttyperessource-nøgle og størrelse, X- og Y-udgangspunktet i bruger-rummet samt den vandrette fremrykning. Det er nok til at bygge ordgrænsedetektering, placeret fremhævning eller en brugerdefineret layoutalgoritme uden selv at skulle røre ved indholdsstrømmen

var
  Glyphs: THPDFGlyphArray;
  I, Unresolved: Integer;
begin
  if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
  begin
    Unresolved := 0;
    for I := 0 to High(Glyphs) do
      if Glyphs[I].Unicode = 0 then
        Inc(Unresolved);
    if Unresolved > 0 then
      ShowMessageFmt('%d of %d glyphs have no Unicode mapping',
        [Unresolved, Length(Glyphs)]);
  end;
end;

At tælle Unicode = 0-poster, som ovenfor, er den ærlige måde at måle udtrækningskvaliteten på et givet dokument på, før du stoler på teksten i efterfølgende led. Tegnposterne forankrer også hvert tegn til kildeoperanden i indholdsstrømmen, hvilket er det, der gør HotPDFs tekstsøgning og -erstatning i indlæste dokumenter mulig på det samme fundament

Hvilke PDF-filer vil ikke afgive deres tekst?

Nogle filer overvinder enhver extractor, og det er bedre at detektere dem end at levere deres output. Scannede dokumenter er det klareste tilfælde: En side, der er ét stort billede, indeholder slet ingen tekstoperatorer, så udtrækning returnerer korrekt en tom streng — løsningen er OCR, og udtrækning af sidebillederne fra den indlæste PDF er det første trin i den pipeline. Delmængdeskrifttyper uden en /ToUnicode-tabel er det sværere tilfælde: Hvis /Encoding-stien og standard-CMaps også er tomme, løses disse tegn til 0 og fremstår som mellemrum i tekstkaldene. Krypterede dokumenter udtrækkes normalt, forudsat at du indlæser dem med deres adgangskode via LoadFromFile-overloaden, så strømmene dekrypteres, før fortolkeren overhovedet ser dem

En snævrere grænse er værd at nævne klart: Afkodningskæden læser CMap- og indholdsstrømme via HotPDFs Flate-sti, så en skrifttype, hvis ToUnicode-strøm bruger et usædvanligt filter, nedgraderes til den næste strategi i stedet for at lade siden fejle. I praksis dækker FlateDecode næsten alt, hvad der er produceret i de sidste to årtier, og nedgraderingen sker stiltiende med vilje — du får den bedste tekst, som filen tillader, i stedet for en undtagelse. Det samme objektmaskineri på læsesiden, som løser skrifttypeordbøger her, understøtter også redigering af metadata i indlæste dokumenter, så en pipeline til dokumentmodtagelse kan udtrække, inspicere og annotere i én arbejdsgang

Tekstudtrækning, layoutbevarende rendering, adgang på tegnniveau samt de søge- og erstatningsfunktioner, der er bygget på dem, er alle en del af standarden HotPDF Component til Delphi og C++Builder — ingen eksterne DLL-filer, geen OS-teksttjenester, blot Object Pascal du kan gennemgå, når en ukendt fil lander i din kø