Tekst, afbeeldingen en lettertypes uit een bestaande PDF halen klinkt als een opgelost probleem tot je er een echt corpus doorheen haalt. Richt een zoek-indexeerder op veertigduizend klantbestanden en de breuk sorteert zich in een paar herkenbare stapels. Woorden plakken aan elkaar omdat niemand de extractor vertelde hoe breed een spleet als spatie telt. Andere pagina's komen terug als onzin omdat een ingebed subset-font geen mapping van zijn glyph-codes naar werkelijke tekens meedraagt. En "het bedrijfslogo" blijkt negen afzonderlijke afbeeldingsobjecten te zijn, gestapeld achter een zacht masker. Niets daarvan is een bug in de bibliotheek. Het is het verschil tussen een extractiefunctie aanroepen en begrijpen wat de functie wel en niet uit de bytes op schijf kan terugkrijgen
losLab PDF Library, de Pascal-editie, geeft Delphi- en C++Builder-code meer dan één manier om elk van die drie streams te lezen, en de niveaus verschillen in wat ze garanderen. De truc is het niveau op de taak af te stemmen: een zoekindex, een redactiereviewer en een PDF/A-preflightpassage willen allemaal verschillende dingen van dezelfde pagina, en de verkeerde aanroep doen verspilt moeite of produceert output die je niet kunt vertrouwen
Tekstextractieniveaus en wat elk belooft
GetPageText neemt een optiewaarde van 0 tot en met 8, en dat nummer kiest een motor in plaats van een formaat. Waarden 0 tot en met 2 draaien een lichte passage die prima is voor een snelle voorvertoning. Waarden 3 tot en met 8 gaan door de lay-out-bewuste motor, die regels en spatiëring herbouwt vanaf waar de glyphs werkelijk op de pagina zitten. Binnen dat bereik doen variaties ertoe: 4 en 6 splitsen de output in woorden, 5 en 6 zenden per-glyph-breedten uit, en 7 geeft platte tekst terug met font, kleur en blokmetadata opzettelijk weggelaten. Optie 7 is degene die je aan een zoekindex voert, omdat de index woorden wil en niets anders
Geen optie-instelling kan een document redden dat de informatie nooit van begin af aan meedroeg. PDF mapt tekencodes naar glyph-vormen, en het enige dat die codes terugbrengt naar leesbare tekst is een ToUnicode CMap van het font (ISO 32000-1 §9.10). Als een subset-font zonder een CMap wordt verzonden, zit elke extractor vast. Deze bibliotheek, kopiëren-en-plakken in een viewer, een concurrerende toolkit: ze worden allemaal herleid tot gissen vanaf glyph-namen of niets teruggeven. Het praktische antwoord is detectie, geen heldhaftigheid. Scoor de pagina als lage betrouwbaarheid en stuur hem naar OCR, want de onzin stil indexeren is erger dan toegeven dat je hem niet kunt lezen
Voor de gevallen die de vlakke opties niet dekken — aangepaste tokenisatie, content-stream-forensiek, een tekst-trechter gebouwd volgens je eigen regels — is de decoder één laag lager beschikbaar. TPDFExtractor wordt geconstrueerd over de resourcewoordenlijst en fontverzameling van een pagina. Zijn ExtractTextW-methode draait ruwe content-stream-tekstbewerkingen opnieuw door dezelfde font-machinerie om Unicode te herstellen, en zijn OnFindObject-gebeurtenis overhandigt je elk object terwijl het voorbijstroomt. Meeste code hoeft nooit zo diep te gaan. De toepassingen die dat wel doen zijn de toepassingen die blij zijn dat de laag publiek is in plaats van begraven
Gepositioneerde blokken: de eenheid van zoektreffers en redactiecontrole
Platte tekst vertelt je wat de pagina zegt. Vroeg of laat moet een product ook weten waar het het zegt, zodat het een zoektreffer kan markeren, een kader om een redactiekandidaat kan trekken, of een annotatie op de juiste plek kan verankeren. ExtractPageTextBlocks retourneert een handle naar een lijst met tekst-runs, en elke run draagt zijn tekst, zijn begrenzingskader, en de fontnaam en -grootte waarin hij was gezet:
var
Pdf: TPDFlib;
Blocks, I: Integer;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('contract.pdf', '') <> 1 then
raise Exception.Create('load failed');
Pdf.SelectPage(1);
Blocks := Pdf.ExtractPageTextBlocks(0);
for I := 0 to Pdf.GetTextBlockCount(Blocks) - 1 do
Writeln(Format('%s [%s %.1f pt at %.0f,%.0f]',
[Pdf.GetTextBlockText(Blocks, I),
Pdf.GetTextBlockFontName(Blocks, I),
Pdf.GetTextBlockFontSize(Blocks, I),
Pdf.GetTextBlockBound(Blocks, I, 0),
Pdf.GetTextBlockBound(Blocks, I, 1)]));
Pdf.ReleaseTextBlocks(Blocks);
finally
Pdf.Free;
end;
end;
Eén detail op dit gebied laat integraties vaker struikelen dan wat ook. SetTextExtractionArea, SetTextExtractionWordGap en SetTextExtractionOptions zijn documentniveau-status die persistent is, geen argumenten die je per aanroep doorgeeft. Configureer een gebiedsbeperking voor één feature — zeg alleen de kopband lezen om een document te classificeren — en het beperkt stilletjes elke extractie die op dezelfde handle volgt, inclusief de lay-out-bewuste GetPageText-niveaus die je later aanspreekt. Reset de extractiestatus tussen logische taken of geef elke taak zijn eigen documenthandle
De woordgappedrempel is de hendel voor die eerste stapel mislukkingen, de woorden die aan elkaar plakken. SetTextExtractionWordGap vertelt de lay-out-motor hoeveel horizontale ruimte, gemeten tegen de pagina's eigen glyph-spatiëring, het ene woord van het volgende scheidt. Een dichte tabel wil een kleinere gap dan een los gezette marketingpagina, dus een per documentklasse afgestemde drempel verslaat één globale constante. Het persistent op het document net als de rest van de extractiestatus, dus plan het opzettelijk in te stellen in plaats van één keer en vergeten
Afbeeldingen: originele streams, geen schermafbeelden
De verkeerde manier om afbeeldingen uit een PDF te krijgen is de pagina te renderen en bij te snijden. Dat herbemonstert de pixels, brandt elke rotatie erin, en gooit weg wat het origineel ook was. GetPageImageList daarentegen somt de werkelijke afbeeldingsresources op waarnaar de pagina verwijst, en elk item levert zijn eigenschappen en zijn originele, ongestoorde data terug:
var
ImgList, I: Integer;
begin
Pdf.SelectPage(1);
ImgList := Pdf.GetPageImageList(0);
for I := 0 to Pdf.GetImageListCount(ImgList) - 1 do
begin
Writeln(Pdf.GetImageListItemFormatDesc(ImgList, I, 0));
Pdf.SaveImageListItemDataToFile(ImgList, I, 0,
Format('page1-img%.2d.bin', [I]));
end;
Pdf.ReleaseImageList(ImgList);
end;
Controleer GetImageListItemFormatDesc voordat je iets over een item aanneemt, want waarnaar een pagina verwijst is zelden één nette foto per zichtbare afbeelding. Een zacht masker verschijnt als zijn eigen afzonderlijke item. Hetzelfde XObject herhaalt zich vaak over veel pagina's, dus dupliceer op content-hash voordat je een "alle afbeeldingen"-export archiveert, of je schrijft hetzelfde logo honderd keer. CMYK-JPEG's hebben kleurbeheer nodig dat stroomafwaarts wordt toegepast, of ze renderen omgekeerd in viewers die de kanalen op nominale waarde nemen. Als je een documentbrede inventaris wilt in plaats van één pagina tegelijk, scant FindImages samen met SetFindImagesMode het hele bestand in één passage
Er is één grens die het waard is bij belanghebbenden op te werpen voordat iemand acceptatiecriteria schrijft: afbeeldingextractie retourneert alleen rasterresources. Een logo of diagram getekend als vectorpaden is geen afbeelding in de resourcezin en zal nooit in enige afbeeldingslijst verschijnen, hoe duidelijk het ook als een plaatje op het scherm leest. Als de eis werkelijk is om dat diagram als bestand te leveren, is de eerlijke aanpak om de paginaregio naar een bitmap te renderen, wat een andere bewerking is met andere getrouwheid. De twee soorten output horen niet in dezelfde exportmap zonder een label dat zegt welke welke is
Lettertypes: een auditoppervlak, geen exportfeature
De lettertype-API beantwoordt vragen over lettertypes. Het overhandigt je niet de lettertypebestanden zelf, en dat onderscheid vormt alles wat je erop kunt bouwen. Nadat FindFonts het document heeft gescand, loopt de enumeratie de lettertypes af op ID, en de eigenschap-aanroepen rapporteren over welk lettertype momenteel geselecteerd is:
var
I: Integer;
begin
Pdf.FindFonts;
for I := 1 to Pdf.FontCount do // lettertype-indexen beginnen bij 1, niet bij 0
if Pdf.SelectFont(Pdf.GetFontID(I)) = 1 then
Writeln(Format('%s type=%d embedded=%d subset=%d',
[Pdf.FontName, Pdf.FontType,
Pdf.GetFontIsEmbedded, Pdf.GetFontIsSubsetted]));
end;
Pas op voor de lusgrenzen. Lettertype-indexen lopen van 1 tot FontCount, terwijl de tekstblok- en afbeeldingslijst-indexen een paar alinea's hoger op nul gebaseerd zijn. Draag de ene conventie over naar de andere en je krijgt een off-by-one die ofwel het eerste lettertype overslaat ofwel voorbij het einde loopt, en hij zal oppervlakkige tests doorstaan omdat de meeste documenten meerdere lettertypes hebben en de verkeerde nog plausibel lijkt. Wees ook duidelijk over de reikwijdte. Deze API heeft geen byte-niveau lettertype-export. Geen aanroep retourneert het ingebedde lettertypeprogramma als een TTF- of OTF-bestand, en enumeratie plus metadata-inspectie is het hele beoogde model. Dat model dekt nog steeds wat productiewerk werkelijk aan lettertypes vraagt: subset-detectie op naampatroon, inbeddingsaudits voor een archiveringsconversie (een niet-ingebouwd lettertype is een harde PDF/A-blokkering, zoals PDF/A- en PDF/UA-preflight in Delphi ingaat), en coderingsdiagnostiek voor wanneer de extractiebetrouwbaarheid daalt. Er is ook een licentiereden dat de grens hier zit. Een subset-lettertypeprogramma is gelicentieerd materiaal en, de meeste van zijn glyphs missend, nutteloos als een installeerbaar lettertype toch. Het als auditmetadata behandelen in plaats van een extraheerbaar actief is de positie die je kunt verdedigen
Die laatste aanroep trekt zijn gewicht in triage. Draai GetFontEncoding op elk lettertype, lees het naast de subset-vlag, en je kunt de extractiekwaliteit voorspellen voordat je een enkel teken trekt. Een pagina waarvan de lettertypes allemaal gesubset zijn met niet-standaard coderingen is bij inspectie alleen al een OCR-kandidaat, wat een batch-pipeline haar correct laat routeren zonder eerst een mislukte extractiepassage op haar te verspillen
Extractie op schaal zonder documenten te laden
In een batch-pipeline is een heel document laden alleen om één pagina te lezen verspilde I/O, en het hoopt snel op over een corpus. De enkele-aanroep-varianten, ExtractFilePageText en ExtractFilePageTextBlocks, nemen een bestandsnaam, wachtwoord en paginanummer direct en slaan de volledige lading over. Voor bestanden op gigabyte-schaal is er nog een lagere versnelling. Het directe-toegangspad opent een bestand via streaming-xref-lezingen, dus DAOpenFileReadOnly gevolgd door DAExtractPageText raakt alleen de objecten die die ene pagina werkelijk nodig heeft. Het komt met een conventiewisseling die de moeite waard is om te onthouden: de DA-functies adresseren pagina's op PageRef, een object-referentie-handle die je van DAFindPage krijgt, nooit op ruw paginanummer. Geef het nummer waar de handle hoort en de aanroep werkt op het verkeerde object zonder een fout op te werpen, wat de ergste soort fout is om te debuggen. De rest van de directe-toegangstoolkit is uiteengezet in grote PDF samenvoegen, splitsen en directe toegang
Als er één gewoonte is die extractiecode die een echt corpus overleeft onderscheidt van code die hinkt, is het de pagina als onvertrouwde invoer behandelen in plaats van een schone databron. Tekst die het oneens is met wat de viewer rendert is bijna altijd een coderingsprobleem, een ligatuur die tot één glyph inzakt of een subset-font dat zijn ToUnicode-vermeldingen mist, en de oplossing is betrouwbaarheid meten en de slechte pagina's naar OCR afleiden, niet met de bytes vechten. De lettertype-API zal nooit een TTF of OTF produceren, door het ontwerp, dus bouw lettertypeworkflows rond auditvragen. En de persistente extractiestatus, het gebied-rechthoek vooral, is een instelling die je bezit voor de levensduur van een documenthandle, geen parameter die je vergeet na één aanroep. Krijg die drie reflexen goed en de rest van de API gedraagt zich
Evaluatie-builds, demoprojecten en de volledige extractie-API-referentie staan op de losLab PDF Library voor Delphi-productpagina