2 points par GN⁺ 2023-09-05 | 1 commentaires | Partager sur WhatsApp
  • Pour modifier le contenu textuel d’un PDF sans outil PDF commercial, il faut décompresser les flux compressés, remonter l’encodage propre à chaque police et remplacer le texte d’origine et le texte de substitution de la même manière
  • Beaucoup de PDF stockent le vrai texte du document non pas en clair, mais dans des flux compressés et des chaînes ressemblant à de l’hexadécimal, qu’on peut déplier vers une forme plus proche d’un format éditable avec qpdf --qdf --object-streams=disable
  • Les blocs de texte sont composés de commandes PDF comme BT, Tf, Td, Tj, ET, et une valeur comme <002a004800570003003600480057> Tj n’est pas forcément une simple suite de points de code Unicode
  • Si le PDF contient un mapping ToUnicode propre à la police, on peut vérifier avec pdffonts que emb et uni valent yes, puis suivre une référence comme /ToUnicode 19 0 R pour trouver la table d’encodage
  • Cette méthode est difficile à généraliser à tous les fichiers à cause de la complexité du standard PDF, mais sur certains PDF il est possible de retrouver l’encodage d’origine et de remplacer le texte par find-and-replace

Décompresser les flux de texte d’un PDF compressé

  • En ouvrant un PDF dans un éditeur de texte, une partie de la structure peut être lisible, mais le vrai texte du document peut ne pas apparaître
  • Beaucoup de PDF stockent les données textuelles sous forme compressée
  • On peut décompresser les flux de texte avec qpdf pour les inspecter
    • Sur Mac, il existe une formule Homebrew
    • Des outils équivalents sont aussi disponibles sur les distributions Linux
qpdf --qdf --object-streams=disable in.pdf out.pdf
  • Après modification, on peut recompresser les flux
qpdf out-edited.pdf out-recompressed.pdf
  • La commande de recompression a généré une erreur, mais le PDF obtenu restait lisible dans Preview

Trouver le texte dans les commandes PDF

  • Même après décompression, le texte reste contenu dans des blocs de commandes PDF
  • Un bloc de texte de base a la forme suivante
BT
  /Font_0 12 Tf
  288 720 Td
  <002a004800570003003600480057> Tj
ET
  • D’après la PDF Reference, ce bloc effectue les opérations suivantes dans l’ordre
    • démarre un objet texte
    • définit la police et sa taille à utiliser
    • indique la position de départ sur la page
    • dessine les glyphes de la chaîne à cet emplacement
    • termine l’objet texte

Pourquoi le texte n’est pas directement lisible

  • Une valeur comme <002a004800570003003600480057> ressemble à une chaîne hexadécimale, mais ce n’est pas forcément une simple liste de points de code Unicode
  • Le PDF propose de nombreuses façons de spécifier l’encodage, et peut inclure un encodage personnalisé dans le fichier
  • Ces encodages peuvent le plus souvent être mappés vers des points de code Unicode
  • Cette procédure suppose que le PDF en cours contient un encodage embarqué
    • Le traitement des autres cas n’est pas abordé ici

Vérifier la police et le mapping Unicode

  • L’encodage du texte dans un PDF est lié à une police précise
  • pdffonts affiche les polices du PDF ainsi que des informations liées à l’encodage
$ pdffonts sample.pdf
name                                 type              emb sub uni prob object ID
------------------------------------ ----------------- --- --- --- ---- ---------
CLDQZB+TrebuchetMS,Bold              CID TrueType      yes yes yes           9  0
YQBAIZ+TrebuchetMS                   CID TrueType      yes yes yes          10  0
  • Les champs à vérifier ici sont les suivants
    • emb : la police est embarquée dans le PDF
    • uni : un mapping Unicode est disponible
  • Si les deux valent yes, il y a des chances de pouvoir retrouver le mapping nécessaire
  • Un identifiant de police comme /Font_0 dans l’exemple peut, selon les cas, correspondre à un nom de police dans la sortie de pdffonts
    • Dans le cas montré, /Font_0 pointait vers CLDQZB+TrebuchetMS,Bold

Suivre la table de mapping ToUnicode

  • Une fois le nom complet de la police utilisée pour le texte trouvé, il faut rechercher ce nom dans le PDF
  • À un certain endroit, on peut trouver une référence comme celle-ci
/ToUnicode 19 0 R
  • Cette valeur pointe vers l’ID d’objet de la table d’encodage
  • En recherchant ensuite 19 0 obj, on a pu retrouver cette table

Remplacer le texte à l’aide de la table d’encodage

  • La partie essentielle de la table de mapping peut ressembler à ceci
