6.12 Manipuler les encodages UTF8, UTF16, UTF32 en CPlusPlus... Et les emojis aussi... - naver/lispe GitHub Wiki

Un code pour les identifier tous

English Version

Lorsque l'on analyse un texte qui contient tout de sorte de caractères bizarres et saugrenus, on peut s'attendre à ce qu'un caractère corresponde à un unique code Unicode... C'est du moins, l'espoir que le néophyte place dans cet encodage.

C'est un beau rêve qui vient vite s'écraser sur le douloureux mur de la réalité...

Surtout, si vous travaillez en C++... Parce que là, le rêve tourne vite au cauchemar...

std::wstring n'est pas portable

Si on lit la documentation, on découvre vite qu'il existe un std::wstring qui a l'air tout indiqué pour manipuler des caractères Unicode.

Un wstring est implanté sous la forme d'une séquence de wchar_t.

Déjà à cette étape de votre découverte de l'Unicode, vous découvrez une chose troublante. Si sur la majorité des machines, un wchar_t est encodé sur 32 bits. Sur Windows, c'est du 16 bits...

  • Windows utilise de l'UTF-16
  • Le reste du monde utilisé de l'UTF-32

UTF-16 sous Windows

Pour la majorité des caractères sous Windows, vous aurez l'équivalence:

1 wchar_t vaut 1 caractère Unicode

Pour les autres caractères, il vous faudra analyser deux wchar_t pour obtenir le bon code Unicode...

Et la majorité des emojis tombe dans cette catégorie. Il faut généralement deux wchar_t pour les encoder...

Ainsi

  • 👨 en UTF-32 est: 128104
  • 👨 en UTF-16 est: 55357 56424

La valeur 128104 est ré-encodée sous la forme de deux wchar_t: 55357 56424.

Cela a un impact sur votre programme, il vous faudra tenir compte de l'OS pour savoir comment manipuler vos chaines de caractères.

UTF-8

Alors pourquoi ne pas utiliser de l'UTF-8... Dans ce cas les chaines sont une simple séquence d'octets sur 8 bits...

C'est UNIVERSEL...

Tout à fait... C'est vrai... Sauf que... Il y a toujours un sauf que.

La manipulation d'une telle chaine n'est pas toujours optimale. Prenons l'exemple suivant: l'été arrive à Cannes, sur la Côte d'Azur.

Quelle est la taille de cette chaine?

Imaginons qu'elle soit enregistrée sous la forme suivante:

std::string s = "l'été arrive à Cannes, sur la Côte d'Azur.";

Si je demande: s.size(), j'obtiens: 46...

Mais me direz-vous, il n'y a que 42 caractères dans cette chaine? J'ai recompté trois fois...

Les octets sous-jacents

Si je regarde ce que s contient vraiment, j'obtiens la chose suivante:

[108,39,195,169,116,195,169,32,97,114,114,105,118,101,32,195,160,32,67,97,110,110,101,115, 44,32,115,117,114,32,108,97,32,67,195,180,116,101,32,100,39,65,122,117,114,46]

Plus exactement, pour chaque caractère accentué, j'obtiens:

  • é : 195,169
  • à : 195,160
  • ô : 195,180

Et pour ceux qui pense que 195 est un accent, ça n'a rien avoir...

  • Tous les caractères dont le code est inférieur à 128, sont des caractères ASCII encodés sur un octet.
  • Pour les caractères Unicode supérieurs, on répartit le code Unicode sur plusieurs octets, au maximum jusqu'à 4. Le premier octet a un codage binaire qui permet de savoir le nombre d'octets nécessaire pour encoder ce caractère.

Voici une fonction C++ qui reconstruit le code Unicode sous-jacent:

