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ö