Unicode en 5 min

Chaque langue comporte des caractères particuliers et parfois spécifiques. Pour retranscrire ces caractères dans une chaîne de caractère, l’application générant la chaîne utilise un encodage. Pour que les caractères soient correctement lues, il est souvent nécessaire que l’application lectrice connaisse l’encodage qui est utilisé ou réussisse à le deviner.

L’encodage des caractères dans une chaîne repose sur des conventions dont les caractéristiques dépendent de critères particuliers:

  • Nécessité de traiter la chaîne de caractères en économisant les ressources matérielles de la machine,
  • Nécessité d’économiser la bande passante lors de la transmission de la chaîne de caractères à travers un réseau,
  • Rechercher la compatibilité entre plusieurs encodages.

Les encodages le plus connus sont l’ASCII, l’UTF-8 ou l’UTF-16. Ils présentent des différences qui peuvent mener, très souvent, à un mauvais traitement d’une chaîne de caractères. Le but de ce document est de rappeler les caractéristiques principales de ces encodages.

Editeur héxadécimal de Visual Studio

Pour voir les encodages des fichiers texte en hexadécimal, on peut s’aider de Visual Studio qui possède un éditeur hexadécimal.
Pour ouvrir un fichier avec l’éditeur hexadécimal de Visual Studio:

  1. Open a file
  2. Utiliser la flêche à coté du bouton “Open”
  3. Cliquer sur “Open With”
  4. Sélectionner “Binary Editor”
  5. Cliquer sur OK

1. ASCII

L’ASCII (American Standard Code for Information Interchange) est une norme de codage de caractères incontournable car largement utilisé depuis sa création dans les années 60. Il comporte 128 caractères numérotés de 0 à 127. Chaque numéro correspond à un caractère. Pour encoder une chaîne de caractères, il suffit de traduire chaque caractère dans un code hexadécimal en utilisant le numéro correspondant au caractère.

On peut se reporter à www.asciitable.com pour voir l’encodage des caractères ASCII. Par exemple, la lettre "M" correspond au numéro 77 en décimal et 4D en hexadécimal.

Pour encoder les caractères en binaire, 7 bits (2^7= 128) suffisent, toutefois l’ASCII utilise 8 bits. Le 8e bit permet de définir des caractères numérotés de 128 à 255 mais la norme ASCII laisse la place vacante à d’autres caractères puisqu’elle ne définit des caractères que de 0 à 127.

Exemple d’encodage en ASCII

Par exemple, pour encoder la chaîne "AZERTY", la traduction en hexadécimal en utilisant la table ASCII est:

A Z E R T Y
41 5A 45 52 54 59

Inconvénients de l’ASCII

Si on regarde la table ASCII, on se rends compte que les caractères sont:
Des lettres de la langue anglaise et
Des caractères spéciaux courants.

Il n’y a pas d’autres caractères spécifiques à d’autres langues: é, è, à, ç, etc…

Pages de code

Sachant que l’ASCII utilise un encodage sur 8 bits et ne définit que les caractères de 0 à 127, il reste une place vacante pour les caractères de 128 à 255. Pour définir les caractères spécifiques à chaque langue, les pays ont utilisés cet espace pour les encoder. Chaque pays a donc défini un standard utilisant ASCII pour les premiers caractères et un encodage spécifique pour les caractères suivants. Ces standards s’appelent “pages de code” (code pages).

L’inconvénient majeur des pages de code est qu’il en existe plusieurs et qu’il est nécessaire d’utiliser exactement le même entre l’application générant la chaîne de caractères et l’application la lisant.

Par exemple, la page de code 1252 correspond à un jeu de caractères de l’alphabet latin utilisé par Windows en anglais et pour les principales langues d’Europe de l’Ouest y compris le français.

On peut se reporter à fr.wikipedia.org/wiki/Windows-1252 pour avoir le détail de la table de caractères pour la page de code 1252 ou Windows-1252.

