Teknisk artikel

Extrahera text från en inläst PDF i Delphi med HotPDF

HotPDF Component extraherar Unicode-text från valfri PDF du läser in i Delphi genom två anrop: ExtractLoadedPageText returnerar läsflödestexten på en sida, och ExtractLoadedPageTextLayout (tillagd i v2.263.0) rekonstruerar sidans visuella layout som vanlig text, så att kolumner, indrag och tabelljustering överlever i utdata. Båda fungerar på dokument som HotPDF inte har skapat, vilket är det fall som faktiskt spelar roll: fakturan som en kund e-postade till dig, rapporten som en skanningsbyrå levererade, kontraktet som genererats av en programvara som ingen längre kan namnge

Att nå dit krävde mer maskineri än vad de två signaturerna antyder, eftersom en PDF-fil inte lagrar text på samma sätt som en textfil gör. Den här artikeln går igenom båda extraheringslägena och öppnar sedan motorhuven för de tre underliggande delarna — CMap-läsaren, tolkaren för innehållsströmmen och reservkedjan för avkodning av typsnitt — eftersom att förstå hur mappningen fungerar är skillnaden mellan att rycka på axlarna åt skräputdata och och diagnosticera det

Varför är textextrahering svårare än att läsa strängar ur filen?

En PDF-innehållsström registrerar teckenkoder, inte tecken. Operatorerna Tj och TJ (ISO 32000-1 §9.4.3) bär på strängar av byte vars betydelse helt beror på vilket typsnitt som valts av föregående Tf: byte 0x41 kan vara bokstaven A under WinAnsi, en godtycklig glyf i ett delmängdstypsnitt eller hälften av en två-byte CID i ett sammansatt CJK-typsnitt. ISO 32000-1 §9.10 definierar textextrahering som exakt detta avkodningsproblem — att mappa varje kod tillbaka till Unicode med hjälp av den information som typsnittsordboken tillhandahåller — och standarden är tydlig med att en kompatibel fil inte krävs tillhandahålla tillräckligt med information för att göra det

Den sista klausulen förklarar varje felrapport av typen ”varför producerar kopiera-klistra in från den här PDF-filen rappakalja” som du någonsin har sett. En producent som bäddar in ett delmängdstypsnitt utan /ToUnicode-tabell har skapat en fil som renderas perfekt men extraheras som nonsens, eftersom mappningen från kod till glyf finns men mappningen från kod till Unicode skickades aldrig med. Alla ärliga extraherings-API:er är därför en bäst-möjliga-kedja av reservrutiner (fallbacks), och den relevanta frågan är hur djupt kedjan går

Läsflödesextrahering med ExtractLoadedPageText

För sökindexering, sökordsmatchning eller för att mata text till en analyspipeline är ExtractLoadedPageText anropet du vill ha. Signaturen är function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean — sidindex är nollbaserade, resultatet returneras som en inbyggd Delphi-UnicodeString, och funktionen returnerar False när sidan inte har någon läsbar innehållsström istället för att kasta ett undantag

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 innehåller nu dokumentets läsflödestext
  finally
    Pdf.Free;
  end;
end;

När en glyfs vertikala ursprung flyttas med mer än hälften av den aktuella typsnittsstorleken — signaturen för ett Td- eller T*-steg i innehållsströmmen — infogas en radbrytning. Tecken som avkodaren inte kan lösa upp blir till mellanslag istället för att försvinna, så ordgränser överlever även när enskilda glyfer inte gör det. Vad detta läge inte försöker göra är klustring av läsordning eller detektering av flera kolumner: en tvåspaltig sida kommer ut varvad i innehållsströmmens ordning, vilket vanligtvis men inte alltid är den visuella ordningen

När ska du använda layoutbevarande extrahering istället?

ExtractLoadedPageTextLayout är rätt anrop när positionen har betydelse: tabeller, formulär, kodlistor, allt du tänker diffa, greppa eller tolka per kolumn. Istället för att platta till glyfer till en ström, klustrar den dem i baslinjer, sorterar varje baslinje efter X och återskapar horisontellt och vertikellt tomrum på ett teckenrutnät med fast teckenbredd (monospaced) dimensionerat efter medianvärdet för glyfframmatning och typsnittsstorlek. Stora luckor mellan körningar på samma baslinje blir till mellanslag; stora luckor mellan baslinjer blir tomma rader. Resultatet ser ut som sidan gör

var
  Grid: UnicodeString;
begin
  if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
    TFile.WriteAllText('page1.txt', Grid, TEncoding.UTF8);
  // Kolumner, indrag och tabelljustering överlever som
  // mellanslag och tomma rader på ett teckenrutnät
end;

De två lägena delar varje byte av avkodningsmaskineriet och skiljer sig endast i hur de ordnar de avkodade glyferna, så valet kostar ingenting i trohet. Välj ExtractLoadedPageText när endast orden spelar roll och ExtractLoadedPageTextLayout när arrangemanget gör det. Läsordningsdetektering för flera kolumner ligger utanför ramarna för båda — en rutnätsrendering av en tvåspaltig sida visar dig båda kolumnerna sida vid sida, troget, vilket för diffning är helt rätt men för prosatexters läsflöde inte är det

Hur avkodar HotPDF teckenkoder till Unicode?

HotPDF Component löser varje teckenkod genom en prioriterad reservkedja: typsnittets inbäddade /ToUnicode-CMap först, sedan /Encoding-posten (ström eller namngiven CMap), sedan — för sammansatta typsnitt — Adobes standard-CMap-filer för teckensamlingar som Adobe-GB1, Adobe-CNS1, Adobe-Japan1 och Adobe-KR, och slutligen de inbyggda WinAnsi- och MacRoman-tabellerna för enkla typsnitt. En strategi som inte kan leverera ett svar trappas tyst ner till nästa istället för att utlösa fel, och en kod som tömmer hela kedjan löses upp till 0 så att anroparen kan räkna missar istället för att gissa

