Traitement des caractères UTF8

publication: 1 décembre 2023 / mis à jour 4 décembre 2023

Read this page in english

 

C’est en réalisant quelques tests de saisie de caractères au clavier qu’il est apparu un petit souci. Si on fait ceci :

key      \ and press 'a' key, push 97 on stack 

Jusque là, tout est normal. Mais sur le clavier, on dispose aussi, en France sur clavier AZERTY, de caractères accentués et certains caractères comme . Réessayons key en tentant de récupérer le code de ce caractère:

keyok 
226 --> �� 
ERROR: �� NOT FOUND! 

Le premier code récupéré a la valeur 226. mais il y a deux autres codes qui perturbent l’interpréteur FORTH. Voyons cette solution :

key key keyok 
226 130 172

Ah… ?!?!? Trois codes?

Le codage UTF8

Reprenons les trois codes 226 130 172 en hexadécimal : E2 82 AC. Si on fait ceci:

$e2 emit 

Ca affiche � ok. Mmmm…. Vérifions dans un boucle qui est dans l’intervalle 32-255:

: dispChars  ( -- ) 
    256 32 do 
        i emit 
    loop 
  ; 

L’exécution de dispChars affiche ceci:

 !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmno 
pqrstuvwxyz{|}~��������������������������������������������� 
������������������������������������������������������ ok 

eForth Linux a quelques soucis pour afficher des caractères ayant un code ASCII supérieur à 127. Si on refait ce test avec eForth Windows, le même mot dispChars affiche ceci:

 !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmno 
pqrstuvwxyz{|}~ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜø£Ø׃áíóúñѪº¿®¬½¼¡«»░▒▓│┤ÁÂÀ©╣║╗╝¢¥┐ 
└┴┬├─┼ãÃ╚╔╩╦╠═╬$ðÐÊËÈıÍÎÏ┘┌█▄¦Ì▀ÓßÔÒõÕµþÞÚÛÙýݯ´­±‗¾¶§÷¸°¨·¹³²■  ok 

Pour les caractères dont le code ASCII est dans l’intervalle [32..127], les caractères sont identiques.

Pour les caractères dont le code ASCII est supérieur à 127 (7F en hexadécimal), eForth Linux ne sait pas afficher de caractères valides.

Pour afficher le caractère sous eForth Linux, nous avons des solutions simples:

: .eur ( -- ) 
    ." €" ; 

ou

: .€ ( -- ) 
    ." €" ; 

ou

: .eur ( -- ) 
    226 emit  130 emit  172 emit ; 

Mais nous ne réglons pas le problème des caractères dont le code ASCII est supérieur à 127. Pour résoudre ce problème, il faut s’intéresser au codage UTF8, celui utilisé par eForth Linux.

En codage UTF8, les caractères ASCII sont codés sur 7 bits :

Pour tous les autres caractères, le codage est sur 2 ou 3 ou 4 octets:

Pour tous les codes supérieurs à $7F, les premiers bits de poids fort déterminent le nombre d’octets codant un caractère UTF8. Revenons à notre caractère €. Le premier code qui remonte en exécutant key est $E2. En binaire: 11100010. Ici on a trois bits à 1. Ceci signifie que le caractère € est codé sur 3 octets.

Effectuons un test avec le caractère UTF8 𭫷. Une exécution de key fait remonter le code 240, en binaire: 11110000. On a 4 bits à 1. Le caractère 𭫷 est codé sur quatre octets.

Récupérer le code de caractères UTF8 entrés au clavier

L’idée est de détecter le nombre d’exécution de key à exécuter en fonction du caractère saisi au clavier:

Voici le code capable de saisir n’importe quel caractère UTF8:

0 value keyUTF8 
 
: toKeyUTF8  ( c -- ) 
    keyUTF8 8 lshift or to keyUTF8 
  ; 

Le mot toKeyUTF8 reçoit un code clavier sur 8 bits et le concatène au contenu de la valeur keyUTF8. L’idée est de récupérer le codage UTF8 en une seule valeur numérique finale.

\ execute key recursively 
: getKeys ( n -- ) 
    1 lshift dup $80 and    \ test if bit b7 is not null 
    if   recurse            \ re-execute xkey 
    else drop  then         \ otherwise, drop n 
    key toKeyUTF8           \ and execute key 1 or may times 
  ; 