38 beginbfrange^M
<0036><0036><0053>^M
<0057><0057><0074>^M
<0044><0044><0061>^M
<0048><0048><0065>^M
<0050><0050><006D>^M
...
  • Cet exemple mappe des plages de points d’encodage personnalisés vers des points Unicode
    • Ici, chaque plage ne contient qu’un seul caractère
    • 0036 est mappé vers le point Unicode 0053
  • Si le format de la table diffère, on peut consulter le tutoriel sur les fichiers de mapping ToUnicode
  • Pour automatiser la conversion, une table a été transformée en dictionnaire Python, puis de simples fonctions d’encodage/décodage ont été écrites
  • Le flux final de remplacement se déroule ainsi
    • identifier la valeur en encodage personnalisé du texte existant à remplacer
    • encoder le texte de remplacement dans le même encodage personnalisé
    • remplacer dans le PDF la valeur existante par find-and-replace

1 commentaires

 
GN⁺ 2023-09-05
Avis de Hacker News
  • La spécification PDF est assez rude. Ma curiosité préférée du moment, c’est qu’elle prend en charge tous les modes de fusion de calques de Photoshop lors du rendu d’éléments superposés.
    Ma deuxième préférée, c’est que du contenu ajouté à la fin peut modifier du contenu précédent, si bien qu’il faut examiner de manière forensique toutes les différentes versions représentées dans un même fichier.
    C’est aussi un exemple de la vacuité des DRM. La spécification prévoit un chiffrement par mot de passe, avec des mots de passe « propriétaire » et « utilisateur » distincts, ainsi que des champs de bits du type « impression interdite » ou « copie de texte interdite ». Mais pour lire le document, il faut bien le déchiffrer au final ; donc, si l’on ouvre un PDF chiffré avec le mot de passe « utilisateur » dans un outil non conforme et qu’on l’enregistre en version non chiffrée, on obtient un équivalent modifiable.
    [1] Section “More than just transparency” de https://blog.adobe.com/en/publish/2022/01/31/20-years-of-tra...
    [2] https://blog.didierstevens.com/2008/05/07/solving-a-little-p...
    [3] Page 61 de https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandard...
    [4] Par exemple, un script utilisant la bibliothèque pypdf

    • À l’origine, c’était un format propriétaire, pas largement ouvert à tout le monde, et il a été créé à une époque où le chiffrement était strictement contrôlé par les lois sur l’exportation ; dans ce contexte, cette forme de sécurité par l’obscurité était très courante.
      L’un des tutoriels de cracking célèbres de l’époque consistait à patcher la fonction qui vérifiait les permissions pour retirer les DRM du lecteur officiel.
    • Méthode : https://www.cyberciti.biz/faq/removing-password-from-pdf-on-...
    • Les modes de fusion pris en charge ne sont-ils pas simplement les modes de composition Porter-Duff ? Cela peut sembler excessif, mais ça correspond très bien à ce que proposent d’autres pipelines de rendu, et ça aide pas mal à réduire la charge de travail pour générer des PDF.
    • Quand on creuse les champs de permissions, on tombe dans un terrier de lapin fait de générateurs de PDF qui ne respectent pas la spécification, et de contournements présents ou non selon les lecteurs ou bibliothèques PDF.
    • Pour être juste, si l’on veut empêcher la copie de texte, le plus simple est de supprimer le mappage ToUnicode de la police. Les gens doivent alors le reconstituer manuellement.
  • Ce sujet revient périodiquement : la plupart des gens voient le PDF comme un format binaire difficile à percer, alors qu’en réalité ce n’est pas le cas.
    Un PDF est un graphe d’objets de différents types, et ces types sont bien décrits dans la spécification officielle. J’ai le goût douloureux de la lire pour le plaisir.
    Ce que je recommande toujours, c’est de faire comme l’auteur et de convertir le PDF en une version sans données compressées. Mon outil préféré est mutool : mutool clean -d in.pdf out.pdf suffit. Ensuite, en fouillant, on se rend compte avec surprise qu’on peut suivre bien plus de choses qu’on ne l’imaginait.
    L’article saute l’étape qui consiste à regarder l’objet page et à vérifier les ressources. C’est là que le nom de police utilisé dans le flux de contenu est relié au véritable sous-objet.
    Un autre point important qui manque : la plupart des polices sont intégrées dans le PDF sous forme de sous-ensembles. Seuls les glyphes nécessaires sont conservés dans la police. Le réencodage semble généralement se produire à ce niveau, et ToUnicode est conservé pour permettre la copie de texte ou la recherche dans le PDF. C’est un plus pour l’utilisateur et, d’après mon expérience, c’est généralement présent et plutôt correct.

    • Si on dit qu’on lit ça « pour le plaisir », on est plutôt masochiste que sadique. Si on avait participé à la rédaction de la spécification, ou forcé quelqu’un d’autre à la lire, là on aurait été sadique.
    • C’est dommage qu’Adobe ait conçu un format si pénible à manipuler que les gens soient surpris quand quelqu’un réussit ce qui devrait être une opération de base.
      Dès le départ, la philosophie consistant à créer un format en lecture seule était mauvaise. La première fonctionnalité que les gens allaient demander était pourtant évidente, non ?
    • Si l’on prend plaisir à recevoir de la douleur, on est masochiste ; si l’on aime infliger de la douleur aux autres, on est sadique. Comme il semble plutôt vouloir aider les gens, c’est plus proche du premier cas. Merci pour le conseil sur mutool.
    • Pour l’analyse de PDF, je dépends beaucoup d’Amazon Textract.
      Je me demande s’il existe d’autres pistes pour bien gérer cela nativement, sans fournisseur cloud. En particulier pour les tableaux.
  • On peut faire comme ceci :
    convertir en PostScript a.ps avec pdf2ps a.pdf
    modifier directement le PostScript avec vim a.ps
    reconvertir en PDF avec ps2pdf a.ps
    Les PDF complexes qui contiennent par exemple du JavaScript intégré ou des animations peuvent ne plus fonctionner correctement après cet aller-retour. Mais pour un document ordinaire, cela marche plutôt bien. Supprimer un filigrane, changer des mots et des nombres est facile ; ajuster les espacements est plus difficile. Bien sûr, il faut connaître un peu PostScript.

    • Dans une ancienne entreprise de traitement du courrier où je travaillais, une partie du prépresse se faisait presque de cette manière. En général, on produisait les documents à imprimer avec les bons outils, mais parfois un client pensait s’y connaître mieux que nous et envoyait un PDF ; la plupart du temps, cela demandait plus de travail et augmentait le risque d’erreurs.
      Même lorsque la sortie était correcte, il fallait réordonner les pages, appliquer les codes-barres pour les machines de mise sous pli et le tri postal, puis générer des rapports. En général, on récupérait les adresses en extrayant le texte des pages avec Perl et d’autres outils ; le PS était globalement beaucoup plus facile à gérer que le PDF, mais il y avait aussi des cas très fragiles, par exemple quand des PDF « sécurisés » n’avaient pas de correspondance correcte des glyphes.
      Dans le pire des cas, les clients envoyaient des PDF constitués de documents uniques ; une fois fusionnés, les polices sous-ensembles explosaient dans la sortie, remplissaient la mémoire de l’imprimante et la faisaient planter. Quand j’ai quitté ce poste, je ne crois pas qu’il existait d’outil utile pour unifier et fusionner des polices sous-ensembles issues de la même police d’origine. Cela devrait être possible et aurait été très utile, mais je n’avais ni le temps ni les connaissances pour m’y pencher.
    • Si l’on peut mettre du JavaScript et des animations dans un PDF, qu’est-ce qui empêche d’y créer un frontend ? Je me demande où se situent les limites de ce qu’on peut faire avec un PDF.
      Honnêtement, j’ai l’impression que les seuls à tirer profit de la complexité du PDF sont les auteurs de malwares.
  • Il me semble qu’un point important manque. À la fin d’un PDF se trouve une table qui stocke les décalages en octets de plusieurs objets dans le fichier : la table de références croisées.
    Si l’on modifie le contenu du fichier, ces offsets changent généralement, ce qui casse le fichier. L’article semblait surtout s’intéresser au remplacement d’un chiffre par un autre, ce qui a pu laisser les positions inchangées.
    Mais en général, si l’on ajoute, supprime ou modifie quelque chose au milieu du fichier, il faut recalculer la table xref ; utiliser une bibliothèque devient donc beaucoup plus simple que d’éditer le texte à la main.

    • C’est pour cela qu’on décode avec qpdf puis qu’on réencode ensuite. qpdf s’occupe alors de cette partie.
      qpdf semble reconstruire la structure PDF d’origine et essayer de conserver les numéros d’objets, mais les offsets sont entièrement recalculés.
    • À mon avis, c’est la partie la plus étrange de la spécification PDF. Elle mélange binaire et texte, et les offsets en octets sont indiqués sous forme de texte.
      Si les auteurs en parlaient un jour, j’aimerais lire pourquoi le format a été conçu ainsi. J’ai l’impression qu’il était au départ pensé comme un format entièrement textuel, puis qu’ils ont ensuite jugé le binaire plus efficace. Cela dit, l’obligation de donner une longueur fixe aux entrées de la table xref s’accorde assez bizarrement avec cette hypothèse.
    • D’après mon expérience, le plus simple est de casser purement et simplement la table xref, puis de lancer quelque chose comme mutool clean pour la réparer. Comme elle peut être entièrement déduite du contenu, c’est sans danger.
    • C’est assez comparable à l’édition de binaires compilés.
      Tant qu’on préserve la longueur, on peut modifier le binaire comme on veut.
      Un logiciel que j’utilisais autrefois s’authentifiait auprès d’un serveur, mais tout le traitement se faisait côté client. Le client exécutait directement du SQL sur le serveur, et le serveur vérifiait seulement que ce client ne dépassait pas le nombre de licences achetées.
      On pouvait le passer dans un désassembleur, trouver la partie qui effectuait le contrôle, la remplacer directement par un JMP, puis remplir l’espace restant avec des NOP.
  • Il me semble qu’une étape importante manque quand on utilise le mode --qdf de qpdf. Une fois l’édition terminée, il faut passer le fichier dans l’utilitaire fix-pdf pour recalculer tous les offsets d’objets et reconstruire la table de références croisées à la fin du fichier. Sauf si l’on n’a fait que remplacer en place, sans ajouter ni supprimer d’octets.
    Voici mon top 3 des faits amusants sur les PDF :

    1. Les documents PDF sont généralement des fichiers binaires 8 bits, mais avec le filtre ASCII85, on peut créer un PDF en texte brut UTF-8 valide, images comprises.
    2. PDF autorise énormément de fonctionnalités étranges : objets 3D, JavaScript, vidéos dans des objets Flash intégrés, annotations invisibles, etc. PDF/A est un sous-ensemble bien plus raisonnable et sûr.
    3. La spécification PDF permet de rédiger des widgets, par exemple des contrôles de formulaire, en texte enrichi, qui est un sous-ensemble de XHTML et de CSS. En revanche, cette fonctionnalité est très rarement prise en charge en dehors de l’Adobe Reader officiel.
      [0] Exemple : https://lab6.com/2
    • Une fois l’édition terminée, il suffit de le passer dans qpdf sans paramètres, comme expliqué au début de l’article. Cela recomprime les données et recrée aussi la table xref. Aucun autre outil n’est nécessaire.
  • Si vous voulez continuer votre exploration du PDF et en apprendre davantage, vous pouvez lire ce document récent où j’en dissèque la structure :
    https://pdfsyntax.dev/introduction_pdf_syntax.html

  • Ce que les gens oublient souvent à propos des PDF, c’est qu’à certains égards, ils sont plus proches d’un format d’image que d’un document Word. Dans l’édition de documents, les documents Word, les PDF et les images sont comme les projets DAW, le MIDI et le MP3 en musique, ou comme le code source Java, le bytecode JVM et le pur code machine x86 en logiciel.
    L’objectif principal d’un fichier PDF est d’indiquer de façon parfaitement non ambiguë quoi afficher ou imprimer, avec beaucoup moins d’octets qu’une véritable image. Il exploite le fait que l’auteur du document connaît les motifs de sa structure, de sorte qu’une représentation correcte se compresse bien mieux qu’avec de vrais algorithmes de compression d’image. Par exemple, si l’on a accès à la police réelle, il vaut mieux dire « place ces caractères à ces coordonnées avec cet espacement » plutôt que d’inclure chaque occurrence de lettre comme une partie d’image et d’espérer que l’algorithme de compression réduise les répétitions. Savoir quel caractère appartient à quel mot, voire quel point de code Unicode correspond à quel glyphe de police, n’a fondamentalement pas d’importance si le seul objectif est de transmettre efficacement l’image du document.
    Pour un document modifiable, la structure sémantique du contenu devient bien plus importante que sa simple représentation. Il importe de savoir si une rupture précise dans le texte correspond à plusieurs espaces, à la colonne suivante d’un tableau, ou à une mise en page étrange due à une image. Il importe aussi de savoir si le texte en bas de chaque page a été inséré plusieurs fois par l’auteur, ou saisi une seule fois puis défini comme pied de page. Si l’ajout d’un nouveau paragraphe doit modifier la mise en page, il importe de savoir que le dernier paragraphe de cette page est une note de bas de page et ne doit pas passer à la page suivante. Ce qui compte, c’est qu’un titre de section déplacé sur une autre page déclenche la mise à jour automatique de la table des matières, et non qu’il ne soit qu’un texte saisi manuellement par l’auteur.
    Du point de vue d’une imprimante ou d’un écran, tout cela n’a aucune importance. Ils n’ont qu’à imprimer ou afficher ce qu’on leur demande. Dans un PDF, les notes de bas de page, les titres de section, les pieds de page et la table des matières n’ont pas besoin d’être spéciaux ; ils peuvent être simplement du texte mis en forme sans signification particulière. C’est pourquoi, dès qu’on utilise un PDF à d’autres fins que l’affichage ou l’impression, on ne peut pas atteindre une précision de 100 %. Bien sûr, des efforts existent pour y remédier, et les programmes générant des PDF peuvent y insérer librement les métadonnées qu’ils souhaitent, mais ce n’est pas obligatoire.
    Ce n’est peut-être pas le modèle mental que les auteurs de PDF avaient en tête, mais c’est un point de vue utile pour comprendre pourquoi le PDF a cette forme.

    • L’objectif initial du PDF était de créer une copie fidèle imprimable et portable. Du vrai WYSIWYG. Un fichier PDF devait avoir le même rendu, qu’on l’envoie à une imprimante laser, à une Linotype ou à un écran.
      Sorti sur une imprimante PostScript, il avait exactement le même aspect, et avec des polices Type 1, c’était parfois encore mieux.
  • Les personnes qui veulent faire cela passent à côté de l’essence du PDF. Le PDF est un format de description de page : il ne représente pas la structure du document, mais seulement les traces visibles sur la page.
    Il ne faut pas essayer de modifier le PDF, mais modifier le document source qui l’a généré.

    • Quand les gens arrêteront de m’envoyer des PDF que je dois modifier, j’arrêterai moi aussi d’éditer des PDF.
      D’une manière ou d’une autre, envoyer simplement le document conçu pour être modifié est devenu quelque chose de « peu professionnel », et voilà où on en arrive.
    • Le PDF prend aussi en charge l’inclusion d’informations de structure logique du document. C’est ce qu’on appelle un PDF balisé, ou Tagged PDF.
      C’est optionnel, mais recommandé pour l’accessibilité. PDF/UA en est un exemple. Voir les sections 14.7–14.8 de [1].
      [1] https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandard...
    • Ici, dire « il ne faut pas » n’a pas grand sens. Dans la réalité, il existe énormément de situations où l’on vous demande de modifier un PDF, d’une manière ou d’une autre.
    • Une difficulté est d’indiquer les corrections qui doivent être reportées dans le document source. On reçoit une épreuve du maquettiste, et il faut marquer ce qu’il doit corriger.
      On ne peut pas modifier directement le texte du PDF, car le maquettiste ne verrait pas cette modification. Les outils d’annotation d’Acrobat ne sont pas horribles, mais ils ne valent pas l’époque du papier et du crayon rouge. Sauf si l’on utilise l’outil « pencil » d’Acrobat. J’aimerais que cela s’améliore.
    • Si c’est un « format de description de page » et qu’il ne représente pas la structure du document mais seulement ce qui est affiché sur la page, n’aurait-il pas fallu l’appeler Page Description Format plutôt que Portable Document Format ?
  • Il y a 20 ans, je travaillais dans un journal comme responsable des plaques d’impression. Nous avions deux « imprimantes » Kodak à un million de dollars pour les plaques, mais le mot imprimante n’est pas vraiment exact. Si je me souviens bien, l’émulsion de la plaque était exposée aux UV puis dissoute dans un bain chimique.
    Le matériel Kodak tombait souvent en panne, et mon chef ouvrait directement les fichiers PostScript ou EPS pour corriger les en-têtes ou les passages erronés envoyés par le logiciel de mise en page. Ensuite, tout refonctionnait. À titre d’anecdote, notre énorme rotative offset allemande tournait sous Linux.
    Je crois que mon chef s’appelait Bill. C’était quelqu’un de bien, qui m’avait emmené à un concert de Sigur Ros quand j’avais 17 ans. Voilà déjà deux histoires sans rapport avec le PDF.

  • Très bon article. Ces cinq dernières années environ, en créant DocSpring [1], j’ai beaucoup lu la spécification PDF, et j’ai pourtant encore l’impression de n’en avoir qu’effleuré la surface. qpdf est un excellent outil, et parmi mes autres outils préférés il y a RUPS [2], qui permet d’explorer en profondeur la structure d’un PDF.
    [1] https://docspring.com
    [2] https://github.com/itext/i7j-rups