Article technique

Extraire du texte à partir d'un PDF chargé en Delphi avec HotPDF

Le composant HotPDF extrait le texte Unicode de tout PDF chargé en Delphi via deux appels : ExtractLoadedPageText renvoie le texte dans l'ordre de lecture d'une page, and ExtractLoadedPageTextLayout (ajouté dans la version 2.263.0) reconstruit l'agencement visuel de la page sous forme de texte brut, de sorte que les colonnes, l'indentation et l'alignement des tableaux subsistent dans la sortie. Les deux fonctionnent sur des documents que HotPDF n'a pas créés, ce qui est le cas le plus important : la facture envoyée par e-mail par un client, le rapport fourni par un bureau de numérisation ou le contrat généré par un logiciel inconnu

Y parvenir a nécessité plus de mécanismes que ce que suggèrent les deux signatures, car un PDF ne stocke pas le texte de la même manière qu'un fichier texte. Cet article détaille les deux modes d'extraction, puis présente les trois composants sous-jacents — le lecteur CMap, l'interpréteur de flux de contenu et la chaîne de fallback de décodage des polices — car comprendre comment fonctionne ce mappage fait toute la différence entre ignorer un résultat incohérent et pouvoir le diagnostiquer

Pourquoi l'extraction de texte est-elle plus difficile que la lecture des chaînes du fichier ?

Un flux de contenu PDF enregistre des codes de caractères, pas des caractères. Les opérateurs Tj et TJ (ISO 32000-1 §9.4.3) contiennent des chaînes d'octets dont la signification dépend entièrement de la police sélectionnée par le Tf précédent : l'octet 0x41 peut être la lettre A sous WinAnsi, un glyphe arbitraire dans une police de sous-ensemble ou la moitié d'un CID de deux octets dans une police CJK composite. L'ISO 32000-1 §9.10 définit l'extraction de texte précisément comme ce problème de décodage — associer chaque code à l'Unicode en utilisant les informations fournies par le dictionnaire de polices — et la norme précise explicitement qu'un fichier conforme n'est pas tenu de fournir suffisamment d'informations pour cela

Cette dernière clause explique tous les rapports d'anomalies du type « pourquoi le copier-coller depuis ce PDF produit-il du texte illisible » que vous avez pu rencontrer. Un producteur qui intègre une police de sous-ensemble sans table /ToUnicode génère un fichier qui s'affiche parfaitement mais dont l'extraction produit des données incohérentes, car l'association code-glyphe existe mais le mappage code-Unicode n'a jamais été fourni. Toute API d'extraction honnête est donc une chaîne de fallbacks au mieux, et la question utile est de savoir jusqu'où va cette chaîne

Extraction dans l'ordre de lecture avec ExtractLoadedPageText

Pour l'indexation de recherche, la correspondance de mots-clés ou l'alimentation d'un pipeline d'analyse, ExtractLoadedPageText est l'appel idéal. La signature est function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean — les index de page commencent à zéro, le résultat est renvoyé sous la forme d'une UnicodeString native de Delphi, et la fonction renvoie False lorsque la page ne contient aucun flux de contenu lisible, au lieu de lever une exception

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 now holds the reading-flow text of the document
  finally
    Pdf.Free;
  end;
end;

Les sauts de ligne dans la sortie proviennent d'une heuristique délibérément simple : lorsque l'origine verticale d'un glyphe se déplace de plus de la moitié de la taille de la police actuelle — caractéristique d'une étape Td ou T* dans le flux de contenu — un saut de ligne est inséré. Les caractères que le décodeur ne peut pas résoudre deviennent des espaces au lieu de disparaître, afin que les limites des mots subsistent même si les glyphes individuels ne le font pas. Ce mode ne tente pas de regroupement par ordre de lecture ni de détection de colonnes multiples : une page à deux colonnes ressort entrelacée dans l'ordre du flux de contenu, ce qui correspond généralement mais pas toujours à l'ordre visuel

Quand devriez-vous utiliser plutôt l'extraction préservant la mise en page ?