2. Unicode

Comme indiqué plus haut, les gros problèmes des pages de code est qu’il en existe beaucoup et qu’il est compliqué de déchiffrer une chaîne de caractères sans connaitre la page de code avec laquelle elle a été codée.

Unicode vise à normaliser des jeux de caractères pour qu’un seul standard puisse être utilisé pour encoder la grande majorité des caractères possibles. Comme pour ASCII, Unicode définit un identifiant pour un caractère unique. La correspondance entre le caractère et sa valeur numérique est appelée “point de code” (code point).

Au standard Unicode correspond plusieurs encodages de façon à convertir le point de code Unicode en octets. Les encodages les plus connus sont UTF-8, UCS-2 et UTF-16.

Ainsi si on prends le point de code U+23374:

  • En UTF-8: l’encodage correspondant est F0A3 8DB4.
  • En UTF-16: l’encodage correspondant est D84C DF75.

UCS-2

Pour définir plus de caractères que l’ASCII, il faut augmenter l’intervalle de la valeur numérique permettant d’identifier un caractère. L’ASCII était définit sur 8 bits, ainsi UCS-2 est définit sur 16 bits soit 2 octets. Il permet d’identifier 65536 caractères ce qui permet d’adresser un grand nombre de langues avec un seul standard. Les caractères sont numérotés de 0 à 65535 ou 0x0000 à 0xFFFF en hexadécimal.

On peut se reporter à www.columbia.edu/kermit/ucs2.html pour avoir le détail de la table de caractères de l’UCS-2.

Avec un mot de 16 bits, l’UCS-2 permet de coder les caractères dont les points de code se trouvent dans le plan multilingue de base (Basic Multilingual Plan BMP). Toutefois il ne permet pas d’encoder les caractères qui ne se trouvent pas dans le plan multilingue de base. D’où l’apparition du standard UTF-16.

Exemple d’encodage en UCS-2

Par exemple, pour encoder la chaîne "AZERTY éèà" en utilisant UCS-2, on aura:

A Z E R T Y (space) é è à
0041 005A 0045 0052 0054 0059 0020 00E9 00E8 00E0

Ainsi:

  • Les caractères "A", "Z", "E", "R", "T" et "Y" font partie de l’encodage ASCII donc la valeur hexadécimal du point de code est la même que pour celle en ASCII.
  • Idem pour le caractère espace.
  • Les caractères "é", "è" et "à" ne sont pas dans la table ASCII, le point de code utilisé est particulier.
UCS-2 et UTF-16

Souvent UCS-2 et UTF-16 sont confondus, pourtant les 2 encodages sont différents: UCS-2 utilisent strictement 2 octets alors que l’UTF-16 permet d’utiliser 4 octets.

L’inconvénient de coder les caractères sur plus de 1 octet est qu’il faut convenir de l’ordre des octets: quel sera l’octet fort ? Le premier octet ou le 2e octet ?

“Big endian” ou “Little endian”

Comme indiqué plus haut, un inconvénient d’utiliser plusieurs octets pour identifier un caractère est qu’il faut s’entendre sur l’ordre des octets. En effet suivant les architectures matérielles, les processeurs utilisent des conventions différentes concernant le poids des octets.
UCS-2 utilise 2 octets donc il faut pouvoir identifier l’octet fort et l’octet faible:

  • Big endian (BE): l’octet de poids le plus fort est enregistré à l’adresse mémoire la plus petite, le nombre 0xA7E8 est donc rangé:
    0 1
    A7 E8
  • Little endian (LE): l’octet de poids le plus faible est enregistré à l’adresse mémoire la plus petite, le nombre 0xA7E8 est donc rangé:
    0 1
    E8 A7

Pour savoir quelle convention est choisie, il faut convenir d’une façon de l’indiquer dans la chaîne de caractères.

Exemple d’encodage en UCS-2

