mardi 21 avril 2020

Langage C et codage des caractères

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.

Saleté d'accents

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.

Longueur de chaîne

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

  1. Dans vim, avec le curseur sur le «é» tapez en mode normal g8, vous verrez sur la ligne du bas c3 e9.
  2. Utf-8 est un format de transformation (Universal Character Set Transformation Format) entre des caractères unicode et une suite d'octets (exemple : 00e9c3 e9). Mais ce n'est pas là le sujet de cet article.

Le fatras des codages

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.

Une solution

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;
}

Supplique

ā 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)

Nina Catch

«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.»