unsigned char c_utf8_to_unicode(unsigned char* utf, char32_t& code) {
    code = utf[0];

    //On examine les quatre derniers bits (XXXX....)
    unsigned char check = utf[0] & 0xF0;
    
    switch (check) {
        case 0xC0: //2 octets
            if ((utf[1] & 0x80)== 0x80) {
                code = (utf[0] & 0x1F) << 6;
                code |= (utf[1] & 0x3F);
                return 1;
            }
            break;
        case 0xE0: //3 octets
            if ((utf[1] & 0x80)== 0x80 && (utf[2] & 0x80)== 0x80) {
                code = (utf[0] & 0xF) << 12;
                code |= (utf[1] & 0x3F) << 6;
                code |= (utf[2] & 0x3F);
                return 2;
            }
            break;
        case 0xF0: //4 octets
            if ((utf[1] & 0x80) == 0x80 && (utf[2] & 0x80)== 0x80 && (utf[3] & 0x80)== 0x80) {
                code = (utf[0] & 0x7) << 18;
                code |= (utf[1] & 0x3F) << 12;
                code |= (utf[2] & 0x3F) << 6;
                code |= (utf[3] & 0x3F);
                return 3;
            }
            break;
    }

    //1 octet
    return 0;
}

Cette fonction renvoie aussi la position du dernier caractère utilisé dans l'encodage.

Ce que cette méthode montre surtout, c'est que l'on ne peut détecter les caractères UTF-8 non ASCII qu'en parcourant toute la chaine depuis le début... En effet, on ne peut connaître à l'avance où les caractères multi-octets vont apparaître...

En particulier, la seule façon de connaitre la taille de la chaine en caractères est de la parcourir totalement.

Et la représentation des chaines dans LispE ?

Le choix dans LispE a été d'utiliser les chaines de type: std::u32string, qui sont disponibles depuis C++11.

Ces chaines ont l'avantage d'être composées de caractères sur 32 bits quelque soit la plate-forme.

En fait sur les plate-formes Unix (ou Mac OS), std::wstring est généralement équivalent à std::u32string.

Conversion d'une chaine UTF-16 en une chaine UTF-32

Pour Windows (mais aussi pour échanger des chaines de caractères avec le GUI de Mac OS), nous avons construit quelques méthodes (voir str_conv.h ) pour convertir les chaines UTF-16 en std::u32string, ce qui permet ensuite de manipuler les chaines de la même façon partout.

wstring s = L"L'été à Cannes, sur la Côte-d'Azur";
u32string u;

s_utf16_to_unicode(u, s);

Je dois avouer que l'écriture de la fonction: c_utf16_to_unicode qui est à la base de la conversion entre les deux encodages me donne encore quelques cauchemars. En particulier cette ligne:

r = ((((code & 0x03C0) >> 6) + 1) << 16) | ((code & 0x3F) << 10);

Cette ligne permet d'extraire depuis la représentation interne en bits d'un caractère UTF-16, la partie gauche en bits du caractère Unicode final... Nous ne nous appesantirons pas sur la complexité insensée de cet encodage...

Conversion d'une chaine UTF-8 en une chaine UTF-32

Comme, en général, les chaines de caractères nous parviennent sous la forme d'un encodage UTF-8, nous avons construit une simple méthode qui effectue cette conversion UTF-8 en UTF-32: s_utf8_to_unicode

Elle s'appelle de la façon suivante:

string s = "L'été à Cannes, sur la Côte-d'Azur";
u32string u;

s_utf8_to_unicode(u, s);

Et les emojis

Comme si l'encodage n'était pas déjà assez compliqué, voilà les emojis qui viennent rajouter leur dose de difficulté.

Car un emoji, c'est rarement un seul code Unicode... En fait, c'est une combinaison de codes.

Tout d'abord, la liste des caractères Unicode peut être trouvée ici: table unicode.

Prenons un exemple simple:

  • 🖐 est représenté par le code: 128400
  • 🖐🏽 est représenté par les codes: 128400, 127997

Le deuxième caractère est une combinaison du premier et d'une couleur: 🏽

