Article technique

Rechercher et remplacer du texte dans un PDF existant en Delphi

Le composant HotPDF peut rechercher et remplacer du texte dans un PDF existant depuis Delphi et C++Builder. SearchLoadedPageText et SearchLoadedDocumentText localisent chaque occurrence d'une chaîne avec une précision au niveau du glyphe, et ReplaceLoadedPageText et ReplaceLoadedDocumentText réécrivent les octets correspondants sur place — à condition que chaque caractère de remplacement puisse être ré-encodé via la police d'origine, une contrainte physique que cet article traite en toute franchise plutôt que de la masquer dans une note de bas de page

La demande derrière cette fonctionnalité est toujours banale. Une entreprise change de nom et trois mille factures archivées portent encore l'ancien nom. Un modèle de contrat a été envoyé avec la date d'expiration de l'année dernière. Un code produit a été retiré et chaque fiche technique qui le mentionne a besoin du code successeur à la place. Dans un traitement de texte, chacune de ces opérations prend trente secondes. Dans un PDF, c'est un problème réellement difficile, et en comprendre la raison fait toute la différence entre bien utiliser l'API et soumettre un rapport d'anomalie qui n'est en fait qu'une citation de la spécification

Pourquoi remplacer du texte dans un PDF est-il si difficile ?

Remplacer du texte dans un PDF est difficile car une page PDF ne contient pas de texte modifiable — elle contient des glyphes positionnés. Selon le modèle d'affichage de texte de l'ISO 32000-1 §9.4, un flux de contenu pilote des opérateurs comme Tj and TJ qui tracent des séquences de codes de caractères à des coordonnées établies par la matrice de texte. Ces codes ne sont pas Unicode ; ce sont des indices dans l'encodage déclaré par la police de la page, et le mappage vers des caractères lisibles peut résider dans une CMap /ToUnicode, un tableau de différences d'encodage ou une chaîne de mappage CID. Il n'y a pas d'objet paragraphe, pas de flux de texte, et aucune garantie qu'un mot visuel soit la base de stockage d'une seule chaîne

Le remplacement ajoute un deuxième niveau de difficulté en plus du décodage : vous devez savoir exactement quels octets du flux d'origine ont produit chaque glyphe, afin de pouvoir insérer de nouveaux octets précisément dans cette zone et rien d'autre. Un extracteur de texte peut se permettre d'ignorer les positions des octets une fois l'Unicode extrait. Un outil de remplacement ne le peut pas. C'est pourquoi HotPDF a divisé le travail sur deux versions — la v2.251.0 a construit la couche de suivi des décalages et de recherche, et la v2.252.0 a construit la couche de réécriture par-dessus

Trouver du texte : recherche au niveau des glyphes avec suivi des décalages d'octets

La fonction SearchLoadedDocumentText de HotPDF trouve chaque occurrence d'un terme en effectuant une correspondance avec la séquence de glyphes Unicode décodée de chaque page, et non avec les octets bruts du flux. Ainsi, une occurrence trouvée est valide quelle que soit la manière dont la police l'a encodée. L'infrastructure sous-jacente a été introduite dans la version 2.251.0 : le pré-analyseur (tokenizer) du flux de contenu enregistre une plage d'octets StartOfs/EndOfs pour chaque opérande de chaîne — y compris ses délimiteurs ( ) ou < > — et chaque glyphe décodé porte un triplet TokenIndex/ItemIndex/ByteOffset pointant vers l'opérande exact, l'élément du tableau TJ et l'unité d'octet qui l'ont produit. Le même interpréteur de glyphes pilote l'API d'extraction décrite dans l'extraction de texte à partir d'un PDF chargé en Delphi ; la recherche conserve simplement la provenance que l'extraction élimine