ExtractLoadedPageTextLayout is the right call whenever position carries meaning: tables, forms, code listings, anything you intend to diff, grep, or parse by column. Au lieu d'aplatir les glyphes dans un flux, il les regroupe en lignes de base, trie chaque ligne de base selon X et reproduit les espaces blancs horizontaux et verticaux sur une grille de caractères à espacement fixe (monospace) dimensionnée d'après l'avance médiane des glyphes et la taille de la police. Les grands espaces entre les tracés d'une même ligne de base deviennent des séries d'espaces ; les grands espaces entre les lignes de base deviennent des lignes vides. Le résultat se lit comme la page s'affiche

var
  Grid: UnicodeString;
begin
  if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
    TFile.WriteAllText('page1.txt', Grid, TEncoding.UTF8);
  // Columns, indentation and table alignment survive as
  // spaces and blank lines on a character grid
end;

Les deux modes partagent chaque octet du mécanisme de décodage et ne diffèrent que par la disposition des glyphes décodés, le choix ne coûte donc rien en fidélité. Choisissez ExtractLoadedPageText lorsque seuls les mots comptent et ExtractLoadedPageTextLayout lorsque l'agencement importe. La détection de l'ordre de lecture sur plusieurs colonnes reste hors de portée pour les deux — un rendu sous forme de grille d'une page à deux colonnes affiche les deux colonnes côte à côte, fidèlement, ce qui est idéal pour comparer les fichiers (diff) mais inadapté pour le ré-écoulement du texte

Comment HotPDF décode-t-il les codes de caractères en Unicode ?

Le composant HotPDF résout chaque code de caractère via une chaîne de fallbacks ordonnée par priorité : d'abord la CMap /ToUnicode intégrée à la police, puis l'entrée /Encoding (flux ou CMap nommée), puis — pour les polices composites — les fichiers CMap standards d'Adobe pour les collections de caractères telles que Adobe-GB1, Adobe-CNS1, Adobe-Japan1 et Adobe-KR, et enfin les tables WinAnsi et MacRoman intégrées pour les polices simples. Une stratégie qui ne permet pas d'obtenir de réponse rétrograde silencieusement vers la suivante au lieu de lever une exception, et un code qui épuise toute la chaîne se résout en 0 afin que l'appelant puisse compter les échecs plutôt que de deviner

La CMap /ToUnicode (ISO 32000-1 §9.10.3) se place en premier car il s'agit du mappage écrit par le producteur spécifiquement pour l'extraction. Le chemin CMap standard d'Adobe est important pour les documents CJK qui utilisent des CMaps prédéfinies comme UniGB-UTF16-H au lieu de tout intégrer : HotPDF livre les fichiers de collection sous son répertoire resources\CMap, les localise par rapport à l'exécutable au moment de l'exécution et met en cache chaque table analysée par processus — ce qui est utile à savoir car le plus grand d'entre eux, le fichier Adobe-GB1, représente environ 2 Mo de texte source que vous ne souhaitez pas réanalyser pour chaque page. Si le répertoire est absent, le décodeur ignore simplement les CMaps basées sur le disque et travaille avec les tables intégrées ainsi que les encodages intégrés. C'est le pendant côté lecture du problème de mise en forme traité dans la mise en forme des textes à scripts complexes avec HotPDF, où la même distinction code/glyphe est rencontrée lors de l'écriture

Deux pièges de syntaxe CMap à connaître

Les fichiers CMap semblent simples à analyser mais ne le sont pas, et deux détails expliquent la plupart des échecs de première tentative d'analyse. Le premier est que le nombre d'enregistrements vient avant le mot-clé de section : une section contient 2 beginbfchar, et non beginbfchar 2. Un analyseur qui attend le compte après le mot-clé consomme le nombre comme un jeton (token) isolé, puis trouve zéro entrée dans chaque section. L'approche robuste — celle adoptée par le lecteur de HotPDF — consiste à ignorer complètement le décompte et à boucler jusqu'au mot-clé endbfchar / endbfrange correspondant, ce qui présente l'avantage de tolérer les fichiers réels dont les décomptes sont tout simplement erronés

