Tekstextractie uit PDF-bestanden lijkt eenvoudig totdat u een document tegenkomt waarin de tekstlaag ontbreekt, beschadigd is of is opgesplitst in tientallen kleine karakterreeksen zonder betekenisvolle volgorde. PDFium Component biedt u twee invoerpunten: de array Character[] voor ruwe, op index gebaseerde toegang tot elke glyph op een pagina, en ReadablePageContent voor een gestructureerde weergave die alinea's en koppen reconstrueert uit de tag-structuur van de PDF of via een heuristische analyse. Geen van beide is altijd de juiste keuze, dus het is belangrijk om te begrijpen wat ze elk blootleggen
Het document openen en de valkuil van geruisloze fouten
TPdf opent een bestand door FileName in te stellen en Active := True in te schakelen. Het kritieke detail: Active := True roept nooit een uitzondering op. Als het bestand ontbreekt, met een wachtwoord is beveiligd of beschadigd is, PDFium de fout intern opvangt en blijft Active simpelweg op False staan. Dit betekent dat elke extractielus hiertegen moet waken:
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Active := True;
if not Pdf.Active then
begin
ShowMessage('Kon PDF niet openen (beschadigd of verkeerd wachtwoord)');
Exit;
end;
// extractie volgt hier
finally
Pdf.Active := False;
Pdf.Free;
end;
Bestanden die met een wachtwoord zijn beveiligd, vereisen dat Pdf.Password := '...' wordt ingesteld voordat Active := True wordt ingeschakeld. Er is geen tweede kans: zodra Active faalt, sluit u het bestand en opent u het opnieuw met het juiste wachtwoord
Pagina-voor-pagina extractie met Character[]
De meest basale benadering doorloopt elk karakter op elke pagina. Stel Pdf.PageNumber in om de tekstlaag voor die pagina te laden, en doorloop vervolgens de CharacterCount-vermeldingen met behulp van de eigenschap Character[]. Twee vlaggen op elke vermelding zijn het controleren waard: CharacterGenerated[i] markeert synthetische glyphs die door de renderer zijn ingevoegd (zoals zachte afbreektekens aan het einde van een regel) die geen echte Unicode-waarde hebben, en CharacterMapError[i] geeft aan dat PDFium de glyph niet kon toewijzen aan een code point, wat gebeurt bij lettertype-coderingen die een ToUnicode-tabel missen
procedure ExtractAllText(Pdf: TPdf; Output: TStrings);
var
Page, I: Integer;
Line: string;
Ch: WideChar;
begin
for Page := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := Page;
Line := '';
for I := 0 to Pdf.CharacterCount - 1 do
begin
if Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I] then
Continue;
Ch := Pdf.Character[I];
if Ch = #13 then
Ch := #10; // CR normaliseren naar LF
Line := Line + Ch;
end;
Output.Add(Line);
end;
end;
Het resultaat is een platte string van Unicode-code points in de volgorde waarin PDFium ze opsomt. Dit is de volgorde waarin ze in de inhoudsstroom verschijnen, en niet noodzakelijkerwijs de leesvolgorde van links naar rechts. Voor de meeste Latijnse documenten die met standaard kantoorsoftware zijn geproduceerd is dit prima. Voor gescande PDF's die via OCR zijn verwerkt met ongebruikelijke glyph-volgordes, of voor tekst die van rechts naar links leest, kan de volgorde onjuist zijn. In dat geval is ReadablePageContent nuttiger
Gestructureerde extractie met ReadablePageContent
ReadablePageContent gaat een niveau hoger: het retourneert een TPdfReadableContent-record waarvan de array Fragments getagde inhoudsfragmenten bevat, elk met een Kind die alinea's, koppen, lijstitems, tabelcellen, enzovoort identificeert. Wanneer de PDF een structuurboom bevat (controleer Pdf.IsTagged), is de bron rosStructure en is de leesvolgorde gezaghebbend. Voor niet-getagde bestanden valt PDFium terug op rosHeuristic, die karakters op basis van hun begrenzingskaders groepeert in aannemelijke leeseenheden, maar de nauwkeurigheid niet kan garanderen
procedure ExtractStructured(Pdf: TPdf; Output: TStrings);
var
Page: Integer;
Content: TPdfReadableContent;
Fragment: TPdfContentFragment;
begin
for Page := 1 to Pdf.PageCount do
begin
Content := Pdf.ReadablePageContent(Page);
for Fragment in Content.Fragments do
begin
case Fragment.Kind of
cfHeading : Output.Add('# ' + Fragment.Text);
cfParagraph : Output.Add(Fragment.Text);
cfListItem : Output.Add('- ' + Fragment.Text);
else
Output.Add(Fragment.Text);
end;
end;
end;
end;
Als Content.Source = rosHeuristic en uw uitvoer er verminkt uitziet, is de tekstlaag van het document waarschijnlijk niet geschreven met de leesvolgorde in het achterhoofd. Op dat moment is de enige betrouwbare oplossing het opnieuw exporteren vanuit de bronapplicatie met de juiste tagging, of het uitvoeren van een nabewerkingsstap die de karakteroorsprongen sorteert op Y en vervolgens op X
Wat CharacterOrigin en CharacterRectangle u bieden
Beide eigenschappen retourneren de positie van een karakter in de paginaruimte (in punten, met de oorsprong in de linkerbenedenhoek en Y die naar boven toe toeneemt). CharacterOrigin[i] is het basislijnas-ankerpunt van de glyph; CharacterRectangle[i] is het volledige begrenzingskader (bounding box). Dit zijn de bouwstenen voor alles wat verder gaat dan platte tekst: het detecteren van kolomgrenzen, het groeperen van karakters in regels door Y-coördinaten binnen een tolerantie te vergelijken, of het bouwen van een hit-testkaart voor tekstselectie in een viewer. Als u wilt weten welk karakter zich onder een muisklik bevindt, voert CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) die zoekopdracht rechtstreeks uit zonder dat u de rechthoeken hoeft te doorlopen
De DLL op zijn plaats krijgen
PDFium Component delegeert alle PDF-ontleding aan een systeemeigen DLL, ofwel pdfium32.dll of pdfium64.dll, afhankelijk van uw doelplatform. De component wordt geleverd met een CopyDlls.bat-script dat het juiste bestand naar de Windows-systeemmap kopieert. Dit script eenmalig als Administrator uitvoeren op een ontwikkelmachine is voldoende; voor distributie kopieert u de DLL in plaats daarvan naast het uitvoerbare bestand van de applicatie. De V8-compatibele varianten (pdfium32v8.dll, pdfium64v8.dll) zijn aanzienlijk groter en alleen nodig als uw PDF's JavaScript bevatten dat moet worden uitgevoerd. Voor pure tekstextractie is de standaard build de juiste keuze
Als de DLL tijdens runtime ontbreekt, zal Active := True geruisloos falen, net zoals bij een ontbrekend bestand, omdat de component de laadfout intern opvangt. Test altijd op een schone machine voordat u de applicatie distribueert
FontSize[] gebruiken naast Character[] voor lay-outanalyse
Naast platte tekst stelt de API op karakterniveau FontSize[i] beschikbaar, die de gerenderde puntgrootte van elke glyph retourneert. Gecombineerd met CharacterOrigin[i] en CharacterRectangle[i] stelt dit u in staat om hoofdtekst te onderscheiden van koppen zonder afhankelijk te zijn van de structuurboom. Een karakterreeks waarin de lettergrootte boven een drempelwaarde stijgt, is in een niet-getagd document vrijwel zeker een kop. Dezelfde techniek kan worden toegepast om bijschriften (kleine tekst onder het begrenzingskader van een afbeelding) of voetnoten (kleine tekst onderaan de pagina) te detecteren. Dit vereist geen rendering; alle drie de eigenschappen lezen rechtstreeks uit de tekstlaag die PDFium opbouwt tijdens Active := True
Eén nuance: FontSize[i] weerspiegelt de grootte nadat de CTM (current transformation matrix) van de pagina is toegepast. Een document waarin de auteur de hele pagina heeft geschaald, zal dus proportioneel aangepaste groottes rapporteren. Als u groottes vergelijkt over pagina's met verschillende pagina-afmetingen, normaliseer dan ten opzichte van de MediaBox-hoogte van elke pagina voordat u beslissingen neemt over drempelwaarden
De uitvoer naar een bestand schrijven
De TStringList van Delphi handelt UTF-8-uitvoer sinds XE probleemloos af. Stel WriteBOM := False in als u een BOM-vrij bestand nodig hebt (veel downstream-verbruikers verslikken zich in een leidende BOM):
var
Lines: TStringList;
begin
Lines := TStringList.Create;
try
ExtractAllText(Pdf, Lines);
Lines.WriteBOM := False;
Lines.SaveToFile('output.txt', TEncoding.UTF8);
finally
Lines.Free;
end;
end;
Voor zeer grote documenten waarbij het geheugen een rol speelt, schrijft u in de paginalus rechtstreeks naar een TStreamWriter met TEncoding.UTF8, in plaats van eerst alles in een lijst te verzamelen
De Character[]-, CharacterCount-, CharacterOrigin[]-, CharacterRectangle[]-, ReadablePageContent- en CharacterIndexAtPos-API's die hier worden getoond, maken deel uit van het PDFium Component voor Delphi en C++Builder