HotPDF Delphi 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 bevat nu de tekst van het document in leesvolgorde
finally
Pdf.Free;
end;
end;
Regelafbrekingen in de uitvoer komen van een bewust eenvoudige geometrische regel. Sinds HotPDF v2.766.79 wordt een nieuwe regel ingevoegd wanneer de glyph-oorsprong over de schrijfrichting heen beweegt met meer dan de helft van de teksthoogte, genomen als de grootste van de ascent-tot-descent-kaderhoogte van deze glyph en de vorige, in pagina-eenheden. Vóór die release werd de verticale verplaatsing vergeleken met de helft van de lettergrootte die aan Tf was meegegeven, wat vaak 1 is in documenten die tekst op maat zetten via de text-matrix, dus een verhoogde superscript begon een nieuwe regel en gedraaide tekst zette elk teken op een eigen regel. Woordspaties volgen sinds v2.766.76 een bijpassende regel: waar een producer woorden scheidt door de tekstpositie te verschuiven, met een TJ-aanpassing of een Td, in plaats van een spatie-teken te tonen, wordt een spatie toegevoegd wanneer de opening langs de basislijn na de eigen breedte van de vorige glyph meer dan 0.15 van de kaderhoogte van de glyph bedraagt, en nooit tussen twee Chinese, Japanse of Koreaanse tekens. 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);
// Kolommen, inspringing en tabeluitlijning blijven behouden als
// spaties en lege regels op een tekenraster
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 Delphi 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
De tweede valkuil is dat de doelwaarden van bfchar en bfrange UTF-16BE-strings zijn, geen gehele getallen. De bestemming <D83DDE00> betekent U+1F600 — een surrogaatpaar dat tot één codepunt moet worden samengevoegd — en die vier bytes lezen als een big-endian geheel getal levert een betekenisloze waarde op voor elk codepunt buiten het Basic Multilingual Plane. Emoji in PDF’s zijn allang niet exotisch meer, dus een decoder die het samenvoegen van surrogaten overslaat, gaat onderuit op bestanden die uw gebruikers daadwerkelijk hebben. HotPDF parseert de hex-literal eerst naar ruwe bytes en voegt daarna de UTF-16BE code units samen, wat meteen ook de doelwaarden met meerdere tekens dekt die ligatuurmappings opleveren
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); sinds v2.766.76 bevat hij ook GlyphEndX en GlyphEndY, het punt in paginaruimte waar de eigen breedte van de glyph eindigt, zonder teken- en woordspatiëring, en waar de woordafstand-regel hierboven vanaf meet. 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 Delphi 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