Chaque correspondance est renvoyée sous la forme d'un enregistrement THPDFTextMatch portant l'index de page, la plage de glyphes inclusive, l'origine X/Y de l'espace utilisateur et la largeur de la correspondance, l'indice du jeton et de l'élément d'origine, ainsi que le texte correspondant lui-même. C'est suffisant pour piloter une superposition de surbrillance, une interface utilisateur de révision ou l'étape de remplacement. Une recherche qui ne trouve rien renvoie un tableau vide plutôt que d'échouer, ce qui simplifie le modèle d'appel

var
  Pdf: THotPDF;
  Matches: THPDFTextMatchArray;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoices-2025.pdf') > 0 then
    begin
      if Pdf.SearchLoadedDocumentText('Acme Corp', False, Matches) then
        for I := 0 to Length(Matches) - 1 do
          WriteLn(Format('page %d at (%.1f, %.1f): "%s"',
            [Matches[I].PageIndex, Matches[I].X, Matches[I].Y,
             Matches[I].Text]));
    end;
  finally
    Pdf.Free;
  end;
end;

Un choix de conception délibéré mérite d'être noté. Lorsque CaseSensitive est à False, la comparaison convertit la casse uniquement pour les caractères ASCII, par conception : la conversion complète de la casse Unicode se comporte différemment selon les chaînes d'outils de Delphi 5 à XE prises en charge par HotPDF, et une API de recherche qui trouve des correspondances différentes selon le compilateur qui a construit votre application est pire qu'une API dotée d'une limite documentée et prévisible. Pour les textes professionnels en alphabet latin — noms, codes, dates — la conversion ASCII couvre les cas pratiques

Remplacer du texte : encodage inverse et épissure chirurgicale

La fonction ReplaceLoadedDocumentText, ajoutée dans HotPDF v2.252.0, réécrit chaque occurrence d'un terme en exécutant le mécanisme de décodage à l'envers. La fonction HPDFEncodeUnicode est l'inverse du décodeur de codes de caractères : elle parcourt la même chaîne de stratégies en sens inverse — recherche dans les sections bfchar et bfrange de /ToUnicode, mappage CID du flux d'encodage, mappages d'identité Type0 et tables prédéfinies WinAnsi et MacRoman — pour reconvertir chaque caractère de remplacement en octets de code de caractère attendus par la police d'origine. Les octets ré-encodés sont ensuite sérialisés dans un littéral de chaîne ou une chaîne hexadécimale bien formés, reflétant les règles d'échappement propres au tokenizer afin qu'un cycle analyse → ré-sérialisation soit stable

L'épissure (splice) elle-même est chirurgicale plutôt que globale. Seule la plage d'octets de code couverte par la correspondance est remplacée dans l'opérande de chaîne ; les octets non correspondants du même opérande, les espaces blancs entre les jetons et chaque opérateur environnant sont préservés textuellement, octet par octet. Remplacer bca dans abcabc donne a + remplacement + bc, et non un opérande détruit. Les remplacements peuvent être plus courts ou plus longs que le terme recherché — le littéral est resérialisé et l'élément /Length du flux est mis à jour — et chaque flux /Contents d'une page multiflux est traité de manière isolée afin que la page reste bien formée

