Un seul caractère erroné dans un numéro de facture, et le seul primitive d’édition disponible réécrit toute la séquence de texte. PDF Library for Delphi comble cette lacune : GetTextBlockCharContentLocation relie chaque position UTF-16 extraite à l’instruction du flux de contenu, à l’opérande et à la plage d’octets encodés qui l’ont produite, tandis que ReplaceTextBlockCharSourceBytes ne réécrit que cette plage. L’extraction de texte abandonne normalement tout ce dont vous auriez besoin pour cela. Vous obtenez l’Unicode, les largeurs et la géométrie, mais la provenance disparaît : le caractère en position 7 du bloc 3 n’est plus qu’un caractère. Quel flux l’a produit, quelle instruction, quel opérande, quel octet dans cet opérande : tout a disparu. Toute stratégie d’édition ponctuelle construite là-dessus doit deviner, généralement en recherchant une sous-chaîne dans le contenu décodé et en espérant qu’elle n’apparaisse qu’une seule fois. Sur une vraie page, ce n’est pas le cas
Pourquoi réécrire toute une séquence de texte abîme-t-il la page ?
Parce que la séquence n’est pas seulement du texte. Les opérateurs d’affichage du texte de l’ISO 32000-1 §9.4.3 comprennent TJ, dont l’opérande est un tableau qui entrelace des chaînes et des ajustements numériques ; ces nombres assurent la composition. Une ligne disposée comme [(AB) -120 (CD)] TJ contient un crénage de 120 millièmes de cadratin entre les deux chaînes. Émettez un nouveau Tj avec le texte concaténé : le crénage disparaît, la ligne se redispose d’un rien et, sur un formulaire, la valeur sort de sa case. La même objection vaut pour la police : les octets de l’opérande sont des codes dans l’encodage sélectionné par Tf, pas de l’Unicode, et pour une police composite ils peuvent être des CID de deux octets sans rapport avec le caractère extrait. Régénérez la séquence et vous devez être exact sur l’encodage de la police, sa table /ToUnicode et sa couverture de glyphes. L’édition ponctuelle évite tout cela en ne quittant jamais le domaine des octets
Que renvoie GetTextBlockCharContentLocation ?
La méthode résout un caractère en un enregistrement de neuf champs, chaque champ étant une adresse plutôt qu’une valeur. ContentLayer est l’index, commençant à 1, dans le tableau /Contents de la page, ou 0 lorsque le caractère provient d’un contenu imbriqué. StreamObjectNumber et StreamGeneration identifient le flux conteneur. InstructionIndex est la position commençant à 0 dans le programme de contenu décodé, OperandIndex l’opérande de chaîne de texte et ArrayElementIndex l’élément à l’intérieur d’un tableau TJ, ou -1 pour un opérande de chaîne directe. SourceByteOffset et SourceByteLength désignent ensuite la plage d’octets à l’intérieur de cette chaîne décodée
Var
Lib: TPDFlib;
ListID, Block, CharPos: Integer;
ContentLayer, StreamObjectNumber, StreamGeneration: Integer;
InstructionIndex, OperandIndex, ArrayElementIndex: Integer;
SourceByteOffset, SourceByteLength, Flags: Integer;
Begin
Lib:= TPDFlib.Create;
Try
Lib.LoadFromFile('invoice.pdf', '');
Lib.SelectPage(1);
ListID:= Lib.ExtractPageTextBlocks(3);
Try
// Block et CharPos proviennent de votre propre parcours de GetTextBlockText
If Lib.GetTextBlockCharContentLocation(ListID, Block, CharPos,
ContentLayer, StreamObjectNumber, StreamGeneration,
InstructionIndex, OperandIndex, ArrayElementIndex,
SourceByteOffset, SourceByteLength, Flags)= 1 Then
Begin
// ContentLayer = 0 signifie que le glyphe se trouve dans un Form XObject imbriqué
// ArrayElementIndex = -1 signifie un opérande Tj simple, pas un tableau TJ
End;
Finally
Lib.ReleaseTextBlocks(ListID);
End;
Finally
Lib.Free;
End;
End;
La recherche ne coûte rien au moment de la requête. Pendant que le renderer décode chaque couche de contenu, il enregistre les intervalles logiques qu’il parcourt ; une requête de position est donc une recherche binaire dans une liste d’intervalles ordonnée, plutôt qu’un parcours linéaire de chaque intervalle de contenu pour chaque caractère. Rien n’est reparsé lorsque vous posez la question : la map a été construite pendant la passe d’extraction déjà payée. Si vous énumérez déjà des résultats avec la recherche de texte PDF qui renvoie les coordonnées des résultats, ajouter un emplacement de contenu par résultat coûte presque rien
Modifier des octets, pas de l’Unicode
ReplaceTextBlockCharSourceBytes prend un AnsiString d’octets de remplacement bruts dans l’encodage de police PDF actif. C’est tout le principe, et il est délibéré. Rien n’est transcodé, rien n’est réencodé et rien n’est deviné sur la police. La bibliothèque insère vos octets à la place de la plage nommée de la chaîne cible et réémet la couche de contenu qui la contient. Les chaînes voisines du même tableau TJ et les crénages numériques entre elles restent identiques octet par octet. Reprenons la composition ci-dessus : localiser le B dans [(AB) -120 (CD)] TJ donne ArrayElementIndex 0, SourceByteOffset 1 et SourceByteLength 1. Remplacez-le par Z et le contenu émis contient (AZ), toujours suivi de -120 et de (CD), tous deux inchangés. La suite de régression l’affirme exactement, car « le crénage a été préservé » est le genre de promesse qui cesse discrètement d’être vraie
Function EditableHere(Flags: Integer): Boolean;
Begin
Result:= ((Flags and PDF_TEXT_CHAR_CONTENT_LOCATION_VALID)<> 0)and
((Flags and (PDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED or
PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT or
PDF_TEXT_CHAR_CONTENT_LOCATION_NESTED or
PDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER or
PDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED))= 0);
End;
// ...
If EditableHere(Flags) Then
Begin
If Lib.ReplaceTextBlockCharSourceBytes(ListID, Block, CharPos, 'Z')= 1 Then
Begin
// Tous les emplacements de l’ancienne liste sont désormais obsolètes. Réextraire.
Lib.ReleaseTextBlocks(ListID);
ListID:= Lib.ExtractPageTextBlocks(3);
End
Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_STALE Then
// La couche a changé depuis l’extraction
Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_READ_ONLY Then
// Un flag que nous n’avons pas vérifié, ou ajouté par une version ultérieure
End;
Deux détails opérationnels méritent d’être retenus. L’appel bascule temporairement vers la page dont la liste de texte a été extraite et restaure la page précédemment sélectionnée en cas de succès comme d’échec ; il ne déplace donc pas silencieusement votre curseur. En cas de succès, il efface aussi les instantanés des éléments de page, ce qui invalide les handles que vous conserviez d’une précédente passe d’énumération
Quels caractères ne peuvent pas être modifiés ?
Six catégories, et la bibliothèque nomme chacune d’elles dans le masque de bits Flags au lieu d’échouer vaguement. Cela compte davantage que le chemin nominal, car les cas non mappables sont fréquents dans les vrais documents et chacun a une raison différente
PDF_TEXT_CHAR_CONTENT_LOCATION_LIGATURE: plusieurs positions UTF-16 extraites proviennent d’un seul glyphe source. Une entrée/ToUnicodequi mappe un code versfivous donne deux caractères partageant une plage d’octets ; traitez-les donc comme un seul glyphe source et modifiez la plage une seule foisPDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED: le caractère a été synthétisé pendant la mise en page. Les espaces de mots inférés sont le cas habituel et ne possèdent aucun octet source ;SourceByteOffsetvaut donc -1 etSourceByteLength0PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT: le texte lu provient d’un remplacement/ActualText. Il n’existe pas de correspondance inverse unique entre la chaîne substituée et les octets source ; l’emplacement est donc uniquement diagnostiquePDF_TEXT_CHAR_CONTENT_LOCATION_NESTED: le glyphe se trouve dans un Form XObject. Les octets sont adressables, mais le Form peut être dessiné par plusieurs pages ; le modifier par l’API de haut niveau serait donc une modification que vous n’avez pas demandéePDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED: l’opérande était une chaîne hexadécimale contenant une marque d’ordre des octets UTF-16BE, que le chemin d’extraction existant décode avant le mappage de police. Les offsets du résultat décodé ne désignent plus les octets originaux, et le flag valid est donc effacéPDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER: l’opérande de chaîne et son opérateur d’affichage du texte se trouvent dans deux flux différents
Ce dernier cas mérite une phrase à part, car les ingénieurs supposent régulièrement qu’il ne peut pas arriver. L’ISO 32000-1 §7.8.2 indique que les flux du tableau /Contents d’une page sont concaténés et que leur séparation doit seulement tomber sur une limite lexicale. Ainsi, BT /F1 16 Tf 220 340 Td (CrossLayer) dans un flux et Tj ET dans le suivant constitue une page parfaitement légale. Le mappage conserve la position diagnostique mais la marque en lecture seule, parce que l’index d’instruction de l’opérateur appartient à une autre couche que les octets de l’opérande ; utiliser l’un pour adresser l’autre corromprait le fichier
Comment la bibliothèque sait-elle que la map est toujours valide ?
Avec des empreintes, vérifiées immédiatement avant l’écriture. Chaque liste d’extraction enregistre la page source et, pour chaque couche de contenu, la longueur de la couche ainsi que deux hash glissants indépendants : un hash FNV-1a et un hash XOR de style DJB2. Avant que ReplaceTextBlockCharSourceBytes n’analyse quoi que ce soit, il relit la couche cible et compare les trois valeurs. Toute modification d’octet, n’importe où dans cette couche, renvoie PDFLIB_ERROR_TEXT_LOCATION_STALE et l’écriture n’a pas lieu. C’est volontairement conservateur : le contrôle se fait par couche, pas par instruction, donc une modification sans rapport ailleurs dans le même flux de contenu invalide également votre emplacement. C’est le bon compromis : un offset dans un flux décalé d’un seul octet n’est pas une approximation, c’est une corruption silencieuse. La même discipline gouverne le reste de la surface d’édition, notamment le tracker d’état du flux de contenu pour CTM et clipping. Après tout remplacement réussi, abandonnez la liste et extrayez de nouveau
Mappage en lecture seule par Direct Access
DAGetTextBlockCharContentLocation vous fournit le même enregistrement pour une page ouverte par le chemin Direct Access, avec le même vocabulaire de flags. C’est uniquement diagnostique, par construction : ReplaceTextBlockCharSourceBytes agit sur le document éditable sélectionné et Direct Access est un chemin de lecture. Les données de localisation restent présentes dans la liste de blocs de texte après la fermeture du handle de fichier, ce qui les rend utilisables pour un audit hors ligne
FileHandle:= Lib.DAOpenFileReadOnly('audit.pdf', '');
Try
PageRef:= Lib.DAFindPage(FileHandle, 1);
DirectList:= Lib.DAExtractPageTextBlocks(FileHandle, PageRef, 3);
Try
Lib.DAGetTextBlockCharContentLocation(DirectList, Block, 1,
ContentLayer, StreamObjectNumber, StreamGeneration,
InstructionIndex, OperandIndex, ArrayElementIndex,
SourceByteOffset, SourceByteLength, Flags);
// Les emplacements restent lisibles après DACloseFile
Finally
Lib.DAReleaseTextBlocks(DirectList);
End;
Finally
Lib.DACloseFile(FileHandle);
End;
Utilisez-le pour répondre à des questions, pas pour modifier. Quelles pages contiennent du texte que vous ne pourrez jamais éditer sur place ? Quelle part de ce corpus arrive avec des remplacements /ActualText ? La sortie de quel fournisseur répartit les opérateurs sur plusieurs couches de contenu ? Ces requêtes sont peu coûteuses dès que chaque caractère possède une adresse, et il vaut la peine de les exécuter avant de vous engager dans un pipeline de correction
Où s’arrête l’édition ponctuelle
L’édition ponctuelle est un scalpel, pas un moteur de texte. Elle modifie les octets sur place, si bien qu’un texte de remplacement plus large ou plus étroit que l’original ne redisposera pas la ligne, ne fera pas de retour automatique et ne mettra pas à jour les crénages voisins. Remplacer un chiffre par un autre dans un champ à chasse fixe convient bien. Retaper un paragraphe, non. Et ce n’est absolument pas un outil de sécurité : écraser les octets des glyphes laisse les octets originaux récupérables dans l’historique des révisions du fichier ; tout ce qui exige de la confidentialité relève donc d’une vraie biffure qui supprime le contenu au lieu de le masquer. En échange de ces limites, vous obtenez de la transparence. Chaque caractère possède soit une adresse d’octet exploitable, soit un flag nommé qui explique pourquoi ce n’est pas le cas, et le contrôle par empreinte transforme une map obsolète en erreur franche plutôt qu’en page corrompue. Le mappage caractère-vers-octets-du-contenu et le remplacement sur place des octets source font partie de la surface d’extraction de texte et d’édition du contenu de PDF Library for Delphi, la bibliothèque PDF native Object Pascal pour Delphi, C++Builder et Lazarus