Encodages de caractères, de l'ASCII à UTF-8
15 mai 2026
Un é qui devient é dans un terminal, un fichier "Latin-1" qui
contient en fait du Windows-1252, un BOM caché en tête d'un CSV qui fait
planter un script. Tout ça vient du même endroit : ranger des lettres
dans des octets est un problème simple qui s'est compliqué dès qu'on a
voulu sortir de l'anglais. ASCII a tenu tant qu'il a pu, Latin-1 et
Windows-1252 ont tenté de pousser les murs, UTF-16 a essayé des unités
16 bits avant de devoir les étendre avec des surrogate pairs, et
UTF-8 a fini par résoudre les problèmes que les autres avaient laissés
derrière. Voici la suite, avec les détails qui servent au moment où on
tombe dessus.
Sommaire
1. Encodages 8 bits
ASCII
- 7 bits, 128 caractères
0x00–0x1F: caractères de contrôle (NUL, LF, CR, TAB, BEL...)0x20–0x7E: imprimable (espace inclus)0x7F: DEL- Base universelle : tout encodage 8 bits commence par étendre ASCII
Latin-1 (ISO 8859-1)
- 8 bits, 256 caractères = ASCII + 128
0x00–0x7F: identique ASCII0x80–0x9F: C1 controls (vides en pratique)0xA0–0xFF: caractères imprimables Europe occidentale (é,à,ç,ñ,ü...)
Windows-1252 (CP1252)
- Identique à Latin-1 sauf
0x80–0x9Fqui contient des caractères utiles (€en0x80, guillemets typographiques,…,–,—,™...) 0xA0–0xFF: strictement identique à Latin-1- C'est la réalité de la plupart des fichiers texte créés sous Windows, même quand le header dit "Latin-1"
2. Détecter Latin-1 vs Windows-1252
Pour trancher sur un buffer 8 bits :
- Tout octet
< 0x80→ c'est de l'ASCII pur, les deux encodages s'accordent - Octets dans
0xA0–0xFFseulement → indistinguable, mais peu importe : Latin-1 et Windows-1252 donnent le même rendu sur cette plage (on peut traiter comme Latin-1) - Au moins un octet dans
0x80–0x9F→ forcément Windows-1252, Latin-1 n'utilise pas cette plage
Détection peu fiable sur les petits buffers : il est facile de passer à
côté de la plage 0x80–0x9F et de se tromper.
3. UTF-16 et UTF-32
256 caractères ne suffisent évidemment pas pour couvrir toutes les écritures du monde. Unicode a d'abord essayé de tout caser dans une unité fixe plus large que l'octet : 16 bits, puis 32.
UTF-16
- Longueur variable : 2 ou 4 octets par caractère
U+0000–U+FFFF(BMP, Basic Multilingual Plane) : 2 octets directsU+10000–U+10FFFF: 4 octets via surrogate pair- Endianness obligatoire → BOM nécessaire
UCS-2 vs UTF-16
UCS-2 (1991), l'ancêtre de UTF-16, était à largeur fixe : 2 octets par caractère, point final. Mais le BMP - les 65 536 premiers code points Unicode - s'est révélé insuffisant pour couvrir toutes les écritures. UTF-16 (1996) a hérité de UCS-2 et a introduit les surrogate pairs pour étendre la couverture sans casser la rétrocompatibilité avec les implémentations UCS-2 existantes.
La zone surrogate (U+D800–U+DFFF)
Les 2048 code points de cette plage sont définitivement interdits comme caractères Unicode - pas juste réservés pour la mécanique UTF-16, mais inutilisables dans n'importe quel encodage. Un fichier qui contient un caractère seul dans cette plage est par définition malformé.
Algorithme surrogate pair
Pour un code point hors-BMP (U+10000–U+10FFFF), on construit deux
unités 16 bits :
offset = code_point - 0x10000
high = 0xD800 + (offset >> 10)
low = 0xDC00 + (offset & 0x3FF)
Pourquoi cette construction :
- Soustraction de
0x10000ramène l'offset à zéro. La plage hors-BMP va deU+10000àU+10FFFF, soit0x100000valeurs = exactement 20 bits d'information. - 20 bits = 10 + 10 : on découpe en deux unités 16 bits, chacune
avec un préfixe fixe de 6 bits (
0xD800pour le high,0xDC00pour le low), laissant 10 bits utiles par unité. - High surrogate (
U+D800–U+DBFF) : 1024 valeurs (10 bits hauts du code point). - Low surrogate (
U+DC00–U+DFFF) : 1024 valeurs (10 bits bas du code point). - 1024 × 1024 = 1 048 576 combinaisons → couvre exactement la plage hors-BMP.
Exemple : 😀 (U+1F600)
Code point : U+1F600 = 128512
Plage : hors-BMP → surrogate pair nécessaire
Offset :
offset = 0x1F600 - 0x10000 = 0xF600 = 62976
Découpage en 20 bits (10 hauts + 10 bas) :
0xF600 sur 20 bits : 0000 1111 0110 0000 0000
Découpage : 0000 1111 01 | 10 0000 0000
= 0x03D | 0x200
Calcul des deux unités 16 bits :
high = 0xD800 + 0x03D = 0xD83D
low = 0xDC00 + 0x200 = 0xDE00
Résultat (octets, big-endian) : 0xD8 0x3D 0xDE 0x00
Pièges pratiques
"😀".lengthen JS etString.length()en Java/Kotlin retournent 2, pas 1 - la longueur compte les unités 16 bits, pas les code points. Pour avoir le nombre de code points :[...s].lengthen JS,s.codePointCount(0, s.length)en Java.- Un surrogate isolé (high sans low qui suit, ou low sans high qui précède) est du texte malformé : la paire n'a de sens qu'ensemble.
- La JVM accepte des surrogates isolés en mémoire (les
StringJava sont des séquences d'unités 16 bits, pas de vraies chaînes Unicode). Mais convertir une telleStringen UTF-8 (par exemples.getBytes("UTF-8")) produit des octets invalides ou throw selon l'implémentation - source classique de bugs sur les pipelines I/O.
Pourquoi UTF-16 existe encore
UTF-8 (1992, Ken Thompson et Rob Pike) précède UTF-16 avec surrogates
(~1996) de 4 ans. UTF-16 survit aujourd'hui par lock-in : Windows
NT (1993) et Java (1995) avaient déjà parié sur UCS-2 avant que le BMP
s'avère insuffisant. Quand Unicode a dépassé U+FFFF en 1996, ces
plateformes ne pouvaient pas casser leur ABI - d'où le retrofit des
surrogate pairs sur UCS-2 pour produire UTF-16. Sans cet héritage,
UTF-8 serait probablement la norme partout.
UTF-32
- Toujours 4 octets par caractère, longueur fixe
- Simple à indexer, mais verbeux ; rare sur disque, courant en mémoire pour du traitement
- Endianness obligatoire → BOM nécessaire
4. Endianness
Ordre de stockage des octets pour les unités multi-octets (UTF-16, UTF-32).
Exemple avec U+4E2D (中) en UTF-16 :
- Big-endian :
4E 2D(octet de poids fort en premier - most significant byte first) - Little-endian :
2D 4E(octet de poids faible en premier - least significant byte first)
Contextes :
- Big-endian : réseau TCP/IP (network byte order), PowerPC, mainframes IBM
- Little-endian : x86, x64, ARM (mode courant)
- Bi-endian : ARM (configurable), MIPS, PowerPC
5. BOM (Byte Order Mark) - U+FEFF
Le BOM est le caractère U+FEFF placé en tête de fichier. Sa
représentation en octets indique l'encodage et l'endianness.
| Encodage | BOM | Notes |
|---|---|---|
| UTF-8 | EF BB BF | Optionnel, pas d'endianness. Créé par Windows/Notepad. À stripper en lecture. |
| UTF-16 big-endian | FE FF | Obligatoire |
| UTF-16 little-endian | FF FE | Obligatoire |
| UTF-32 big-endian | 00 00 FE FF | Obligatoire |
| UTF-32 little-endian | FF FE 00 00 | Obligatoire |
Piège classique : FF FE ressemble à un BOM UTF-16 LE, mais si les
deux octets suivants sont 00 00, c'est en fait un BOM UTF-32 LE. Lire
4 octets avant de trancher.
6. UTF-8
UTF-8 contourne tous les inconvénients ci-dessus : plus de gaspillage de place pour les caractères ASCII, plus d'endianness, plus de BOM obligatoire.
- Unité de base : 8 bits (1 octet)
- Longueur variable : 1 à 4 octets par caractère
- Le chiffre "8" désigne la taille de l'unité de base, pas la taille max d'un caractère
- Rétrocompatible ASCII : tout fichier ASCII est un UTF-8 valide
- Pas de problème d'endianness puisque l'unité fait 1 octet
Patterns des octets
Notation : les 0 et 1 sont les bits fixes des marqueurs (ce qui
identifie le format) ; les x sont les bits de données du code point
qui seront placés à ces positions.
0xxxxxxx → 1 octet (U+0000–007F, ASCII)
110xxxxx 10xxxxxx → 2 octets (U+0080–07FF)
1110xxxx 10xxxxxx 10xxxxxx → 3 octets (U+0800–FFFF)
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx → 4 octets (U+10000–10FFFF)
10xxxxxx → octet de continuation
(jamais un premier octet)
Manipulation de bits - rappels
L'encodage et le décodage UTF-8 reposent sur deux opérations bit à bit :
le masquage avec AND (&) pour isoler certains bits d'un octet, et le
décalage (>> à droite pour l'encodage, << à gauche pour le décodage)
pour les repositionner.
Masque avec AND (&)
Un masque est une valeur qu'on combine avec AND pour isoler des bits.
Règles : 1 AND x = x, 0 AND x = 0. Les bits à 1 du masque "laissent
passer", les bits à 0 "effacent".
Exemple - isoler les 6 bits bas de 0xE9 avec le masque 0x3F :
1110 1001 (0xE9)
& 0011 1111 (0x3F = masque des 6 bits bas)
-----------
0010 1001 → 6 bits bas isolés
Le masque 0x3F revient partout en UTF-8 parce que chaque octet de
continuation porte exactement 6 bits.
Décalage à droite (>>)
x >> n pousse tous les bits n positions vers la droite. Les bits bas
tombent, les bits hauts descendent à leur place.
Exemple - récupérer ce qui reste au-dessus des 6 bits bas de 0xE9 :
0xE9 = 1110 1001
0xE9 >> 6 = 0000 0011 → les 2 bits hauts restants
En UTF-8, après avoir extrait les 6 bits bas pour l'octet de continuation,
>> 6 permet de récupérer les bits au-dessus pour les placer dans l'octet
de tête.
Décalage à gauche (<<)
x << n pousse tous les bits n positions vers la gauche. Les bits hauts
sortent (au-delà de la largeur du type), les bits bas remontent.
Exemple - remettre 0x03 à sa place six bits plus haut :
0x03 = 0000 0011
0x03 << 6 = 1100 0000 → bits repositionnés
En UTF-8, c'est l'opération inverse de >> : au décodage, on l'utilise
pour remettre les bits d'un octet de tête en haut avant de les fusionner
avec ceux d'un octet de continuation.
En combinant les deux
Pour construire les deux octets UTF-8 de é (U+00E9) en C :
uint8_t c = 0xE9;
uint8_t byte1 = 0xC0 | (c >> 6); // marqueur 110 + bits hauts
uint8_t byte2 = 0x80 | (c & 0x3F); // marqueur 10 + 6 bits bas
c & 0x3Fisole les 6 bits basc >> 6isole les bits au-dessus| 0xC0colle le marqueur110devant l'octet de tête| 0x80colle le marqueur10devant l'octet de continuation
C'est la traduction directe du découpage "bits hauts / 6 bits bas" en code.
Les masques utiles pour décoder
Détection du format (sur l'octet de tête) :
0xC0= marqueur11000000(début séquence 2 octets)0xE0= marqueur11100000(début séquence 3 octets)0xF0= marqueur11110000(début séquence 4 octets)0x80= marqueur10000000(octet de continuation)
Extraction des bits de données :
0x1F= masque des 5 bits bas (00011111) - bits utiles d'un octet de tête sur 2 octets0x0F= masque des 4 bits bas (00001111) - bits utiles d'un octet de tête sur 3 octets0x07= masque des 3 bits bas (00000111) - bits utiles d'un octet de tête sur 4 octets0x3F= masque des 6 bits bas (00111111) - bits utiles d'un octet de continuation
Exemple de décodage
Pour décoder la séquence UTF-8 0xC3 0xA9 :
Octets reçus : 0xC3 0xA9
Étape 1 - identifier le format depuis l'octet de tête
0xC3 = 1100 0011
Préfixe `110` → séquence sur 2 octets
Étape 2 - extraire les bits utiles de chaque octet
Octet de tête : 0xC3 & 0x1F = 0000 0011 (5 bits)
Octet de continuation : 0xA9 & 0x3F = 0010 1001 (6 bits)
Étape 3 - recoller en décalant les bits de tête de 6 places
(0x03 << 6) | 0x29 = 1100 0000 | 0010 1001 = 1110 1001 = 0x00E9
Résultat : U+00E9 = é
En C :
uint8_t byte1 = 0xC3;
uint8_t byte2 = 0xA9;
uint32_t code_point = ((byte1 & 0x1F) << 6) | (byte2 & 0x3F); // 0xE9
Pour 3 octets, l'octet de tête utilise 0x0F (4 bits utiles) et les
décalages deviennent << 12, << 6. Pour 4 octets, 0x07 (3 bits
utiles) avec << 18, << 12, << 6. Le principe est toujours le même :
masquer chaque octet pour récupérer ses bits de données, puis les
positionner avec << avant de les fusionner avec |.
Encoder un code point en UTF-8
Pour encoder un code point Unicode en octets UTF-8, il faut choisir le
format en fonction de la valeur du code point, puis répartir ses bits
dans les emplacements x du format.
| Plage du code point | Bits significatifs | Format UTF-8 | Nombre d'octets |
|---|---|---|---|
U+0000–U+007F | 7 bits | 0xxxxxxx | 1 |
U+0080–U+07FF | 11 bits | 110xxxxx 10xxxxxx | 2 |
U+0800–U+FFFF | 16 bits | 1110xxxx 10xxxxxx 10xxxxxx | 3 |
U+10000–U+10FFFF | 21 bits | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 4 |
Chaque octet de continuation porte toujours 6 bits ; seul l'octet de tête varie.
Algorithme général :
- Trouver la plage du code point → en déduire le nombre d'octets et le format
- Écrire le code point en binaire sur le nombre de bits significatifs requis (compléter à gauche avec des zéros)
- Découper en morceaux et remplir les
xdu format (6 bits par octet de continuation) - Coller les marqueurs (
110,1110,11110,10) devant chaque morceau - Convertir chaque octet binaire en hexadécimal (1 chiffre hexa par groupe de 4 bits)
Exemples d'encodage
1 octet : A (U+0041)
Code point : U+0041 = 65
Plage : U+0000–U+007F → 1 octet, format 0xxxxxxx
Binaire sur 7 bits : 1000001
Remplissage du format :
Format : 0xxxxxxx
Bits : 1000001
Octet : 01000001
Conversion en hexa (1 chiffre hexa = 4 bits) :
01000001 = 0100 0001 = 0x41
Résultat : 0x41
ASCII et UTF-8 sont identiques sur cette plage - c'est la rétrocompatibilité.
2 octets : é (U+00E9)
Code point : U+00E9 = 233
Plage : U+0080–U+07FF → 2 octets, format 110xxxxx 10xxxxxx
Binaire sur 11 bits : 000 1110 1001
Découpage : 00011 | 101001 (5 bits hauts, 6 bits bas)
Remplissage du format (les `110`/`10` viennent du format, les bits remplissent les `x`) :
Format : 110xxxxx 10xxxxxx
Bits : 00011 101001
Octets : 11000011 10101001
Conversion en hexa :
11000011 = 1100 0011 = 0xC3
10101001 = 1010 1001 = 0xA9
Résultat : 0xC3 0xA9
En Latin-1, é = 0xE9 (1 octet). Quand un é apparaît en sortie,
c'est du UTF-8 (0xC3 0xA9) lu comme si c'était du Latin-1 :
0xC3 → Ã, 0xA9 → ©.
3 octets : 中 (U+4E2D)
Code point : U+4E2D = 20013
Plage : U+0800–U+FFFF → 3 octets, format 1110xxxx 10xxxxxx 10xxxxxx
Binaire sur 16 bits : 0100 1110 0010 1101
Découpage : 0100 | 111000 | 101101 (4 bits, 6 bits, 6 bits)
Remplissage du format :
Format : 1110xxxx 10xxxxxx 10xxxxxx
Bits : 0100 111000 101101
Octets : 11100100 10111000 10101101
Conversion en hexa :
11100100 = 1110 0100 = 0xE4
10111000 = 1011 1000 = 0xB8
10101101 = 1010 1101 = 0xAD
Résultat : 0xE4 0xB8 0xAD
Tous les caractères CJK (chinois, japonais, coréen) communs sont sur 3 octets.
4 octets : 😀 (U+1F600)
Code point : U+1F600 = 128512
Plage : U+10000–U+10FFFF → 4 octets, format 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
Binaire sur 21 bits : 0 0001 1111 0110 0000 0000
Découpage : 000 | 011111 | 011000 | 000000 (3 bits, 6 bits, 6 bits, 6 bits)
Remplissage du format :
Format : 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
Bits : 000 011111 011000 000000
Octets : 11110000 10011111 10011000 10000000
Conversion en hexa :
11110000 = 1111 0000 = 0xF0
10011111 = 1001 1111 = 0x9F
10011000 = 1001 1000 = 0x98
10000000 = 1000 0000 = 0x80
Résultat : 0xF0 0x9F 0x98 0x80
Tous les emojis sont sur 4 octets - c'est aussi le cas des plans Unicode supplémentaires (écritures historiques comme le cunéiforme, symboles mathématiques, etc.).
Ressources
Tables de caractères
- ASCII - robelle.com
- ASCII - man page Linux
- ISO 8859-1 (Latin-1) - Wikipédia
- Windows-1252 - Wikipédia
- UTF-8 - Wikipédia