Par exemple, pour encoder la chaîne "AZERTY éèà" en utilisant UCS-2BE, on aura:

A Z E R T Y (space) é è à
0041 005A 0045 0052 0054 0059 0020 00E9 00E8 00E0

En utilisant UCS-2LE, on aura:

A Z E R T Y (space) é è à
4100 5A00 4500 5200 5400 5900 2000 E900 E800 E000

BOM (byte order mark)

Pour les encodages de plus d’un octet, il est possible de convenir de la façon d’identifier les octets de poids fort et les octets de poids faible.

Le “byte order mark” (BOM) permet d’indiquer la convention choisie. Utiliser un BOM consiste à placer un point de code particulier au début de la chaîne de caractère. La lecture de ce point de code permettra à l’application lectrice, de savoir comment utiliser les points de code suivants correspondant aux caractères à décoder.
Par convention pour l’USC-2:

  • Le point de code U+FEFF correspondant à la valeur 0xFEFF permet d’indiquer que la convention choisie est “big endian”. Il n’y a donc pas de changements à faire sur l’ordre des octets.
  • Le point de code U+FFFE correspondant à la valeur 0xFFFE permet d’indiquer que la convention choisie est “little endian”. Il faut donc inverser l’ordre des octets pour lire la valeur du point de code.

FE FF et FF FE peuvent être confondu avec des caractères toutefois, s’ils sont présents dans l’entête d’une chaîne de caractères, on peut facilement supposer qu’il s’agit d’un BOM.

Exemple d’encodage du BOM

Par exemple, pour encoder la chaîne "AZERTY éèà" en utilisant UCS-2BE avec un BOM, on aura:

A Z E R T Y (space) é è à
FE FF 0041 005A 0045 0052 0054 0059 0020 00E9 00E8 00E0

En utilisant UCS-2LE avec un BOM, on aura:

A Z E R T Y (space) é è à
FF FE 4100 5A00 4500 5200 5400 5900 2000 E900 E800 E000

UTF-16

L’apport d’UTF-16 par rapport à UCS-2 est de permettre de prendre en compte les caractères dont les points de code ne sont pas dans le BMP (Basic Multilingual Plan). Ainsi les 65536 premiers points de code U+0000 à U+FFFF sont les mêmes que pour UCS-2. A partir de U+10000, les points de code sont spécifiques à UTF-16.

Pour permettre d’identifier des caractères dont le point de code a une valeur supérieure à 0xFFFF codée sur 16 bits, il faut utiliser un encodage sur davantages de bits. Ainsi, UTF-16 utilise 20 bits répartis sur 2 mots de 16 bits soit 2 octets. Ces 2 mots sont appelés “surrogate pair”. Chaque mot seul identifie un caractère invalide mais s’ils sont utilisés ensemble, il désigne un caractère particulier.

Ainsi le point de code du caractère sera codé:

  • Sur 2 octets: si la valeur numérique du point de code du caractère est inférieure ou égale à 0xFFFF.
  • Sur 4 octets: si la valeur numérique du point de code est strictement supérieure à 0xFFFF.
Dénomination Unicode

UTF-16 est utilisé par Windows et lorsqu’on parle d’Unicode, on fait généralement allusion à UTF-16.

La même convention “Big endian” ou “Little endian” est utilisée pour UTF-16 car, comme pour UCS-2, le point de code utilise plus d’un octet.

Les 20 bits sont répartis sur les différents octets de cette façon pour UTF-16BE:

  • Sur 2 octets:
    xxxx xxyy yyyy yyyy

    16 bits répartis sur 2 octets comme pour UCS-2BE.

  • Sur 4 octets:
    1101 10ww wwxx xxxx 1101 11yy yyyy yyyy

    20 bits répartis sur 4 octets.

