Article technique

Extraction de texte, d'images et de polices depuis un PDF en Delphi avec PDFlibPas

Envoyez quarante mille PDF client dans un indexeur de recherche et les échecs se rangent d'eux-mêmes en trois piles : documents dont les mots se collent parce que les seuils d'espacement ont été ignorés, documents dont le texte extrait est du charabia parce qu'une police subsetted ne fournit pas de map ToUnicode, et documents où « le logo » se révèle être neuf image XObjects séparés plus un masque doux. L'extraction n'est pas un seul appel d'API — c'est une chaîne de décisions sur l'ordre, l'encodage et la provenance, et chaque décision change ce que les systèmes aval peuvent croire. losLab PDF Library (PDFlibPas) donne aux applications Delphi et C++Builder plusieurs niveaux d'extraction avec des contrats de fidélité différents ; choisir entre eux de façon délibérée représente l'essentiel du travail.

Niveaux d'extraction de texte et promesse de chacun

GetPageText prend une valeur d'options de 0 à 8, et le nombre choisit un moteur, pas un format. Les options 0 à 2 exécutent une passe légère suffisante pour des aperçus rapides. Les options 3 à 8 passent par le moteur d'extraction sensible à la mise en page, qui reconstruit lignes et espacements à partir de la géométrie des glyphes : 4 et 6 découpent en plus la sortie en mots, 5 et 6 émettent les informations de largeur, et 7 produit du texte brut en ignorant volontairement les métadonnées de police, de couleur et de bloc — le choix habituel pour alimenter un index de recherche.

Quel que soit le niveau, la fidélité est bornée par le document lui-même. Le PDF mappe des codes de caractères vers des glyphes, et seule la ToUnicode CMap d'une police (ISO 32000-1 §9.10) remappe les codes vers du texte. Une police subsetted sans CMap laisse tous les extracteurs — celui-ci, le copier-coller d'un visualiseur, n'importe lequel — deviner depuis les noms de glyphes ou abandonner. Détecter ce cas compte plus que le gérer : marquez la page comme peu fiable et envoyez-la vers l'OCR au lieu d'indexer silencieusement des déchets.

Lorsque les options flat ne conviennent pas — tokenisation personnalisée, forensics de flux de contenu, construction d'un entonnoir texte sur mesure — la couche classe expose directement le décodeur : TPDFExtractor se construit sur le dictionnaire de ressources et la collection de polices d'une page, sa méthode ExtractTextW convertit les opérations texte brutes du flux de contenu en Unicode avec la même mécanique de polices, et son événement OnFindObject expose chaque objet lorsqu'il passe dans le flux. La plupart des applications n'ont jamais besoin de descendre aussi bas ; celles qui le doivent apprécient que la couche soit publique.

Blocs positionnés : l'unité des résultats de recherche et de revue de caviardage

Le texte brut dit ce que la page contient ; la plupart des produits finissent par devoir savoir où elle le dit — pour surligner un résultat de recherche, vérifier un candidat au caviardage, ancrer une annotation. ExtractPageTextBlocks renvoie un handle vers une liste de runs de texte, chacun portant son texte, sa bounding box, son nom de police et sa taille :

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;

Le piège d'état dans cette zone attrape les équipes pendant l'intégration : SetTextExtractionArea, SetTextExtractionWordGap et SetTextExtractionOptions sont des réglages persistants au niveau document, pas des arguments par appel. Une restriction de zone configurée pour une fonctionnalité — lire seulement la bande d'en-tête pour classification, par exemple — tronque discrètement toutes les extractions suivantes sur le même document, y compris les niveaux GetPageText sensibles à la mise en page. Réinitialisez l'état d'extraction entre tâches logiques, ou donnez à chaque tâche son propre handle de document.

Le seuil d'écart entre mots est le levier pour la pile « mots collés » de la triage initiale : SetTextExtractionWordGap indique au moteur de mise en page quel espace horizontal compte comme séparation de mots, mesuré par rapport à l'espacement des glyphes de la page. Les mises en page tabulaires serrées veulent un écart plus petit que les pages marketing aérées, et un réglage par classe de document vaut mieux qu'une constante globale — en vous souvenant qu'il persiste comme le reste de l'état d'extraction.

Images : flux originaux, pas captures d'écran

Rendre la page puis la recadrer est la mauvaise manière d'extraire les images d'un PDF : cela rééchantillonne, fige la rotation et jette les originaux. GetPageImageList énumère les vraies ressources image référencées par la page, et chaque élément expose ses propriétés et ses données originales :

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;

