HotPDF Component extraheert Unicode-tekst uit elke PDF die u in Delphi laadt via twee aanroepen: ExtractLoadedPageText retourneert de leesstroomtekst van een pagina, en ExtractLoadedPageTextLayout (toegevoegd in v2.263.0) reconstrueert de visuele indeling van de pagina als platte tekst, zodat kolommen, inspringing en tabeluitlijning behouden blijven in de uitvoer. Beide werken op documenten die niet door HotPDF zijn gemaakt, wat het geval is dat er echt toe doet: de factuur die een klant u heeft gemaild, het rapport dat een scanbureau heeft geleverd, het contract dat is gegenereerd door software waarvan niemand de naam meer weet
Om dit te bereiken was er meer functionaliteit nodig dan de twee methoden doen vermoeden, omdat een PDF tekst niet op dezelfde manier opslaat als een tekstbestand. Dit artikel behandelt beide extractiemodi en kijkt vervolgens onder de motorkap naar de drie onderliggende onderdelen — de CMap-lezer, de inhoudsstream-interpreter en de fallback-keten voor lettertype-decodering — want weten hoe de mapping werkt is het verschil tussen uw schouders ophalen bij onbruikbare uitvoer en het diagnosticeren ervan
Waarom is tekstextractie moeilijker dan het lezen van strings uit het bestand?
Een PDF-inhoudsstream registreert tekencodes, geen tekens. De Tj- en TJ-operatoren (ISO 32000-1 §9.4.3) bevatten reeksen bytes waarvan de betekenis volledig afhangt van het lettertype dat is geselecteerd door de voorafgaande Tf: byte 0x41 kan de letter A zijn onder WinAnsi, een willekeurige glyph in een subset-lettertype, of de helft van een twee-byte CID in een samengesteld CJK-lettertype. ISO 32000-1 §9.10 definieert tekstextractie exact als dit decodervraagstuk — het terugkoppelen van elke code naar Unicode met behulp van alle informatie die het lettertypewoordenboek biedt — en de standaard stelt expliciet dat een conform bestand niet verplicht is om voldoende informatie te verstrekken om dit te kunnen doen
Die laatste bepaling verklaart elk bugrapport van het type "waarom produceert knippen en plakken uit deze PDF wartaal" dat u ooit hebt gezien. Een maker die een subset-lettertype insluit zonder /ToUnicode-tabel heeft een bestand geschreven dat perfect rendert maar als onzin wordt geëxtraheerd, omdat de code-naar-glyph-koppeling bestaat maar de code-naar-Unicode-koppeling nooit is meegeleverd. Elke eerlijke extractie-API is daarom een fallback-keten die werkt op basis van best-effort, en de nuttige vraag is hoe diep die keten gaat
Extractie van de leesstroom met ExtractLoadedPageText
Voor zoekindexering, trefwoordkoppeling of het invoeren van tekst in een analysepijplijn is ExtractLoadedPageText de aanroep die u zoekt. De signatuur is function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean — pagina-indexen zijn nul-gebaseerd, het resultaat komt binnen als een native Delphi UnicodeString, en de functie retourneert False in plaats van een uitzondering te genereren wanneer de pagina geen leesbare inhoudsstream heeft
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;
Regelafbrekingen in de uitvoer zijn afkomstig van een bewust eenvoudige heuristiek: wanneer de verticale oorsprong van een glyph met meer dan de helft van de huidige lettergrootte verschuift — het kenmerk van een Td- of T*-stap in de inhoudsstream — wordt een nieuwe regel ingevoegd. Tekens die de decoder niet kan herleiden, worden spaties in plaats van te verdwijnen, zodat woordgrenzen behouden blijven, zelfs als individuele glyphs dat niet doen. Wat deze modus niet probeert, is clustering van de leesvolgorde of kolomdetectie: een pagina met twee kolommen komt er interleaved uit in de volgorde van de inhoudsstream, wat meestal maar niet altijd de visuele volgorde is
Wanneer moet u in plaats daarvan lay-outbehoudende extractie gebruiken?
ExtractLoadedPageTextLayout is de juiste aanroep wanneer de positie betekenis heeft: tabellen, formulieren, codelijsten, alles wat u wilt diffen, greppen of per kolom wilt parsen. In plaats van glyphs af te platten tot een stroom, clustert het ze in basislijnen, sorteert het elke basislijn op X en reproduceert het horizontale en verticale witruimte op een monospaced raster van tekens dat is gedimensioneerd op basis van de gemiddelde glyph-breedte en lettergrootte. Grote gaten tussen tekstblokken op dezelfde basislijn worden reeksen spaties; grote gaten tussen basislijnen worden lege regels. Het resultaat leest zoals de pagina eruitziet
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 twee modi delen elke byte van de decoderingsfunctionaliteit en verschillen alleen in hoe ze de gedecodeerde glyphs rangschikken, dus de keuze kost niets qua getrouwheid. Kies ExtractLoadedPageText wanneer alleen de woorden belangrijk zijn en ExtractLoadedPageTextLayout wanneer de indeling dat is. Detectie van leesvolgorde met meerdere kolommen blijft buiten het bereik van beide — een rasterweergave van een pagina met twee kolommen toont u beide kolommen getrouw naast elkaar, wat voor diffen precies goed is en voor het opnieuw laten stromen van proza niet
Hoe decodeert HotPDF tekencodes naar Unicode?
HotPDF Component herleidt elke tekencode via een prioriteit-gestuurde fallback-keten: eerst de ingesloten /ToUnicode CMap van het lettertype, dan de vermelding /Encoding (stream of benoemde CMap), vervolgens — voor samengestelde lettertypen — de Adobe-standaard CMap-bestanden voor tekencollecties zoals Adobe-GB1, Adobe-CNS1, Adobe-Japan1 en Adobe-KR, en tot slot de ingebouwde WinAnsi- en MacRoman-tabellen voor eenvoudige lettertypen. Een strategie die geen antwoord kan leveren, valt terug op de volgende in plaats van een uitzondering te genereren, en een code die de hele keten uitput, wordt herleid tot 0 zodat de aanroeper missers kan tellen in plaats van te gokken
De /ToUnicode CMap (ISO 32000-1 §9.10.3) staat op de eerste plaats omdat dit de mapping is die de maker specifiek voor extractie heeft geschreven. Het Adobe-standaard CMap-pad is belangrijk voor CJK-documenten die vooraf gedefinieerde CMaps zoals UniGB-UTF16-H gebruiken in plaats van iets in te sluiten: HotPDF levert de verzamelingsbestanden mee onder de map resources\CMap, zoekt ze tijdens runtime op relatief ten opzichte van het uitvoerbare bestand en cachet elke geparste map per proces — handig om te weten, want de grootste daarvan, de Adobe-GB1-map, is ongeveer 2 MB aan brontekst die u niet per pagina opnieuw wilt parsen. Als de directory ontbreekt, slaat de decoder simpelweg de van schijf gelezen CMaps over en werkt hij met ingebedde tabellen plus the ingebouwde coderingen. Dit is de spiegelbeeldige leeszijde van het vormgevingsprobleem dat wordt behandeld in complexe schrifttekstvormgeving met HotPDF, waar tijdens het schrijven met hetzelfde onderscheid tussen code en glyph wordt omgegaan
Twee syntaxvalkuilen van CMap die het waard zijn om te weten
CMap-bestanden lijken triviaal te parsen maar zijn dat niet, en twee details verklaren de meeste mislukkingen bij een eerste parserpoging. Het eerste is dat het aantal records *voor* het sectiesleutelwoord komt: een sectie luidt 2 beginbfchar, niet beginbfchar 2. Een parser die het aantal na het sleutelwoord verwacht, consumeert het getal als een verdwaald token en vindt vervolgens nul vermeldingen in elke sectie. De robuuste benadering — die de lezer van HotPDF hanteert — is om de telling volledig te negeren en te herhalen tot het overeenkomstige endbfchar / endbfrange sleutelwoord, wat als bijkomend voordeel heeft dat bestanden uit de praktijk waarvan de tellingen simpelweg onjuist zijn, worden getolereerd
Afdalen naar glyph-niveau met ExtractLoadedPageGlyphs
Beide tekstaanroepen zijn gebouwd op ExtractLoadedPageGlyphs, en de onderliggende THPDFGlyphArray is ook beschikbaar voor uw code. Elk THPDFGlyphRecord bevat het herleide Unicode-codepunt naast de ruwe tekencode, de byte-breedte van de code (1, 2 of 4, bepaald door de codespacerange van de CMap), de actieve lettertypebronsleutel en -grootte, de X- en Y-oorsprong in de gebruikersruimte en de horizontale voorsprong (advance). Dat is voldoende om woordgrensdetectie, gepositioneerde markering of een aangepast lay-outalgoritme te bouwen zonder zelf de inhoudsstream aan te raken
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;
Het tellen van Unicode = 0 records, zoals hierboven, is de eerlijke manier om de extractiekwaliteit op een bepaald document te meten voordat u de tekst stroomafwaarts vertrouwt. De glyph-records verankeren elk karakter ook aan de bron-operand in de inhoudsstream, wat de tekstzoek- en vervangfuncties van HotPDF op geladen documenten mogelijk maakt bovenop datzelfde fundament
Welke PDF's geven hun tekst niet vrij?
Sommige bestanden verslaan elke extractor, en het is beter om ze te detecteren dan om hun uitvoer te verzenden. Gescande documenten zijn het duidelijkste geval: een pagina die één grote afbeelding is, bevat helemaal geen tekstopmerkingen, dus extractie retourneert correct een lege string — de oplossing is OCR, en het extraheren van de pagina-afbeeldingen uit de geladen PDF is de eerste stap van die pijplijn. Subset-lettertypen zonder /ToUnicode-tabel zijn het moeilijkere geval: als het /Encoding-pad en de standaard CMaps ook leeg blijven, worden die glyphs herleid tot 0 en verschijnen ze als spaties in de tekstaanroepen. Gecodeerde documenten worden normaal geëxtraheerd, mits u ze laadt met hun wachtwoord via de overload van LoadFromFile, zodat de streams worden gedecodeerd voordat de interpreter ze ooit ziet
Eén nauwere grens moet duidelijk worden aangegeven: de decoderingsketen leest CMap- en inhoudsstreams via het Flate-pad van HotPDF, dus een lettertype waarvan de ToUnicode-stream een ongebruikelijk filter gebruikt, valt terug op de volgende strategie in plaats van dat de pagina faalt. In de praktijk dekt FlateDecode bijna alles wat in de afgelopen twee decennia is geproduceerd, en de terugval is stil uit hoofde van het ontwerp — u krijgt de beste tekst die het bestand toestaat in plaats van een uitzondering. Dezelfde objectfunctionaliteit aan de leeszijde die hier lettertypewoordenboeken oplost, drijft ook het bewerken van metagegevens op geladen documenten aan, zodat een documentinnamepijplijn in één werkgang kan extraheren, inspecteren en annoteren
Tekstextractie, lay-outbehoudende weergave, toegang op glyph-niveau en de daarop gebouwde functies voor zoeken en vervangen maken allemaal deel uit van de standaard HotPDF Component voor Delphi en C++Builder — geen externe DLL's, no OS-tekstservices, gewoon Object Pascal waar u doorheen kunt stappen als er een vreemd bestand in uw wachtrij belandt