Dans le cas d’UTF-16LE, l’ordre des octets est inversé à l’intérieur des pairs des “surrogate pair”:

  • Sur 2 octets:
    yyyy yyyy xxxx xxyy

    16 bits répartis sur 2 octets comme pour UCS-2LE.

  • Sur 4 octets:
    wwxx xxxx 1101 10ww yyyy yyyy 1101 11yy

    20 bits répartis sur 4 octets.

Comme on peut le voir, la première paire est identifiée avec 110110. La 2e paire est identifiée avec 110111. L’intérêt d’utiliser des bits dont la valeur est fixe pour désigner les paires est qu’il rend possible de faire la différence entre un caractère codé sur 2 octets et un autre caractères codé sur 4 octets. D’autre part, une paire seule ne correspond pas au point de code d’un caractère.

Lorsque le point de code utilise 4 octets, pour obtenir l’encodage correspondant il faut:

  • Soustraire 0x10000 à la valeur numérique du points de code,
  • Séparer les 20 bits en 2 mots de 10 bits,
  • Ajouter 0xD800 (en binaire 1101 1000 0000 0000) à la première paire,
  • Ajouter 0xDC00 (en binaire 1101 1100 0000 0000) à la 2e paire.

Par exemple, pour le caractère U+23374:

  • Si on soustrait 0x10000 à 0x23374, on obtient 0x13374 ce qui correspond à la valeur binaire 0001 0011 0011 0111 0100
  • Si on sépare les 20 bits en 2 mots de 10 bits: 0001 0011 00 puis 11 0111 0100.
  • Si on ajoute 0xD800 à 0x004C (en binaire 0001001100) on obtient 0xD84C.
  • Si on ajoute 0xDC00 à 0x0374 (en binaire 1101110100) on obtient 0xDF74.

L’encodage est donc en UTF-16BE: D8 4C DF 75. Ainsi D84C ne correspond pas à un caractère et de même pour DF75.

Exemple d’encodage en UTF-16

Si on prends l’exemple de la chaîne "AZERTY éèà" suivi du caractère U+23374, l’encodage UTF-16BE sera:

A Z E R T Y (space) é è à
U+0041 U+005A U+0045 U+0052 U+0054 U+0059 U+0020 U+00E9 U+00E8 U+00E0 U+23374
FE FF 0041 005A 0045 0052 0054 0059 0020 00E9 00E8 00E0 D84C DF75

Dans le cas de l’encodage UTF-16LE:

A Z E R T Y (space) é è à
U+0041 U+005A U+0045 U+0052 U+0054 U+0059 U+0020 U+00E9 U+00E8 U+00E0 U+23374
FF FE 4100 5A00 4500 5200 5400 5900 2000 E900 E800 E000 4CD8 75DF

On peut se reporter à www.fileformat.info/info/charset/UTF-16/list.htm pour avoir l’ensemble des caractères de UTF-16.

Inconvénient majeur d’UTF-16

UTF-16 utilise beaucoup d’espace car pour la plupart des caractères correspondant aux langues occidentales, on est obligé de préciser "00" (par exemple pour "é" l’encodage est 00E9) ce qui fait perdre beaucoup d’espace. Pour des transmissions de chaîne de caractères à travers un réseau, ce gaspillage d’espace peut causer des performances moins bonnes que des encodages plus simples.

UTF-16 peut causer une incompatibilité avec des applications utilisant l’ASCII à cause de la présence des "0x00".

UTF-8

Comme indiqué précédemment, le gros intérêt d’UTF-8 est d’éviter le gaspillage d’espace causé par l’utilisation d’UTF-16. Il est utilisé en majorité pour l’encodage de fichiers HTML ou XML.
L’autre avantage d’UTF-8 est d’apporter une compatibilité avec l’encodage ASCII contrairement à UTF-16.

UTF-8 encode les points de code dans des mots de 8 bits:

  • Les points de code de 0x0000 à 0x007F correspondant à des caractères ASCII sont codés sur 8 bits donc 1 octet suffit.
  • Les points de code supérieurs à 0x0080 correspondent à des caractères non-ASCII, ils sont codés sur 2, 3 ou 4 octets.

