U+F0xx), qu’on rasterise pour le voir, puis qu’on identifie en caractère Unicode.Extraire le texte d’un PDF passe pour un problème résolu : on ouvre le fichier, on demande le texte, on l’obtient. Jusqu’au jour où l’on regarde le résultat de près et qu’on trouve, à la place du symbole copyright, un petit carré — ou rien du tout. Le diamètre « Ø » a disparu, les puces des listes aussi, et un mystérieux « 8 » s’est glissé là où l’on attendait un exposant. Ce billet raconte pourquoi ces caractères se perdent, et comment les récupérer proprement — c’est-à-dire sans deviner au jugé. Il reste au niveau des principes : rien ici n’est propre à un document ou à un outil particulier.
Le caractère qui n’existe pas
Unicode réserve une plage, la zone à usage privé (Private Use Area, ou PUA : U+E000 à U+F8FF), où aucun caractère n’a de sens officiel. C’est un terrain vague : chacun peut y ranger ce qu’il veut, à condition de fournir la légende. Or de vieilles polices symboliques — les célèbres Symbol et Wingdings — y logent leurs glyphes, typiquement autour de U+F0xx (l’octet d’origine décalé de 0xF000).
Quand l’extracteur rencontre le symbole copyright de la police Symbol, il ne lit pas « © » : il lit le code U+F0E3. Ce code ne désigne rien dans l’absolu ; aucune police normale n’a de dessin à cette adresse. Résultat : un carré, ou le vide. Le caractère n’est pas « abimé » — il n’a simplement jamais été traduit en un caractère standard.
U+E000–U+F8FF, vous avez affaire à des glyphes privés. Un simple filtre 0xE000 <= ord(c) <= 0xF8FF les débusque tous.
La police est la clé
Première tentation : bâtir une table « U+F0E3 = © » et passer à la suite. Mauvaise idée, car le même code ne veut rien dire tant qu’on ignore dans quelle police il apparaît. Le code U+F0FB désigne une croix en Wingdings ; le même octet en Symbol pointe vers un tout autre dessin. Sans le nom de la police, on devine ; avec lui, on sait.
Bonne nouvelle : ce nom est disponible dès l’extraction. Les bibliothèques de lecture de PDF exposent, pour chaque fragment de texte, la police qui l’a produit. C’est cette information — SymbolMT, Wingdings-Regular… — qui débloque tout le reste. Elle désigne la table de décodage à appliquer.
Avant tout : regarder la table ToUnicode
Un PDF bien construit peut embarquer, avec sa police, une table ToUnicode qui donne directement la correspondance entre les codes utilisés dans le fichier et les caractères Unicode. Lorsqu’elle est présente et correcte, c’est la source à privilégier : elle exprime l’intention du producteur du PDF et évite d’avoir à reconstruire le sens à partir du dessin ou du nom de la police.
Le problème raconté ici apparaît justement quand cette information est absente, incomplète ou erronée. L’extracteur retombe alors sur l’encodage de la police et peut restituer un code privé du type U+F0xx. La police et ses tables deviennent alors notre meilleur indice pour retrouver le caractère standard.
ToUnicode exploitable existe, on la prend d’abord. Sinon, on revient au mapping de la police. Et si les deux sont douteux, on conserve le contexte et l’image du glyphe pour validation humaine.
Deux étages pour une police Adobe
Pour une police de la famille Adobe (Symbol en fait partie), la traduction se fait en deux temps :
- code → nom de glyphe. L’encodage de la police dit que, chez Symbol, l’octet
0xE3s’appellecopyrightserifet0xBBs’appelleapproxequal. Cet encodage se lit dans les métriques de la police (les fichiers.afm). - nom de glyphe → Unicode. L’Adobe Glyph List (AGL) dit que
copyrightserifvautU+00A9(©) etapproxequalvautU+2248(≈).
L’AGL, souvent citée comme la référence, ne couvre en réalité que le second étage. Sans l’encodage de la police pour le premier, elle ne sert à rien : on aurait un nom sans savoir de quel code il vient. Les deux tables sont indispensables, et complémentaires.
Pour le cas standard d’Adobe Symbol, Unicode publie aussi une table directe symbol.txt qui associe l’encodage Symbol à Unicode. Elle constitue un excellent raccourci de production et une référence croisée ; je conserve néanmoins l’AFM et l’AGL, car le chemin code → nom → Unicode reste beaucoup plus parlant pour diagnostiquer un cas litigieux.
def resoudre_symbol(octet, afm_code_vers_nom, agl_nom_vers_unicode):
nom = afm_code_vers_nom.get(octet) # 0xBB -> "approxequal"
if not nom:
return None
return agl_nom_vers_unicode.get(nom) # "approxequal" -> 0x2248 (≈)
Wingdings, l’orpheline
Wingdings casse ce bel édifice : c’est une police Microsoft, absente de l’AGL. Ses noms de glyphes n’y figurent pas, donc le second étage ne se traite pas de la même manière. Pour elle, on utilise une table directe « code → Unicode », conservée dans le référentiel avec sa provenance. Dans notre table Wingdings, l’octet 0xFB correspond ainsi à U+2717 (✗). Point essentiel : cela ne veut jamais dire que U+F0FB, pris isolément, « signifie » ✗ ; cette conclusion n’est valable que parce que la police identifiée est Wingdings.
Gérer les ressources externes
Ces tables ne sont pas à nous : l’AGL et l’encodage Symbol viennent d’Adobe, le mapping Wingdings de Microsoft. La tentation est de les recopier à la main dans le code — et c’est exactement ce qu’il ne faut pas faire : une valeur recopiée de mémoire est une erreur en puissance (j’en ai fait une, j’y reviens). La bonne pratique est d’en conserver un miroir local, versionné avec le projet :
- Reproductibilité. Le résultat ne dépend pas de ce qui est installé sur la machine ni d’un accès réseau au moment où l’on exécute : la source est là, figée, traçable.
- Fonctionnement hors-ligne. Un traitement par lot ne devrait jamais dépendre de la disponibilité d’un site distant.
- Repli explicite. Si le miroir est absent, l’outil retombe sur une petite table codée en dur et le dit, plutôt que d’échouer en silence.
Concrètement, un dossier dédié regroupe la liste des noms de glyphes (AGL), les métriques de la police symbolique (encodage), les tables directes Adobe Symbol et Zapf Dingbats publiées par Unicode, la table Wingdings, et un petit fichier d’overrides maison — on y vient. Le programme lit ce dossier au démarrage ; le message d’exécution indique quelle source a servi (« miroir » ou « repli »), ce qui évite les mauvaises surprises silencieuses.
copyrightsans peut donc reboucler sur un code PUA. Il faut une courte table de normalisation qui ramène ces variantes vers le caractère standard (© = U+00A9).
L’œil garde le dernier mot
Une table, aussi officielle soit-elle, reste un défaut, pas une vérité. Deux polices peuvent porter le même nom mais un dessin différent ; un document peut embarquer une police retouchée. Le meilleur arbitre visuel, c’est le dessin réel du glyphe tel qu’il apparaît dans le document. D’où une étape qui change tout : rasteriser chaque glyphe inconnu — en découper une petite image depuis la page — et le regarder.
Mais l’image ne suffit pas toujours à fixer le sens. Ø, ⌀ et ∅, par exemple, peuvent se ressembler tout en signifiant respectivement une lettre barrée, un signe de diamètre et l’ensemble vide. Quand plusieurs candidats Unicode sont graphiquement proches, le contexte technique du document tranche : un symbole placé devant « 6 mm » n’a vraisemblablement pas le même sens que le même dessin au milieu d’une formule.
L’anecdote qui justifie la méthode : dans un premier jet, j’avais fait correspondre un code à la flèche bidirectionnelle « ↔ ». L’aperçu montrait sans ambiguïté un double tilde « ≈ » (« environ égal »). J’avais tout simplement interverti deux positions voisines de la table. Sans l’image sous les yeux, l’erreur passait inaperçue et se propageait partout.
Détecter tout le corpus d’un coup
Traiter les glyphes « au fil de l’eau », un document après l’autre, condamne à redécouvrir sans cesse les mêmes symboles. Plus efficace : un balayage unique de tout le lot qui, pour chaque code privé rencontré, compte les occurrences, garde un exemple de contexte, découpe l’image du glyphe, et pré-remplit la correspondance quand la police le permet. Le tout alimente un référentiel — un simple fichier tabulé — accompagné d’une planche visuelle où chaque glyphe s’affiche à côté de son identification.
Deux propriétés rendent ce référentiel agréable à vivre :
- Cumulatif et idempotent. Relancer le balayage ne perd jamais les identifications déjà validées ; il ne fait qu’ajouter les codes inédits. On peut donc le rejouer sans crainte après chaque nouveau document.
- Auto-signalant. Tout code non résolu ressort en rouge sur la planche et via un code de sortie non nul : un traitement automatisé peut alerter, l’humain n’intervient que lorsqu’un symbole vraiment nouveau apparaît.
Corriger à la source, pas au dernier moment
Reste une question d’architecture : où appliquer la correction ? La tentation est de nettoyer « à l’affichage », tout à la fin. C’est un piège : tout ce qui lit le texte avant l’affichage — une recherche, une traduction, un export intermédiaire — travaille alors encore sur des codes privés. La correction doit se faire le plus en amont possible, dès l’extraction du texte, pour que toute la chaîne en aval hérite d’un texte propre.
La logique gagne à être centralisée dans un unique petit module — une fonction « nettoyer(texte) » qui lit le référentiel — importée partout où du texte naît. Un seul endroit à maintenir, un comportement identique partout.
Il ne faut pas attendre d’une normalisation Unicode classique (NFC, NFD, NFKC ou NFKD) qu’elle résolve ces codes : un caractère de la PUA n’a volontairement aucune équivalence standard imposée. La transformation U+F0xx → Unicode standard est donc une normalisation propre au corpus, gouvernée par le référentiel et non par Unicode lui-même.
from glyphes import nettoyer
# a la sortie de l'extracteur, avant toute ecriture :
texte = nettoyer(texte) # les codes prives deviennent ©, ≈, •, Ø...
Et pour les documents déjà traités ? Un utilitaire de rattrapage repasse sur les fichiers existants ; lui aussi idempotent (on peut le relancer sans risque de double correction), écrivant une sauvegarde avant toute modification. Règle d’or des transformations rejouables : reconstruire proprement plutôt que bricoler l’existant par petites touches.
Pour aller plus loin
Deux lectures de référence sur la mécanique sous-jacente :
- Derek Noonburg (auteur de Xpdf /
pdftotext), Text Extraction — comment un extracteur passe des codes aux noms de glyphes puis à Unicode, et pourquoi les tablesToUnicodechangent la donne. Lire → - Wikipedia, Private Use Areas — la définition de la zone privée, et ce point central : deux parties peuvent ranger des dessins différents au même code. Lire →
- Unicode Consortium, Adobe Symbol Encoding to Unicode — table directe officielle de correspondance pour Symbol. Lire →
- Unicode Consortium, Adobe Zapf Dingbats Encoding to Unicode — même principe pour Zapf Dingbats. Lire →
Bilan
U+F0xx), qu’on rasterise pour le voir, puis qu’on identifie en caractère Unicode.Dans le cas étudié ici, le caractère manquant n’est pas un bug de l’extracteur : c’est un glyphe symbolique dont la correspondance Unicode n’a pas été correctement transmise. Le résoudre proprement tient en quelques principes : exploiter d’abord une table ToUnicode fiable si le PDF en fournit une ; sinon partir du nom de la police (qui désigne la table) ; enchaîner code → nom → Unicode pour les polices Adobe, ou utiliser une table directe lorsqu’elle existe ; mirrorer ces ressources externes plutôt que de les recopier ; se donner un garde-fou visuel et contextuel ; enfin corriger à la source pour que l’aval n’hérite jamais du problème.
La ligne directrice, encore une fois : une seule source de vérité — ici le référentiel validé — et tout le reste, extraction comme affichage, n’en est que la conséquence déterministe.