L'extraction de texte à partir de PDF semble simple jusqu'à ce que vous rencontriez un document où la couche de texte est absente, corrompue ou divisée en dizaines de minuscules fragments de caractères sans aucun ordre logique. Le composant PDFium vous propose deux points d'entrée : le tableau Character[] pour un accès brut basé sur l'index de chaque glyphe d'une page, et ReadablePageContent pour une vue structurée qui reconstitue les paragraphes et les titres à partir de l'arborescence des balises (tags) du PDF ou d'une analyse heuristique. Aucun de ces deux choix n'étant universel, il convient de comprendre ce que chacun propose
Ouvrir le document et le piège de l'échec silencieux
TPdf ouvre un fichier en définissant FileName et en passant Active := True. Un détail essentiel est à noter : Active := True ne lève jamais d'exception. Si le fichier est manquant, protégé par un mot de passe ou corrompu, PDFium intercepte l'erreur en interne et Active reste simplement à False. Cela implique que chaque boucle d'extraction doit être protégée comme suit :
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Active := True;
if not Pdf.Active then
begin
ShowMessage('Could not open PDF (damaged or wrong password)');
Exit;
end;
// extraction follows here
finally
Pdf.Active := False;
Pdf.Free;
end;
Les fichiers protégés par mot de passe nécessitent de définir Pdf.Password := '...' avant de passer Active := True. Il n'y a pas de seconde chance : si Active échoue, vous devez fermer et réouvrir le document avec le mot de passe correct
Extraction page par page avec Character[]
L'approche de plus bas niveau consiste à parcourir chaque caractère de chaque page. Définissez Pdf.PageNumber pour charger la couche de texte de cette page, puis parcourez les entrées CharacterCount via la propriété Character[]. Deux indicateurs sur chaque entrée méritent d'être vérifiés : CharacterGenerated[i] signale les glyphes synthétiques insérés par le moteur de rendu (comme les tirets conditionnels en fin de ligne) qui n'ont pas de valeur Unicode réelle, et CharacterMapError[i] indique que PDFium n'a pas pu associer le glyphe à un point de code, ce qui se produit avec les encodages de polices dépourvus de table ToUnicode
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; // normalize CR to LF
Line := Line + Ch;
end;
Output.Add(Line);
end;
end;
Le résultat obtenu est une chaîne de points de code Unicode dans l'ordre où PDFium les énumère. Il s'agit de l'ordre d'apparition dans le flux de contenu, qui ne correspond pas nécessairement à l'ordre de lecture gauche-à-droite. Pour la plupart des documents en caractères latins produits par des outils de bureautique classiques, cela convient. Pour les PDF numérisés dont la reconnaissance de caractères (OCR) a produit des séquences de glyphes inhabituelles, ou pour les textes s'écrivant de droite à gauche, cet ordre peut être erroné. C'est dans ces situations que ReadablePageContent s'avère plus utile
Extraction structurée avec ReadablePageContent
ReadablePageContent se situe un niveau au-dessus : elle renvoie un enregistrement TPdfReadableContent dont le tableau Fragments contient des fragments de contenu balisés, chacun ayant un type (Kind) identifiant des paragraphes, des titres, des éléments de liste, des cellules de tableau, etc. Lorsque le PDF contient une arborescence structurelle (vérifiez Pdf.IsTagged), la source est rosStructure et l'ordre de lecture fait foi. Pour les fichiers non balisés, PDFium se rabat sur rosHeuristic, qui regroupe les caractères selon leurs boîtes de délimitation (bounding boxes) en unités de lecture logiques, sans toutefois garantir une précision absolue
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;
Si Content.Source = rosHeuristic and your output looks garbled, the document’s text layer probably was not written with reading order in mind. At that point the only reliable fix is re-exporting from the source application with proper tagging, or running a post-processing step that sorts character origins by Y then X
Content.Source = rosHeuristic et que votre sortie semble désordonnée, il est probable que la couche de texte du document n'a pas été conçue en respectant l'ordre de lecture. Dans ce cas, la seule solution fiable consiste à exporter à nouveau le document depuis l'application d'origine avec un balisage correct, ou à appliquer un traitement ultérieur pour trier les positions des caractères par Y puis par X"
Let's verify this. Yes!
)
Ce que CharacterOrigin et CharacterRectangle vous apportent
Ces deux propriétés renvoient la position d'un caractère dans l'espace de la page (exprimée en points, avec l'origine dans le coin inférieur gauche et Y croissant vers le haut). CharacterOrigin[i] représente le point d'ancrage de la ligne de base du glyphe ; CharacterRectangle[i] correspond à sa boîte de délimitation complète. Ce sont les briques de base pour tout traitement avancé : détection des limites de colonnes, regroupement des caractères en lignes en comparant les coordonnées Y dans une plage de tolérance, ou construction d'une carte de hit-test pour la sélection de texte dans une visionneuse. Si vous devez identifier le caractère survolé par un clic de souris, CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) effectue cette recherche directement, vous évitant de parcourir les rectangles
Mettre en place la DLL
Le composant PDFium délègue toute l'analyse des PDF à une DLL native, pdfium32.dll ou pdfium64.dll selon votre plateforme cible. Le composant fournit un script CopyDlls.bat qui copie le bon fichier dans le dossier système de Windows. L'exécuter une fois en tant qu'administrateur sur votre poste de développement suffit ; pour le déploiement, vous copiez la DLL à côté de l'exécutable de l'application. Les variantes incluant le moteur V8 (pdfium32v8.dll, pdfium64v8.dll) sont beaucoup plus volumineuses et ne sont nécessaires que si vos PDF contiennent du code JavaScript devant s'exécuter. Pour une simple extraction de texte, la version standard est le meilleur choix
Si la DLL est absente lors de l'exécution, Active := True échouera silencieusement, de la même manière que pour un fichier manquant, car le composant intercepte l'erreur de chargement en interne. Effectuez toujours un test sur un environnement propre avant de déployer
Utiliser FontSize[] avec Character[] pour l'analyse de mise en page
Au-delà du texte brut, l'API au niveau des caractères expose FontSize[i], qui renvoie la taille en points affichée de chaque glyphe. Combinée avec CharacterOrigin[i] et CharacterRectangle[i], cette propriété vous permet de distinguer le corps du texte des titres sans dépendre de l'arborescence structurelle. Une suite de caractères dont la taille de police dépasse un certain seuil est presque à coup sûr un titre dans un document non balisé. La même technique s'applique pour détecter des légendes (petit texte sous la boîte d'une image) ou des notes de bas de page (petit texte près du bas de la page). Rien de tout cela ne nécessite de rendu ; ces trois propriétés sont lues directement depuis la couche de texte construite par PDFium lorsque Active := True
Une nuance est à observer : FontSize[i] reflète la taille après application de la matrice de transformation courante (CTM) de la page, ainsi un document dont l'auteur a redimensionné la page entière affichera des tailles ajustées proportionnellement. Si vous comparez des tailles entre des pages de dimensions différentes, normalisez-les par rapport à la hauteur de la MediaBox de chaque page avant de prendre des décisions de seuil
Écrire le résultat dans un fichier
Le TStringList de Delphi gère correctement la sortie UTF-8 depuis la version XE. Définissez WriteBOM := False si vous avez besoin d'un fichier sans marque d'ordre des octets (BOM), car de nombreuses applications en aval n'acceptent pas de BOM initial :
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;
Pour les documents volumineux où la mémoire est limitée, écrivez directement dans un TStreamWriter avec TEncoding.UTF8 à l'intérieur de la boucle de page, au lieu d'accumuler tout le contenu dans une liste au préalable
Les API Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent et CharacterIndexAtPos présentées ici font partie du composant PDFium pour Delphi et C++Builder