Le second piège est que les cibles bfchar et bfrange sont des chaînes UTF-16BE, pas des entiers. La destination <D83DDE00> signifie U+1F600 — une paire de substitution (surrogate pair) qui doit être recombinée en un seul point de code — et la lecture de ces quatre octets comme un entier gros-boutiste (big-endian) produit une valeur sans signification pour chaque point de code en dehors du plan multilingue de base. Les emojis dans les PDF ne sont plus exotiques, de sorte qu'un décodeur qui ignore la recombinaison de substitution échoue sur les fichiers que vos utilisateurs possèdent réellement. HotPDF analyse d'abord le littéral hexadécimal en octets bruts, puis recombine les unités de code UTF-16BE, ce qui couvre également les cibles multi-caractères produites par les mappages de ligatures

Descendre au niveau des glyphes avec ExtractLoadedPageGlyphs

Les deux appels de texte reposent sur ExtractLoadedPageGlyphs, et la structure THPDFGlyphArray sous-jacente est également disponible pour votre code. Chaque enregistrement THPDFGlyphRecord porte le point de code Unicode résolu aux côtés du code de caractère brut, de la largeur en octets du code (1, 2 ou 4, décidée par la plage codespacerange de la CMap), de la clé et de la taille de la ressource de police active, de l'origine X et Y dans l'espace utilisateur et de l'avance horizontale. C'est suffisant pour concevoir une détection des limites de mots, une mise en surbrillance positionnée ou un algorithme de mise en page personnalisé sans avoir à manipuler vous-même le flux de contenu

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;

Compter les enregistrements Unicode = 0, comme ci-dessus, est le moyen le plus honnête de mesurer la qualité de l'extraction sur un document donné avant de faire confiance au texte en aval. Les enregistrements de glyphes ancrent également chaque caractère à l'opérande source dans le flux de contenu, ce qui permet la recherche et le remplacement de texte dans un document chargé avec HotPDF sur cette même base

Quels PDF ne livreront pas leur texte ?

Certains fichiers résistent à tout extracteur, et il est préférable de les détecter plutôt que de livrer leur résultat. Les documents numérisés en sont l'exemple le plus simple : une page qui n'est qu'une grande image ne contient aucun opérateur de texte, l'extraction renvoie donc correctement une chaîne vide — la solution est l'OCR, et l'extraction des images de la page à partir du PDF chargé constitue la première étape de ce pipeline. Les polices de sous-ensemble sans table /ToUnicode représentent le cas le plus difficile : si le chemin /Encoding et les CMaps standard échouent également, ces glyphes se résolvent en 0 et s'affichent sous forme d'espaces dans les appels de texte. Les documents chiffrés s'extraient normalement à condition de les charger avec leur mot de passe via la surcharge LoadFromFile, afin que les flux soient déchiffrés avant que l'interpréteur ne les examine

Une limite plus étroite mérite d'être énoncée clairement : la chaîne de décodage lit les flux CMap et de contenu via le chemin Flate de HotPDF, de sorte qu'une police dont le flux ToUnicode utilise un filtre inhabituel rétrograde vers la stratégie suivante au lieu de faire échouer la page. En pratique, FlateDecode couvre presque tout ce qui a été produit au cours des deux dernières décennies, et la dégradation est silencieuse par conception — vous obtenez le meilleur texte que le fichier permet plutôt qu'une exception. Le même mécanisme d'objet côté lecture qui résout les dictionnaires de polices ici permet également la modification des métadonnées sur les documents chargés, ainsi un pipeline de réception de documents peut extraire, inspecter et annoter en une seule passe

L'extraction de texte, le rendu préservant la mise en page, l'accès au niveau des glyphes ainsi que les fonctionnalités de recherche et de remplacement associées font tous partie du composant standard HotPDF Component pour Delphi et C++Builder — pas de DLL externe, pas de services de texte de l'OS, juste de l'Object Pascal que vous pouvez suivre pas à pas lorsqu'un fichier étrange arrive dans votre file d'attente