Inspectez GetImageListItemFormatDesc avant de supposer quoi que ce soit sur un élément. Les pages référencent rarement une image propre par image visible : les masques doux arrivent comme entrées séparées, le même XObject se répète entre pages — dédupliquez par hash de contenu avant d'archiver un export « toutes les images » — et les JPEG CMYK exigent une gestion couleur aval, sinon ils se rendent inversés dans des visualiseurs naïfs. Pour un inventaire de document entier plutôt que par page, FindImages avec SetFindImagesMode parcourt tout le fichier.

Une limite doit être communiquée tôt aux parties prenantes : l'extraction d'images renvoie des ressources raster. Les logos et diagrammes dessinés comme chemins vectoriels ne sont pas des images au sens des ressources et n'apparaîtront jamais dans une liste d'images — lorsque l'exigence est de livrer le graphique comme image, l'implémentation honnête rend la région de page en bitmap, et les deux types de sortie ne doivent pas partager un dossier d'export sans étiquetage.

Polices : surface d'audit, pas fonctionnalité d'export

L'API de polices répond à des questions sur les polices ; elle ne livre pas les fichiers de police. Après que FindFonts a parcouru le document, l'énumération se fait par ID et les appels de propriété décrivent la police actuellement sélectionnée :

var
  I: Integer;
begin
  Pdf.FindFonts;
  for I := 1 to Pdf.FontCount do        // font indexes start at 1, not 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;

Notez les bornes de boucle : les index de polices vont de 1 à FontCount, contrairement aux index zéro-based des blocs de texte et listes d'images vus quelques paragraphes plus haut — mélanger les deux conventions produit un off-by-one qui saute la première police ou lit après la dernière. Et pour être précis sur le périmètre : cette API ne propose aucun export de police au niveau octet. Aucun appel ne renvoie le programme de police incorporé comme fichier TTF ou OTF ; l'énumération et l'inspection des métadonnées sont le modèle prévu. En production, ce modèle couvre ce qui compte réellement — détection de subsets par motif de nom, audits d'incorporation avant conversion archivistique (une police non incorporée bloque durement PDF/A, comme expliqué dans le preflight PDF/A et PDF/UA en Delphi), et diagnostics d'encodage lorsque la confiance dans l'extraction baisse. Les programmes de police subsetted sont du matériel sous licence et incomplets comme polices installables ; les traiter comme métadonnées d'audit plutôt que comme actifs extractibles est la position défendable.

La sonde d'encodage gagne sa place dans les pipelines de triage : GetFontEncoding sur chaque police, combiné au flag de subset, prédit la qualité d'extraction avant de tirer le moindre texte — une page dont toutes les polices sont subsetted avec des encodages non standard est candidate à l'OCR par simple inspection.

Extraction à grande échelle sans charger les documents

Pour les pipelines batch, charger tout un document afin de lire une page est de l'E/S gaspillée. Les variantes en un seul appel — ExtractFilePageText et ExtractFilePageTextBlocks — prennent directement un nom de fichier, un mot de passe et un numéro de page. Pour les fichiers à l'échelle du gigaoctet, il existe un rapport inférieur : le chemin Direct Access ouvre le fichier par lectures xref en flux, si bien que DAOpenFileReadOnly suivi de DAExtractPageText ne touche que les objets nécessaires à une page. Une convention à respecter : les fonctions DA adressent les pages par PageRef, un handle de référence d'objet obtenu avec DAFindPage, pas par numéro de page — passer le numéro là où le handle est attendu opère sur le mauvais objet sans lever d'erreur. L'outillage Direct Access plus large est cartographié dans la fusion, la scission et l'accès direct de grands PDF.

Questions fréquentes sur l'extraction

Pourquoi le texte extrait diffère-t-il de ce que le visualiseur affiche ?

Généralement à cause de l'encodage : les ligatures se décodent en glyphes uniques, et les polices subsetted avec des maps ToUnicode incomplètes produisent substitutions ou trous. Comparez la confiance d'extraction entre pages, et traitez les pages dominées par des glyphes non mappés comme candidates à l'OCR.

PDFlibPas peut-il enregistrer une police incorporée comme fichier TTF ou OTF ?

Non. L'API de polices énumère et inspecte — nom, famille, type, encodage, statut incorporé et subsetted — et c'est tout son périmètre. Concevez les workflows de polices autour des questions d'audit plutôt que de l'export de fichiers.

Comment extraire le texte d'une seule région de la page ?

SetTextExtractionArea limite les extractions suivantes à un rectangle. Souvenez-vous qu'il persiste sur le document : réinitialisez-le une fois la tâche régionale terminée, sinon l'extraction pleine page suivante renverra silencieusement la région seulement.

Les builds d'évaluation, projets de démonstration et la référence complète de l'API d'extraction sont sur la page produit losLab PDF Library pour Delphi.