mardi 21 avril 2020
Nos caractères UTF8 qui deviennent la norme sous linux, sont codés sur 1 à 6 octets. Ceci pose des problèmes entre autres en C avec les fonctions de <string.h> mais aussi avec les URL. Dans cet article j'expose ce que j'en comprends et comment je le résous.
Les signes diacritiques de la langue française posent des difficultés pour les URL, qui n'acceptent que quelques caractères simples (84, je crois).
En UTF8 les caractères ascii sont codés sur un octet, mais les autres sont codés sur plusieurs octets. C'est le cas des lettres accentuées qui sont codées sur deux octets. Le premier est toujours 0xc3 et le second est par exemple 0xa0 pour 'à', ou 0xa9 pour le 'é'.
Voici un programme qui nous débarrasse des accents, c'est à dire qui remplace les lettres accentuées, par les lettres simples de l'alphabet (A-Za-z).
/*
:w | !gcc % -o %< -Wall
!./%<
(c) 2023 Mourad Arnout marnout à free.fr
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <inttypes.h>
#include <assert.h>
#define sz(a) sizeof(a)/sizeof(*a)
void codages();
/*
àâ : case 0xa0: case 0xa2:
ÀÂ : case 0x80: case 0x82:
éèêë : case 0xa9: case 0xa8: case 0xaa: case 0xab:
ÉÈÊË : case 0x89: case 0x88: case 0x8a: case 0x8b:
îï : case 0xae: case 0xaf:
ÏÎ : case 0x8f: case 0x8e:
ô : case 0xb4:
Ô : case 0x94:
ùûü : case 0xb9: case 0xbb: case 0xbc:
ÙÛÜ : case 0x99: case 0x9b: case 0x9c:
ç : case 0xa7:
Ç : case 0x87:
*/
char * // mut be freed
noaccent(char *s)
{
char *ret;
ret = (char *)malloc(strlen(s));
*ret = 0;
char *p = s, *r = ret;
while(*p) {
if((uint8_t)*p == 0xc3)
{
p++;
uint8_t u = (uint8_t) *p;
switch(u) {
case 0xa0: case 0xa2: *r++ = 'a'; break;
case 0x80: case 0x82: *r++ = 'A'; break;
case 0xa9: case 0xa8: case 0xaa: case 0xab: *r++ = 'e'; break;
case 0x89: case 0x88: case 0x8a: case 0x8b: *r++ = 'E'; break;
case 0xae: case 0xaf: *r++ = 'i'; break;
case 0x8f: case 0x8e: *r++ = 'I'; break;
case 0xb4: *r++ = 'o'; break;
case 0x94: *r++ = 'O'; break;
case 0xb9: case 0xbb: case 0xbc: *r++ = 'u'; break;
case 0x99: case 0x9b: case 0x9c: *r++ = 'U'; break;
case 0xa7: *r++ = 'c'; break;
case 0x87: *r++ = 'C'; break;
}
}
else *r++ = *p;
p++;
}
*r = 0;
return ret;
}
int main(int argc, char *argv[])
{
char *s = "àâ, ÀÂ, éèêë,ÉÈÊË, îï, ÏÎ, ô, Ô, ùûü, ÙÛÜ, ç, Ç";
printf("%s\n", s);
char *sansaccents = noaccent(s);
puts(sansaccents);
free(sansaccents);
}
void
codages()
{
char a[][64] = {"àâ", "ÀÂ", "éèêë","ÉÈÊË", "îï", "ÏÎ",
"ô", "Ô", "ùûü", "ÙÛÜ", "ç", "Ç" };
for(int i=0; i<sz(a); i++) {
printf("\n%s : ", a[i]);
for(char *p = a[i]; *p; p++) {
if((uint8_t)*p == 0xc3) p++;
printf("case 0x%x: ", (uint8_t)*p);
}
}
}
Commntaire
La fonction codages
déclarée en 13 et définie en 69, est une fonction utilitaire qui m'a permis de remplir les lignes 41 à 52. Le commentaire qui suit sa déclaration (lignes 15 à 28), est le résultat produit par cette fonction.
La fonction noaccent
scrute les octets qui forment une chaîne de caractère en UTF8. Quand elle tombe sur 0xc3 elle replace 0xc3 et l'octet qui le suit par la lettre ascii simple qui correspond. Par exemple 0xc3 Oxa0 sont remplacés par 'a', ainsi 'à' est remplacé par 'a.
Le C s'accomode tant bien que mal de l'utf-8. On peut lire, écrire, copier, concaténer etc. Ça se gâte dès lors qu'il faut compter les caractères.
Exemple codage1.c :
// codage1.c
#include <stdio.h>
int main()
{
char word[] = "déjà";
printf("%s est affiché correctement, mais,\n", "déjà");
printf("%-8srate\n", "déjà");
printf("encodage de «déjà» en utf-8 : ");
for(char *cp=word; *cp != 0; cp++)
printf("%x ", (unsigned char)*cp);
puts("\n");
}
Résultat :
déjà est affiché correctement, mais,
déjà rate
encodage de «déjà» en utf-8 : 64 c3 a9 6a c3 a0
Dans ce bout de code, la deuxième fonction printf
(ligne 7) devrait laisser quatre espaces entre les mots «déjà» et «rate», or elle n'en laisse que deux.
C'est que la largeur du champ est calculée pour huit char
et le mot «déjà» en compte six : 64 c3 a9 6a c3 a0
.
Remarques
g8
, vous verrez sur la ligne du bas c3 e9
. 00e9
⇔ c3 e9
). Mais ce n'est pas là le sujet de cet article. Oublions l'ASCII et l'ISO 8859-1 des débuts, et concentrons nous sur l'Unicode et ses avatars.
L'unicode est une gigantesque table de caractères regroupant tous les caractères de toutes les langues. Exemples : U+0041
est le A, U+00e9
est le é, U+21a6
est ↦ et U+1d1f1
est le 𝄟, etc.
L'utf-8 est un format de transformation d'une suite d'octets (1 à 4 octets) en caractère unicode. Ainsi 41
fait référence à 0x41
, c3 e9
fait référence à 0xe9
, e2 86 a6
fait référence à 0x21a6
, etc. Cette transformation économise de la place, puisque la majorité des caractères latins (ASCII) sont codés sur un seul octet, mais implique un traitement spécifique pour les autres.
L'utf-8 est conçu de telle sorte qu'à tout endroit du texte on peut lire les caractères suivants.
Le défaut de l'utf-8 est qu'en C, les fonctions de la bibliothèque standard ou de la bibliothèque string
présentent des difficultés pour compter le nombre de caractères.
Nous allons convertir (en mémoire) la chaine de caractères en wchar_t
, et au passage compter le nombre de caractères au sens usuel. La fonction de conversion est mbstowcs
(multi-byte string to wide char string)
Fichier codage2.c :
// codage2.c
#include <wctype.h>
#include <locale.h>
#include <wchar.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
int main(int argc, char *argv[])
{
size_t len, mbslen; // number of multibyte characters in source
wchar_t *wcs; // pointer to converted wide character string
// défault word
char *str = argc == 2 ? argv[1] : "déjà";
// Apply the specified locale
assert(setlocale(LC_ALL, "") != NULL);
//length required to hold str converted to a wide character string
mbslen = mbstowcs(NULL, str, 0);
assert(mbslen != (size_t) -1);
len = strlen(str);
printf("%-12s: %u bytes\n", "str lengh", len);
printf("%-*s: %u characters\n", 12 + len - mbslen,
str, mbslen);
}
Cette fois la largeur de champ est calculée correctement.
str lengh : 6 bytes
déjà : 4 characters
C'est quand même un peu laborieux. Les anglophones n'ont pas ces misères. Eux au moins se contentent des vingt-six lettres de l'alphabet. Nous autres nous gaspillons une touche de clavier pour la lettre «ù» qui n'est utilisée que dans un seul mot : «où». De nombreux linguistes (1)ont suggéré de remplacer tous les accents par un macron. Ce serait bien le moment.
Remarque : J'abuse de la macro assert parce que j'ai la flemme de taper
if( ... ) {
perror( ... );
return EXIT_FAILURE;
}
ā Monsieur Macron, Prēsident de la Rēpublique,
Monsieur le Prēsident,
C'est une affaire entendue, votre mandat tourne en eau de boudin. Afin de laisser une trace positive de votre passage ā l'Ēlysēe, je vous propose de prendre une mesure de simplification de l'orthographe.
Il vous suffira de faire paraītre dans le Journal Officiel une seule ligne disant que dēsormais tous les accents aigu, grave, circonflexe, et trēma pourront être remplacēs par une seule diacritique, le macron.
Et quel panache que cette réforme soit signēe par ... Emmanuel Macron !
Nous pourrions alors utiliser un clavier QWERTY avec une touche unique dēdiēe ā ces saletēs.
Bien ā Vous
P.S. Le texte de cette lettre a ētē ēcrit avec cette convention.
(1)«Aujourd’hui je pose la question : avons-nous besoin de deux accents, l’aigu et le grave ? ... un seul accent, horizontal, qu’on appelle couramment l’accent plat.»