var
  Pdf: THotPDF;
  ReplaceCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract-draft.pdf') > 0 then
    begin
      if Pdf.ReplaceLoadedDocumentText('2025-12-31', '2026-12-31',
        True, ReplaceCount) then
        WriteLn(Format('%d operand rewrites performed', [ReplaceCount]));
      Pdf.SaveLoadedDocument('contract-final.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Notez ce que l'API ne fait pas : elle ne recompose pas la page. Le PDF n'a pas de redistribution (reflow), de sorte qu'un remplacement visuellement plus large que l'original occupera simplement plus d'espace horizontal et pourra chevaucher ce qui était dessiné à sa droite. Les substitutions de longueur identique ou proche — dates, chaînes de version, numéros de pièces, corrections de noms — sont idéales. Une reformulation globale doit être effectuée dans le document source, pas dans le PDF

Pourquoi ne pouvez-vous pas remplacer du texte par des caractères que le sous-ensemble de polices n'a jamais inclus ?

Vous ne pouvez pas remplacer du texte par un caractère que le sous-ensemble de polices intégré n'a jamais inclus, car la séquence d'octets qui permettrait de sélectionner ce caractère n'existe tout simplement pas dans les tables de mappage de la police. Lorsqu'un producteur de PDF intègre une police de sous-ensemble, sa CMap /ToUnicode et ses structures d'encodage ne couvrent que les glyphes réellement utilisés par le document d'origine. HPDFEncodeUnicode ne peut inverser qu'un mappage existant : si le document n'a jamais contenu la lettre E dans cette police, il n'y a pas de code de caractère vers lequel inverser E. C'est une propriété physique du fichier, pas une limite d'une bibliothèque particulière — aucun outil ne peut inventer un mappage de glyphes qui n'a jamais été intégré

HotPDF gère cet échec de manière conservatrice. Si un seul caractère du remplacement ne peut pas être ré-encodé, toute cette occurrence du terme recherché est ignorée — pas d'exception, pas de texte partiel illisible, et l'occurrence n'est tout simplement pas comptée dans ReplaceCount. Conséquence pratique : comparez ReplaceCount au nombre de correspondances d'une recherche préalable, et considérez tout écart comme un signal. Dans l'exemple de date ci-dessus, le chiffre 6 doit apparaître quelque part dans le texte du document avec cette même police pour que la réécriture réussisse — probable dans une facture, mais jamais garanti de manière générale. Lorsque les caractères dont vous avez besoin ne sont pas disponibles et que le but est de supprimer des informations sensibles plutôt que de les reformuler, la suppression réelle du contenu reste le meilleur outil ; voir la rédaction et la restructuration de PDF chargés en Delphi pour cette approche

var
  Matches: THPDFTextMatchArray;
  Expected, Replaced: Integer;
begin
  Pdf.SearchLoadedDocumentText('Acme Corp', True, Matches);
  Expected := Length(Matches);
  Pdf.ReplaceLoadedDocumentText('Acme Corp', 'Apex Corp', True, Replaced);
  if Replaced < Expected then
    WriteLn(Format('%d occurrence(s) skipped: characters missing ' +
      'from the font subset, or match spans multiple operands',
      [Expected - Replaced]));
end;

Quels changements se produisent dans le fichier lors de la sauvegarde ?

Un flux /Contents remplacé est enregistré sans compression. Les flux compressés en FlateDecode sont décompressés pour édition, et lorsque HotPDF écrit les octets reconstruits, il supprime l'entrée /Filter du flux et met à jour /Length au lieu de recompresser. Le PDF résultant est entièrement valide et s'affiche normalement dans les visionneuses courantes ; le compromis est un fichier plus volumineux pour chaque flux édité. Pour un pipeline de traitement par lots qui traite des milliers de documents, prévoyez cette croissance ou exécutez une passe de compression distincte en aval. La manière dont les objets réécrits interagissent avec la structure de référence croisée du document lors de la sauvegarde est un sujet à part entière, traité dans les flux d'objets et les mises à jour incrémentielles dans HotPDF

Tout le reste du fichier est laissé intact. Les flux non modifiés conservent leur compression, les polices et les images ne sont pas réécrites, et l'épissure au niveau de l'opérande signifie que même les flux édités ne diffèrent de l'original qu'à l'endroit où une correspondance a été trouvée. Ce conservatisme est délibéré : plus une bibliothèque réécrit de parties d'un document chargé, plus elle risque d'interférer avec une particularité du producteur qu'elle n'avait pas anticipée

La recherche et le remplacement de texte rejoignent l'extraction, la rédaction et le rendu de pages dans la boîte à outils de documents chargés de HotPDF, le tout piloté par le même interpréteur de flux de contenu et disponible de Delphi 5 aux versions actuelles de RAD Studio, sans dépendance externe. La référence complète de l'API et le téléchargement d'essai se trouvent sur la page produit de HotPDF Component