Unidecoder et UTF-8

Lorsque le besoin de “latiniser” des caractères non alphabétiques se présente, la première gem sur laquelle on tombe est Unidecoder. Cette dernière permet de faire ce que l’on appelle de la translittération, c’est-à-dire retranscrire un caractère d’un alphabet à un autre.

On peut par exemple translittérer le “你好” chinois en “Ni Hao” ou encore le “Привет” russe en “Priviet”.

"你好".to_ascii    #=> "Ni Hao "
"Привет".to_ascii #=> "Priviet"

Unidecoder permet donc d’effectuer cette transformation de caractères UTF-8 vers ASCII. Pour comprendre ce processus il faut tout d’abord savoir ce qu’est UTF-8 et comment il fonctionne.

ASCII

À l’origine, à l’époque des systèmes sur 7 bits, l’encodage ASCII a été développé pour permettre le transfert d’informations.

Stocker une donnée sur 7 bits ne permet pas de s’étendre sur le nombre de caractères les limites se situant à 0000000 (0) et 1111111 (127). Les créateurs d’ASCII ont cependant fait un choix plutôt malin, celui de représenter les majuscules à partir de 65 et les minuscules à partir de 97.

Pourquoi malin ? C’est au niveau binaire que se trouve la réponse. En effet si l’on regarde la représentation binaire de 65 on obtient 1000001.

Jusque-là, rien de bien surprenant mais on note déjà que toutes les lettres majuscules seront listées de 10 00001 (A, 65) à 10 11010 (Z, 90), et ont donc toutes une représentation de la forme 10 XXXXX.

En observant maintenant les caractères minuscules, l’astuce apparait. Plutôt que de les mettre directement à la suite des majuscules, ces caractères commencent à 97 c’est-à-dire 1100001. De la même façon que les majuscules, les minuscules sont listées de 11 00001 (A, 97) à 11 11010 (z, 121).

Représentation binaires en ASCII

Si vous prenez le temps d’observer les valeurs binaires des caractères de 0 à 9 vous constaterez le même procédé.

Autres encodages

Lors de l’arrivée des systèmes à 8 bits, le champ de possibilité ne s’étendait plus de 0 à 127 mais de 0 à 255. Tous les pays en ont profité pour créer leurs propres encodages et ajouter leurs caractères spéciaux. En France, nous avons par exemple créé l’encodage ISO-8859-15 (a.k.a. latin-9), basé sur l’encodage européen ISO-8859-1 (a.k.a. latin-1).

À cette époque le besoin de communication d’un encodage à l’autre étant relativement faible, une impression suivie d’un fax faisant généralement l’affaire, tout fonctionnait plutôt bien.

Jusqu’à l’arrivée du World Wide Web…

UTF-8

Pour répondre au besoin devenu urgent de trouver un encodage standard commun et couvrant tous les caractères de tous les alphabets du monde, l’Unicode Consortium finit par créer UTF-8.

La confection de cet encodage a nécessité une intense réflexion. En effet la solution la plus simple, lister tous les caractères, leur assigner un nombre et mettre tous les caractères sur 32 bits (4 octets) n’était pas viable et ce pour plusieurs raisons.

Première raison, tous les caractères inférieurs à 16 777 216 n’ont pas besoin de 4 octets et consommeraient donc beaucoup de place. Le simple hello ASCII passerait de 5 octets à 20 octets. Mauvaise idée.

Deuxième problème, les octets non utilisés seraient mis à zéro. Prenons par exemple le cas de A qui serait représenté 00000000 00000000 00000000 01000001. On voit ici l’espace perdu mais également le fait que les trois octets inutiles valent 0 ce qui est la valeur de fin de chaine \0 dans de nombreux langages de programmation. Mauvaise idée.

Une meilleure solution

Les créateurs d’UTF-8 ont fait preuve d’une remarquable ingéniosité. Pour éviter les deux pièges cités ci-dessus et garder une certaine flexibilité, voici comment UTF-8 fonctionne.

Tout ce qui est ASCII reste tel quel, sur 8 bits, le caractère A reste donc 01000001. Tout ce qui est au-dessus d’ASCII suit la logique suivante :

  • premier octet de la forme 110 XXXXX
  • octets suivants de la forme 10 XXXXXX

Le premier octet indique le nombre d’octets qui le suivent.

Si celui-ci commence par 110, il indique être en deux parties : lui-même et l’octet suivant.

S’il commence par 1110 il est en trois parties : lui-même et les deux octets suivants.

Représentations binaires en UTF-8

Récupération du caractère

Pour comprendre le fonctionnement d’UTF-8, partons d’un caractère d’exemple le P russe qui s’écrit П.

En UTF-8 ce caractère prend la forme 1101000010011111. En suivant les explications précédentes, le tout peut être séparé en deux octets : 110 10000 et 10 011111.

Une fois les en-têtes supprimés, il nous reste 10000 011111 (1055) qui est la valeur concrète du caractère.

Unidecoder

Maintenant que nous en savons un peu plus sur UTF-8, nous sommes plus à même de comprendre comment Unidecoder s’y prend pour translittérer un caractère d’un alphabet à un autre.

En réalité le code Ruby de la gem effectue pour chaque caractère le traitement que nous venons de décrire. Il va ensuite utiliser les informations récoltées pour déterminer le caractère de remplacement.

char = "П"
unpacked = char.unpack("U")[0]
# => 1055

On retrouve bien ici la valeur 1055 précédemment extraite du binaire UTF-8.

Utilisation de unpack en Ruby

Note : Pour en savoir plus sur unpack, consultez la documentation Ruby.

Une fois ce nombre récupéré, Unidecoder va commencer par déterminer le groupe auquel appartient le caractère. Pour ce faire, il va supprimer le dernier octet du nombre, ici 00011111 et extraire le reste.

unpacked >> 8
# => 4

Le groupe est donc ici le 4.

Décalage de bit

Unidecoder contient, dans un dossier spécifique, une liste de fichiers correspondant chacun à un groupe de caractères. Ces fichiers sont nommés de x00.yml à xff.yml, il faut donc convertir notre 4 au bon format.

"x%02x.yml" % 4
# => "x04.yml"

Note : Pour en savoir plus sur le formatage de strings, consultez la documentation Ruby.

Ces fichiers YAML contiennent la liste des translittérations d’un groupe, par exemple pour x04.yml on trouve :

---
- Ie
- Io
- Dj
- Gj
- Ie
- Dz
- I
- Yi
- J
- ...

Connaissant maintenant le nom du fichier à charger, il ne reste plus qu’à identifier la ligne à utiliser dans celui ci. Cette ligne est trouvée grâce au dernier octet du caractère.

Pour connaitre ce dernier octet, Unidecoder utilise un masque binaire de 255 :

unpacked & 255
# => 31

Utilisation d'un masque binaire

Une fois le fichier YAML chargé, nous obtenons un tableau des translittération et devons récupérer l’entrée à l’indice 31 :

codepoints = YAML::load_file('x04.yml')
# => ['Ie', 'Io', 'Dj', 'Gj', 'Ie', 'Dz', 'I', 'Yi', 'J', ...]

codepoints[31]
# => "P"

Nous avons ainsi trouvé la représentation ASCII P du caractère russe П !

Récupération des translittérations dans les fichiers YAML

En conclusion

Une fois les principes d’UTF-8 compris, le travail effectué par Unidecoder n’est plus tout à fait mystérieux.

La lecture du code de la gem peut toutefois révéler quelques lignes de Ruby plutôt intéressantes.

Publié le 15 novembre 2014

Notre vision des choses vous correspond ? Vous avez envie de travailler avec nous ?