Le mot getkeys traite le code remonté par la première exécution de key. Il exécute le glissement sur 1 bit vers la gauche du contenu d"un octet, puis teste le bit b7 (séquence 1 lshift dup $80 and). Si ce bit est à 1, le mot se ré-exécute (séquence if recurse).

La récursivité permet de contrôler le nombre d’itérations de getKeys sans faire appel à des boucles et tests complexes. La récursivité s’interrompt dès qu’un bit b7 est à 0. La sortie de récursivité s’effectue après then. Le mot getKeys exécutera la séquence key toKeyUTF8 autant de fois qu’il y a d’appels récursifs.

\ key version for UTF8 characters 
: ukey 
    key to keyUTF8 
    keyUTF8 $7F > if                \ if 1st key code > $7F 
        keyUTF8 1 lshift getKeys    \ execute xkey 
    then 
    keyUTF8 
  ; 

Le mot ukey peut maintenant se substituer au mot key pour récupérer le code UTF8 de n’importe quel caractère du jeu de caractères UTF8:

hex 
ukey  .    \ paste €   and , display : E282AC 

Ce que confirme la documentation UTF8 en ligne.

Affichage de caractères UTF8 depuis leur code

Si on regarde la définition du mot emit, on trouve ceci:

: emit 
    >R RP@ 1 type rdrop 
  ; 

La séquence de code RP@ 1 type limite strictement l’affichage d’un caractère code sur un seul octet. Cette séquence hex E282AC emit ne fonctionnera pas. De même:

: uemit 
    >R RP@ 4 type rdrop 
  ; 
\  hex e282ac uemit   display : ��� ok 

Le souci vient de l’ordre des octets d’une valeur numérique. Un dump mémoire de la pile donne ceci:

--addr---  00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F  ------chars-----
0802-00FA  00 00 00 00 00 00 AC 82 E2 00 00 00 00 00 08 01  ......���......

Il faut donc retourner les octets comme une chaussette:

\ reverse integer bytes, example: 
\  hex 1a2b3C --> 3c2b1a 
: reverse-bytes  ( n0 -- ) 
    0 { result } 
    3 for 
        result 100 * to result 
        100 u/mod swap +to result 
    next 
    drop 
    result 
  ; 

on peut maintenant réécrire notre mot uemit:

\ emit UTF8 encoded character 
: uemit ( n -- ) 
    reverse-bytes 
    >r rp@ 4 type 
    rdrop 
  ; 

L’exécution de hex E282AC uemit affiche: .

En conclusion, avec ukey et uemit, on dispose maintenant de mots permettant de traiter des caractères non-ASCII. Ainsi, avec un clavier grec:

hex 
ukey     \ press key Σ 
uemit    \ display Σ 

Encodage depuis le point de code des caractères UTF8

Chaque caractère abstrait se voit associer un nombre unique . Ce nombre est appelé point de code. Le point de code est un nombre compris entre 1 et 17×216, soit potentiellement 1.114.112 signes. Un point de code est noté U+ suivi de la valeur hexadécimale du point de code.

Exemple : U+00E9 pour le caractère é.

Le souci, vous l’avez déjà compris, est qu’on ne peut pas faire hex e9 emit avec eForth Linux.

Pour avoir la bonne séquence d’encodage UTF8 b1-b0 (pour byte1 byte0), il faut basculer les deux bits de poids fort de l’octet e9 vers b1. On découpe e9 comme ceci:

hex  
e9 40 /mod 

Ce qui nous laisse sur la pile de données r et q résultant de l’exécution de /mod, soit dans notre exemple les valeurs 29 et 3. Pour transformer ceci en une valeur sur deux octets, on exécute ensuite:

100 * + 

Ce qui nous laisse maintenant sur la pile de données la valeur hexadécimale 329. Revenons maintenant au format de codage UTF8 sur deux octets:

Ici, en jaune, on a une valeur de masquage, 1100000010000000 en binaire, c080 en hexadécimal. C’est cette valeur de masque c080 qu’on va appliquer au résultat de notre calcul précédent. Voici la séquence complète de codage depuis le point de code e9:

e9 40 /mod 
100 * + 
c080 or 