/ToUnicode CMap (ISO 32000-1 §9.10.3) ligger först eftersom det är den mappning som producenten skrev specifikt för extrahering. Adobes standard-CMap-sökväg är viktig för CJK-dokument som använder fördefinierade CMaps som UniGB-UTF16-H instead of embedding anything: HotPDF levererar samlingsfilerna under sin resources\CMap-katalog, letar upp dem relativt programfilen vid körning och cachelagrar varje tolkad mappning per process — värt att veta eftersom den största av dem, den Adobe-GB1-mappningen, är ungefär 2 MB källtext som du inte vill tolka om per sida. Om katalogen saknas hoppar avkodaren helt enkelt över de diskbaserade CMaps och arbetar med inbäddade tabeller samt de inbyggda kodningarna. Detta är lässidesmotsvarigheten till formningsproblemet som behandlas i komplex textformning med HotPDF, där samma distinktion mellan kod och glyf möts vid skrivning

Två syntaxfällor i CMap värda att känna till

CMap-filer ser ut att vara triviala att tolka men är det inte, och två detaljer förklarar de flesta misslyckanden vid första tolkningsförsöket. Det första är att postantalet kommer före sektionens nyckelord: en sektion lyder 2 beginbfchar, inte beginbfchar 2. En tolkare som förväntar sig antalet efter nyckelordet konsumerar numret som en vilsekommen symbol och hittar sedan noll poster i varje sektion. Det robusta tillvägagångssättet — det som HotPDF-läsaren fastnade för — är att helt ignorera antalet och loopa tills det matchande nyckelordet endbfchar / endbfrange hittas, vilket har fördelen att tolerera verkliga filer vars antal helt enkelt är felaktiga

Den andra fällan är att målen för bfchar och bfrange är UTF-16BE-strängar, inte heltal. Destinationen <D83DDE00> betyder U+1F600 — ett surrogatpar som måste kombineras till en kodpunkt — och att läsa dessa fyra byte som ett big-endian-heltal ger ett meningslöst värde för varje kodpunkt utanför Basic Multilingual Plane. Emoji i PDF-filer är inte längre exotiska, så en avkodare som hoppar över surrogatrekombination misslyckas på filer som dina användare faktiskt har. HotPDF tolkar hexadecimalen till råa byte först och kombinerar sedan UTF-16BE-kodenheter, vilket också täcker de mål med flera tecken som ligaturmappningar producerar

Gå ner till glyfnivå med ExtractLoadedPageGlyphs

Båda textanropen bygger på ExtractLoadedPageGlyphs, och den underliggande THPDFGlyphArray är tillgänglig för din kod också. Varje THPDFGlyphRecord bär på den matchade Unicode-kodpunkten tillsammans med den råa teckenkoden, kodens bytebredd (1, 2 eller 4, bestämd av CMap:s codespacerange), den aktiva typsnittsresursnyckeln och storleken, användarrymdens X- och Y-ursprung samt den horisontella frammatningen. Det räcker för att bygga ordgränsdetektering, positionerad markering eller en anpassad layoutalgoritm utan att behöva röra innehållsströmmen själv

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;

Att räkna Unicode = 0-poster, enligt ovan, är det ärliga sättet att mäta extraheringskvalitet på ett givet dokument innan du litar på texten i nästa steg. Glyfposterna förankrar också varje tecken till källoperanden i innehållsströmmen, vilket är det som gör HotPDF:s text sök och ersätt i inlästa dokument möjlig ovanpå samma grund

Vilka PDF-filer kommer inte att lämna ifrån sig sin text?

Vissa filer besegrar alla extraherare, och det är bättre att upptäcka dem än att skicka vidare deras utdata. Skannade dokument är det enklaste fallet: en sida som är en enda stor bild innehåller inga textoperatorer alls, så extraheringen returnerar korrekt en tom sträng — lösningen är OCR, och att extrahera bilderna från den inlästa PDF-filen är det första steget i den pipelinen. Delmängdstypsnitt utan en /ToUnicode-tabell är det svårare fallet: om även /Encoding-sökvägen och standard-CMaps är tomma, matchas dessa glyfer till 0 och visas som mellanslag i textanropen. Krypterade dokument extraheras normalt förutsatt att du läser in dem med deras lösenord via överlagringen av LoadFromFile, så att strömmarna dekomprimeras innan tolkaren ser dem

En snävare gräns är värd att uttalas tydligt: avkodningskedjan läser CMap och innehållsströmmar genom HotPDF:s Flate-sökväg, så ett typsnitt vars ToUnicode-ström använder ett ovanligt filter trappas ner till nästa strategi istället för att misslyckas på sidan. I praktiken FlateDecode täcker nästan allt som har producerats under de senaste två decennierna, och nedtrappningen sker tyst avsiktligt — du får den bästa text som filen tillåter istället för ett undantag. Samma objektsmaskineri på lässidan som löser upp typsnittsordböcker här driver också redigering av metadata i inlästa dokument, så att en pipeline för dokumentmottagning kan extrahera, inspektera och kommentera i ett enda steg

Textextrahering, layoutbevarande rendering, tillgång på glyfnivå samt sök- och ersättningsfunktionerna som bygger på dem är alla en del av standarden HotPDF Component för Delphi och C++Builder — inga externa DLL:er, inga operativsystemstjänster för text, bara Object Pascal du kan stega igenom när en konstig fil hamnar i din kö