Pour les points de code nécessitant plusieurs octets, les mots commençant par:

  • "10" en binaire indique que les bits suivant sont des bits utilisés pour encoder le point de code.
  • "11" en binaire indique que le mot doit être utilisé avec les mots suivants pour identifier un caractère.

Ainsi:

  • Les caractères ASCII ont un point de code codé sur 1 octet du type:
    0xxx xxxx

    7 bits sont utilisés comme pour l’ASCII.

  • Les caractères non-ASCII ayant un point de code codé sur 2 octets sont du type:
    110x xxxx 10xx xxxx
  • Les caractères non-ASCII ayant un point de code codé sur 3 octets sont du type:
    1110 xxxx 110x xxxx 10xx xxxx
  • Les caractères non-ASCII ayant un point de code codé sur 4 octets sont du type:
    1111 0xxx 1110 xxxx 110x xxxx 10xx xxxx

On peut se reporter à www.utf8-chartable.de/unicode-utf8-table.pl pour avoir l’ensemble des caractères UTF-8.

Utilisation d’un BOM

Il n’y a pas de problèmes d’ordre des octets avec UTF-8, les octets sont toujours utilisés dans le même ordre. Il n’est pas nécessaire d’utiliser un BOM, toutefois très souvent les chaînes de caractères ou les fichiers codés en UTF-8 sont précédés d’un BOM dont la valeur est "EF BB BF".

Le plus souvent, ce BOM permet d’indiquer simplement qu’il s’agit d’un fichier UTF-8.

Exemple d’encodage en UTF-8

Si on prends l’exemple de la chaîne “AZERTY éèà” suivi du caractère U+23374, l’encodage UTF-8 sans BOM sera:

A Z E R T Y (space) é è à
U+0041 U+005A U+0045 U+0052 U+0054 U+0059 U+0020 U+00E9 U+00E8 U+00E0 U+23374
41 5A 45 52 54 59 20 C3A9 C3A8 C3A0 F0A3 8DB4

L’encodage en UTF-8 avec BOM sera:

A Z E R T Y (space) é è à
U+0041 U+005A U+0045 U+0052 U+0054 U+0059 U+0020 U+00E9 U+00E8 U+00E0 U+23374
EF BB BF 41 5A 45 52 54 59 20 C3A9 C3A8 C3A0 F0A3 8DB4

Comme on peut le voir, les caractères ASCII sont codés directement en ASCII sans octet nul 0x00. Un programme qui ne lit que l’ASCII peut lire directement les caractères. Seuls les caractères faisant partie de l’ASCII seront décodés.

Inconvénients de l’UTF-8

Le défaut d’UTF-8 est que les points de code ayant une valeur numérique faible sur peu d’octets sont réservés aux caractères des langues occidentales. Les caractères spécifiques des langues autres que les langues occidentales sont codés sur plusieurs octets. Plus des octets sont nécessaires pour encoder des caractères et plus les transmissions par réseau prennent du temps ou plus les fichiers sont lourds.
Ainsi UTF-8 est plus performant qu’UTF-16 pour les transmissions de caractères appartenant à des langues occidentales mais cet avantage diminue si il faut encoder des caractères appartenant à d’autres langues.

Le 2e défaut d’UTF-8 est qu’il nécessite plus de ressources matériels pour décoder les caractères. Pour des transmissions de chaînes de caractères par le réseau, on tente d’économise la bande passante en privilégiant un encodage moins gourmant en espace au détriment d’un traitement un peu plus lourd. C’est la raison pour laquelle UTF-8 est l’encodage le plus utlisé sur internet. En revanche, si on veut privilégier les ressources matérielles et que la taille des chaînes est moins critique comme dans un système d’exploitation, on utilisera plutôt UTF-16 comme dans Windows par exemple.

Leave a Reply