Traitement des caractères UTF8
publication: 1 décembre 2023 / mis à jour 4 décembre 2023
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:
key € ok 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 key € ok 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 :
0bbb-bbbb
codage sur 1 octet
Pour tous les autres caractères, le codage est sur 2 ou 3 ou 4 octets:
110b-bbbb 10bb-bbbb
codage sur 2 octets1110-bbbb 10bb-bbbb 10bb-bbbb
codage sur 3 octets1111-0bbb 10bb-bbbb 10bb-bbbb 10bb-bbbb
codage sur 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:
- on exécute un premier
key
- si le code est supérieur à 127, on fait glisser ce code de 1 bit vers la gauche, puis on teste le bit b7.
Si ce bit est à 1 on re-exécute
key
.
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