Ce qui nous laisse le code final c3a9, qui est maintenant utilisable avec uemit:

c3a9 uemit   \ display char : é 

On va maintenant automatiser cela….

Ré-encodage par récursivité

Dans la séquence n 40 /mod, on récupère à chaque itération un reste et un quotient. Quand une itération donne un quotient nul, on arrête. Ceci se prête merveilleusement à un traitement par récursivité:

$40 constant BYTE_DIVISOR 
 
\ split n modulo BYTE_DIVISOR 
: mod40Recombine ( n -- ) 
    BYTE_DIVISOR /mod  
    dup 0 > if 
        recurse 
    then 
    $100 * + 
  ; 

Le mot mod40Recombine découpe la valeur n en paires r q. Si q est égal à 0, on quitte la récursivité après then et on exécute $100 * + autant de fois que n a été découpé.

Il reste à appliquer un masque en fonction de la taille du point de code ré-encodé. Voici les valeurs limites pour les encodages de deux, trois, ou quatre octets:

$8000     constant LIMIT_2_BYTES 
$10000    constant LIMIT_3_BYTES 
$200000   constant LIMIT_4_bytes 

Pour chacune de ces valeurs limites, voici les masques à appliquer:

$C080       constant MASK_2_BYTES 
$E08080     constant MASK_3_BYTES 
$F0808080   constant MASK_4_BYTES 

Et pour finir, voici le mot bytesToUTF8 qui applique le madque adapté à la taille du numéro de point de code:

: bytesToUTF8 ( n -- n' ) 
    >r 
    r@ LIMIT_2_BYTES < if 
        r> mod40Recombine 
        MASK_2_BYTES OR 
        exit 
    then 
    r@ LIMIT_3_BYTES < if 
        r> mod40Recombine 
        MASK_3_BYTES OR 
        exit 
    then 
    r@ LIMIT_4_BYTES < if 
        r> mod40Recombine 
        MASK_4_BYTES OR 
        exit 
    then 
    abort" UTF8 conversion failed" 
  ; 

Il y a certainement moyen de faire plus élégant. Cette définition a le mérite de fonctionner. En entrée, on empile le point de code à ré-encoder. En sortie, on obtient le code UTF8 utilisable par uemit.

Générer une table de caractères UTF8

L’idée est de prendre les premier et dernier numéros de point de code dans une table de caractères. Ces valeurs sont traitées dans une boucle permettant de générer une table des caractères.

Commençons par quelques mots utiles:

8 constant LINE_LIMIT 
 
\ CR only if i MOD = 0 
: cr? ( i -- ) 
    1+ LINE_LIMIT mod 
    0= if  
        cr 
    then 
  ; 
 
\ display hex value format NNNN 
: .###### ( n -- ) 
    <# # # # # # # #> type 
  ; 

Le mot cr? exécute un retour à la ligne si l’affichage atteint n colonnes.

Le mot .###### affiche une valeur n sur 6 chiffres. Voici enfin la boucle d’affichage:

: utf8Set { start stop -- } 
    base @ { currentBase } 
    hex 
    stop 1+ start do 
        i .###### 
        space  
        i bytesToUTF8 uemit 
        2 spaces 
        i cr? 
    loop 
    currentBase base ! 
  ; 

Voici la définition permettant d’afficher la table des caractères UTF8 du jeu de caractères grecs et coptes:

: greekAndCopt ( -- ) 
    $370 $3ff utf8Set 
  ; 

Son exécution affiche ceci:

Les chiffres indiquent le point de code de chaque caractère. Ici, on voit par exemple que le caractère φ a le point de code 3D5.

En conclusion, à quoi ça sert?

Premièrement, ça sert à comprendre l’encodage UTF8.

Ensuite, on peut s’inspirer d’une partie de ce code pour compter le nombre de caractères. Exemple:

s" nb: φ" 
\ display : 
134495848 6 

A vue d’œil, on a pourtant 5 caractères, alors que eForth indique une longueur de chaîne de 6 caractères!

Dans une procédure de tri alphanumérique, il peut s’avérer nécessaire de transformer certains caractères accentués en leur équivalent sans accent: à → a, é → e, etc.

Je vous laisse toute liberté pour trouver une application pratique.


Legal: site web personnel sans commerce / personal site without seling