Et certains caractères peuvent être encore plus riche:

  • 👩🏾‍🚀: 128105, 127998, 8205, 128640
  • 🧑🏿‍❤️‍💋‍🧑🏻: 129489, 127999, 8205, 10084, 65039, 8205, 128139, 8205, 129489, 127995

Très riche... Il est possible que votre navigateur ne puisse tous les afficher...

On fait comment alors?

Nous avons récupéré l'ensemble des codes ici, et nous avons construit la grande table emoji_sequences dans le fichier emojis_alone.h

Puis, nous avons crée une classe: Emojis qui offre la série de méthodes suivantes:

void store()

La méthode store permet de traduire cette table sous la forme de trois automates:

  • un automate UTF-32
  • un automate UTF-16
  • un automate UTF-8

Le premier élément de chaque séquence est stocké dans un dictionnaire correspondant.

Un chemin est constitué d'une séquence d'objets: Emoji_arc.

Une séquence est valide si lors de la traversée de la chaine, on aboutit à un arc dont le champ end est vrai.

bool scan(std::u32string& u, long& i)

Cette méthode identifie une séquence de caractères Unicode composant l'emoji. Il renvoie la position finale du dernier caractère composant cette séquence.

bool get(std::u32string& u, std::u32string& res, long& i)

Cette méthode recopie la séquence complète du caractère dans res et renvoie la position du dernier caractère dans la séquence.

bool store(std::u32string& u, std::u32string& res, long& i)

Cette méthode contient quasiment le même code que get, sauf que les caractères sont ajoutés à res.

Utilisation

Il suffit pour parcourir une chaine d'effectuer la boucle suivante:

//e est un objet de type Emojis
Emojis e;

//Les chaines de type UTF-32 sont précédées d'un 'U'
u32string strvalue = U"éèà123👨‍⚕️👩🏾‍🚀🐕‍🦺";
//une variable de travail
u32string localvalue;

//Nos caractères 
vector<u32string> result;

for (long i = 0; i < strvalue.size(); i++) {
    //si le caractère à la position courante est un emoji
    //alors localvalue le contient.
    //i pointe dès lors sur le dernier caractère de la séquence
    if (!e.get(strvalue, localvalue, i))
        localvalue = strvalue[i];

    result.push_back(localvalue);
}

Voilà, à l'issue de cette boucle, notre chaine aura été découpée en caractère dont certains seront une longue séquence de char32_t.

Mais si je veux de l'UTF-8

En fait, le code sera légèrement différent, nous utiliserons en effet la méthode: get_one_char

Cette méthode récupère un caractère UTF-8 complet, selon qu'il soit sur un octet, deux, trois ou quatre.

//e est un objet de type Emojis
Emojis e;

//Les chaines UTF-8 sont de type: std::string
string strvalue = "éèà123👨‍⚕️👩🏾‍🚀🐕‍🦺";

//une variable de travail
string localvalue;

//Nos caractères 
vector<string> result;

for (long i = 0; i < strvalue.size(); i++) {
    //si le caractère à la position courante est un emoji
    //alors localvalue le contient.
    //i pointe dès lors sur le dernier caractère de la séquence
    if (!e.get(strvalue, localvalue, i))
        get_one_char(strvalue, localvalue, i); //c'est la différence principale

    result.push_back(localvalue);
}

Voilà, vous avez maintenant tout ce qu'il vous faut pour analyser des textes et récupérer les caractères correspondants.

Expérimenter

Nous avons créé un exemple indépendant de LispE qui vous permet de tester directement cette classe.

Il suffit de faire:

make testemoji

Cet exemple est implanté dans le fichier: testemoji.cxx. Il fait appel à une version de la classe Emojis dont l'implantation est dans le fichier suivant: emojis_alone.h. Ce fichier fait aussi appel à: std_conv.h.

Cet exemple analyse une chaine encodée en UTF-8 et en UTF-32, avec quelques exemples de conversion.

Note: La version par défaut de C++ que nous utilisons ici est a minima: C++11.

⚠️ **GitHub.com Fallback** ⚠️