Sommaire
- Tchars, un Dchars pour Troff
- Du Beta-code à l'unicode
- De l'unicode à l'utf8 : généralités
- De l'unicode à l'utf8 : implémentation
- Conclusion
Vous n'avez pas manqué de suivre ici les aventures de Dchars, de notre ami Xavier Faure. Dchars permet entre autres de transformer une suite codifiée de caractères ascii en caractères utf8 complexes. Par exemple, il transforme le beta-code en grec polytonique. Dans ce journal, vous découvrirez l'histoire de Tchars, le sosie de Dchars.
Tchars, un Dchars pour Troff
Ayant justement besoin d'écrire quelques mots en grec polytonique dans ma thèse, je me suis penché dans les sources de Dchars, pour voir si je ne pouvais pas le modifier pour en faire un pré-processeur pour troff, mon logiciel de formatage de texte, et l'ajouter aux logiciels du projet Utroff. Malheureusement, Dchars est écrit en python, et je rechignais à ajouter python aux dépendances d'Utroff.
Plutôt que de modifier Dchars, je me suis donc lancé dans une ré-écriture en C des fonctionnalités de Dchars qui m'intéressaient. Et cela m'a beaucoup amusé, puisque ça a été l'occasion pour moi de comprendre un peu Unicode, l'utf8, l'hexadécimal et les opérations binaires. Il me semble que cela vous amusera beaucoup aussi, alors puisqu'il me faut publier quelque part ce savoir encore frais pour le jour prochain ou je l'oublierai, entrons dans les entrailles de Tchars.
Tchars, qui signifie Troff|Translate characters, a beaucoup moins de fonctionnalités que Dchars. Mais il fait bien son travail qui consiste à transformer une forme simplifiée de beta-code en grec polytonique. C'est un programme finalement assez simple, écrit en 600 lignes de C, dont 300 ne font que définir les correspondances entre le beta-code et le code unicode.
Du Beta-code à l'unicode
Cette liste de correspondances est d'ailleurs générique car générée automatiquement. La fondation unicode fournit en effet un fichier UnicodeData.txt qui liste l'ensemble des caractères unicode, et indique entre autres leur nom ainsi que leur code hexadécimal. J'ai donc commencé par écrire un script shell appuyé sur sed qui filtre ce fichier pour en extraire les caractères grecs (repérés par le nom GREEK), effacer les lignes inutiles, substituer les noms complets par leur correspondance en beta code (s/SMALL LETTER ALPHA/a/g;), substituer le nom des tons par leur correspondance en beta code (s/DIALYTIKA/+/g;), mettre le tout en forme, et trier la liste pour l'utiliser dans une structure en C. Au final, j'ai une longue liste de 300 lignes de correspondances entre code ascii et valeur hexadécimale, de la forme :
struct runelist
{
char *tag;
unsigned hexa;
};
runelist alphabeta[] =
{
/* This list must be sorted */
{"A", 0x0391,},
{"A&", 0x1FB9,},
{"A'", 0x1FB8,},
{"A(", 0x1F09,},
{"A(/", 0x1F0D,},
{"A(=", 0x1F0F,},
{"A(\\", 0x1F0B,},
{"A)", 0x1F08,},
{"A)/", 0x1F0C,},
{"A)=", 0x1F0E,},
{"A)\\", 0x1F0A,},
{"A/", 0x0386,},
{"A/", 0x1FBB,},
{"A\\", 0x1FBA,},
}
De l'unicode à l'utf8 : généralités
Sur cette base, il a été possible d'écrire le code C. L'ensemble est assez banal : une fonction lit le texte en entrée, repère les lignes où il y a du code à translittérer, et pour chaque suite de caractères en beta-code, un binary search recherche le code hexadécimal correspondant. On pourrait en rester là, car Troff peut transformer lui-même ce code hexadécimal en un caractère utf8. Mais lorsque j'ai compris qu'écrire une fonction qui fasse cette transformation n'était pas hors de ma portée, je me suis jeté à l'eau.
Pour comprendre la dite fonction, il faut savoir qu'un caractère utf8 est encodé sur 1, 2, 3, 4, 5 ou 6 bytes, selon la valeur de son code hexadécimal :
First Last Bytes
U+0000 U+007F 1
U+0080 U+07FF 2
U+0800 U+FFFF 3
U+10000 U+1FFFFF 4
U+200000 U+3FFFFFF 5
U+4000000 U+7FFFFFFF 6
Le premier byte d'un caractère utf8 indique combien de bytes composent le caractère. Chacun des bytes suivant commence par les bits 10, suivant cette table :
Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 Byte 6
0xxxxxxx
110xxxxx 10xxxxxx
1110xxxx 10xxxxxx 10xxxxxx
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
La table hexadécimale nous explique comment écrire chacun de ces bytes sous forme hexadécimale :
0 0000 8 1000
1 0001 9 1001
2 0010 A 1010
3 0011 B 1011
4 0100 C 1100
5 0101 D 1101
6 0110 E 1110
7 0111 F 1111
1100 0000 = C0
1110 0000 = E0
1111 0000 = F0
1111 1000 = F8
1111 1100 = FC
1000 0000 = 80
De l'unicode à l'utf8 : implémentation
Ainsi, pour chaque code unicode indiqué en hexadécimal, il faut commencer par repérer de combien de bytes est composé le caractère. Pour cela, il suffit de quelques tests :
void
hexatochars(unsigned hexa)
{
if (hexa < 0x0080) {
/* 1 byte */
}
else if (hexa < 0x800) {
/* 2 bytes */
}
else if (hexa < 0x10000) {
/* 3 bytes */
}
else if (hexa < 0x200000) {
/* 4 bytes */
}
else if (hexa < 0x4000000) {
/* 5 bytes */
}
else if (hexa < 0x7FFFFFFF) {
/* 6 bytes */
}
else {
/* hors unicode */
}
}
Sitot le nombre de bytes à construire connu, il n'y a plus qu'à construire ces bytes un par un, par des opérations binaires. L'opération générale consiste à décaler, mettre à zero, combler ces zeros. Prenons l'exemple d'un caractère unicode à deux bytes. Il aura la forme : 110xxxxx 10xxxxxx. On commence par compter le nombre de petits x : il y en a 11, ce qui signifie que le code hexadécimal du caractère unicode a donc 11 bits : xxxxxxxxxxx. Pour composer le premier byte, on a besoin des 5 premiers bits. On décale donc tout vers la droite, pour en mettre 6 dehors et obtenir 000xxxxx :
hexa >> 6
Il faut en outre combler les trois premiers zeros par les trois premiers bits du premier byte, pour obtenir 110xxxxx. On utilise l'opérateur binaire | et le byte 11000000 (soit 0xc0 en hexadecimal) :
((hexa >> 6) | 0xc0)
Pour composer le second byte, on a besoin des 6 derniers bits du code hexadecimal. On ne conserve donc que les les 6 derniers avec l'opérateur binaire & et le byte 0011 1111 (soit 0x3F) :
hexa & 0x3F
Puis on impose les deux premiers bits au second byte, avec l'opérateur binaire | et le byte 10000000 (soit 0x80) :
((hexa & 0x3F) | 0x80)
Petit à petit, on obtient la fonction suivante :
void
hexatochars(unsigned hexa)
{
char a=0, b=0, c=0, d=0, e=0, f=0;
/*
** From U+000 to U+007F
** Utf8 is coded on 1 byte of the form :
** 0xxxxxxx
*/
if (hexa<0x0080) {
a = hexa; // 0xxxxxxx
}
/*
** From U+0080 to U+07FF
** Utf8 is coded on 2 bytes of the form :
** 110xxxxx 10xxxxxx
*/
else if (hexa < 0x800) {
a = ((hexa >> 6) | 0xC0); // 110xxxxx
b = ((hexa & 0x3F) | 0x80); // 10xxxxxx
}
/*
** From U+0800 to U+FFFF
** Utf8 is coded on 3 bytes of the form :
** 1110xxxx 10xxxxxx 10xxxxxx
*/
else if (hexa < 0x10000) {
a = ((hexa >> 12) | 0xE0); // 1110xxxx
b = (( (hexa >> 6) & 0x3F) | 0x80);
c = ((hexa & 0x3F) | 0x80);
}
/*
** From U+10000 to U+1FFFFF
** Utf8 is coded on 4 bytes of the form :
** 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
*/
else if (hexa < 0x200000) {
a = ((hexa >> 18) | 0xF0); // 11110xxx
b = (((hexa >> 12) & 0x3F) | 0x80);
c = (((hexa >> 6) & 0x3F) | 0x80);
d = ((hexa & 0x3F) | 0x80);
}
/*
** From U+200000 to U+3FFFFFF
** Utf8 is coded on 5 bytes of the form :
** 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
*/
else if (hexa < 0x4000000) {
a = ((hexa >> 24) | 0xF8); // 111110xx
b = (((hexa >> 18) & 0x3F) | 0x80);
c = (((hexa >> 12) & 0x3F) | 0x80);
d = (((hexa >> 6) & 0x3F) | 0x80);
e = ((hexa & 0x3F) | 0x80);
}
/*
** From U+4000000 to U+7FFFFFFF
** Utf8 is coded on 6 bytes of the form :
** 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
*/
else if (hexa < 0x7FFFFFFF) {
a = ((hexa >> 30) | 0xFC); // 1111110x
b = (((hexa >> 24) & 0x3F) | 0x80);
c = (((hexa >> 18) & 0x3F) | 0x80);
d = (((hexa >> 12) & 0x3F) | 0x80);
e = (((hexa >> 6) & 0x3F) | 0x80);
f = ((hexa & 0x3F) | 0x80);
}
/*
** Out of unicode range
*/
else {
fprintf(stderr,
"tchars : [%x] is not in unicode range, file xxx, line %d\n",
hexa, l);
}
printf("%c%c%c%c%c%c", a, b, c, d, e, f);
}
Conclusion
Il existe probablement une bibliothèque standard du C qui fait cette transformation: j'ai cherché un peu, mais ne l'ai pas trouvée… Si vous la connaissez, ou si, simplement, vous avez des choses à redire sur le code ci-dessus, utilisez les commentaires, je serais enchanté d'en apprendre un peu plus.
Quoi qu'il en soit, Dchars a maintenant un compagnon, et Utroff intègre dorénavant un interpréteur de beta-code facilement extensible à d'autres langages.
Pour le reste, vous pouvez consulter, modifier, publier tchars (licence BSD à deux clauses) ou tout simplement le tester en téléchargeant l'archive d'Utroff
- Dchars : http://94.23.197.37/dchars/doc/fr/index.html
- La liste des caractères unicodes : http://www.unicode.org/Public/5.0.0/ucd/UnicodeData.txt
- Sur l'UTF-8 : https://en.wikipedia.org/wiki/UTF-8
- Sur les opérateurs binaires du C : http://www.bien-programmer.fr/bits.htm
- Readme de Tchars : http://utroff.org/man/tchars-readme.html
- Manuel de Tchars : http://utroff.org/man/tchars.html
- L'archive d'Utroff contenant Tchars : http://download.tuxfamily.org/utroff/pub/
# Meilleur traduction vers l'utf8
Posté par benoar . Évalué à 5.
On peut faire un peut mieux que répéter les shift & masques dans ta fonction
hexatochars()
:J'ai écrit les constantes différemment car je trouve que ça permet de mieux se rendre compte que les intervalles proposés par ce standard encodent 7, 4, 5, 5, 5 et 5 bits chacun, pour un total 31 bits pour un “code point”. En plus, avec le fait qu'on shift toujours de 6 bits, ça m'avait embrouillé sur le calcul, mais en fait ça marche exactement comme ça car chaque octet qui s'ajoute vient rétrécir d'un bit la place qu'on a dans le premier (sauf au premier ajout, où on perd 2 bits car on ne peut pas prendre le préfixe « réservé » 0b10xxxxxx)
[^] # Re: Meilleur traduction vers l'utf8
Posté par Sygne (site web personnel) . Évalué à 3.
C'est beaucoup plus élégant effectivement. Tellement élégant que je ne comprends d'ailleurs pas encore bien comment ça marche: tout ça n'est pas familier pour moi, et il me faudra encore un peu de temps pour bien comprendre ton algorithme.
[^] # Re: Meilleur traduction vers l'utf8
Posté par Pierre Tramonson . Évalué à 4.
Moi non plus je vois pas très bien comment ça marche mais remplacer des else if par des if à la queue-leu-leu c'est uxamifiant.
[^] # Commentaire supprimé
Posté par Anonyme . Évalué à 5.
Ce commentaire a été supprimé par l’équipe de modération.
[^] # Commentaire supprimé
Posté par Anonyme . Évalué à 4.
Ce commentaire a été supprimé par l’équipe de modération.
[^] # Commentaire supprimé
Posté par Anonyme . Évalué à 10. Dernière modification le 31 octobre 2013 à 18:38.
Ce commentaire a été supprimé par l’équipe de modération.
[^] # Commentaire supprimé
Posté par Anonyme . Évalué à 3. Dernière modification le 31 octobre 2013 à 19:46.
Ce commentaire a été supprimé par l’équipe de modération.
[^] # Re: Meilleur traduction vers l'utf8
Posté par benoar . Évalué à 2.
Petite correction : oui pour les 5 tests, mais une seule affectation, la dernière, tout les tests échouant. L'ensemble des shifts & masks ne sont exécutés que pour la zone la plus « haute ».
Effectivement, au départ je me disais que ça serait « mieux » niveau lisibilité car il n'y a pas de répétition, mais ça fait un peu étrange quand même ; je comprends le côté uxamifiant.
Pas tant que ça, même s'il y a effectivement beaucoup de branchements.
Idem.
Ah, là, je n'ai jamais exprimé de but dans ce commentaire ;-) j'avais juste une intuition qu'on pouvait faire « mieux », et je voyais des relations entre les différents préfixes et les décalages, j'ai sorti ce bout de code qui exprimait « mieux » le cœur du problème, je trouve. C'est vrai qu'au final il est un peu étrange. Note qu'on pourrait même en faire une boucle, mais pour la « clareté » je l'ai déroulée.
[^] # Re: Meilleur traduction vers l'utf8
Posté par Sygne (site web personnel) . Évalué à 4.
Ce que j'aime bien dans ton algorithme, c'est qu'il nous dit quelque chose de la structure d'utf8. Je suis d'ailleurs curieux de voir la version boucle.
[^] # Re: Meilleur traduction vers l'utf8
Posté par benoar . Évalué à 1.
J'ai retravaillé le code pour qu'il soit « plus clair », et j'ai minimisé l'information « arbitraire » pour ne plus avoir que la taille des codes pour chaque octet. Le résultat est également donné sous forme de tableau plutôt que des variables
a
àf
, où les octets sont à lire dei
à0
. Ce qui donne :Ça doit sûrement paraître uxamifiant à certains, et ce code n'a pas été testé, mais je pense qu'il est bon (aux casts près, je me rends compte en relisant). Ça n'est pas le top niveau lisibilité, mais niveau quantité d'information donnée et calculée ça doit s'approcher du minimum.
# Bibliothèques
Posté par Damien Thébault . Évalué à 4.
Il existe effectivement déjà des bibliothèques pour faire ce que tu veux en C.
Les deux premières qui pourraient fonctionner sont iconv et icu (uconv).
Dans les deux cas, tu peux partir d'UCS-4 (≃UTF-32) ou wchar et arriver en UTF-8.
En plus en bonus tu as la possibilité de fonctionner avec autre chose que de l'UTF-8 en sortie.
[^] # Re: Bibliothèques
Posté par Sygne (site web personnel) . Évalué à 3.
Si tu as un exemple, je suis preneur, car j'ai quand même cherché du côté de wchar, et suite à ton commentaire j'ai regardé la page de manuel d'iconv(3), et je t'avoue que je ne vois toujours pas bien quelles fonctions dans ces bibliothèques acceptent l'hexadecimal d'unicode en entrée.
[^] # Re: Bibliothèques
Posté par Damien Thébault . Évalué à 4.
Voilà un petit exemple vite-fait avec iconv, qui prend de l'UCS-4 ("hexadécimal") en entrée et qui sort de l'UTF-8.
[^] # Re: Bibliothèques
Posté par Sygne (site web personnel) . Évalué à 2.
Merci pour l'exemple (et pour l'UCS-4, dont j'ignorais l'existence).
Un jour, il faudra bien que j'étudie un peu les bibliothèques du C…
# Code similaire pour l'UTF-16
Posté par calandoa . Évalué à 3.
J'avais eu le même problème il y a quelque temps, donc j'avais codé ce même genre de fonctions, à la diffrence que la conversion ne se fait qu'entre l'UTF-16 et l'UTF-8, et de l'UTF-8 vers le Latin 1 (ISO-8859-1).
https://www.assembla.com/code/dittox/subversion/nodes/34/trunk/parcittox/dittox/dittox.cpp
Chercher à partir de la fonction
conv_utf16_to_8
. Le code est sous domaine publique.[^] # Re: Code similaire pour l'UTF-16
Posté par Sygne (site web personnel) . Évalué à 3.
Trouvé:
C'est marrant ces petites nuances qui font la différence…
# félicitations !
Posté par Xavier Faure (site web personnel) . Évalué à 7.
Bonsoir, je suis l'auteur de DChars et j'écris ces lignes le sourire aux lèvres : quel bonheur que de voir d'autres que moi libérer leur code et le faire savoir de façon aussi intéressante. C'est vraiment le libre au sommet de sa forme : efficacité du code (je parle de TChars pour le coup) et courtoisie maximale. Longue vie à ce projet !
Si je puis être utile en quoi que ce soit à Sygne, qu'il me le fasse savoir : j'ai beaucoup apprécié cette dépêche !
Trust the Python !
[^] # Re: félicitations !
Posté par Sygne (site web personnel) . Évalué à 5. Dernière modification le 03 novembre 2013 à 11:15.
Salut !
C'est ta dépêche sur Dchars qui m'avait appris l'existence du beta-code, que j'ignorais jusqu'alors. J'ai ensuite pris beaucoup de plaisir à lire ton code et ta doc, tous deux très soignés.
Puisque tu es là, et que tu m'y invites, permets-moi ces quelques questions :
Comment ce fait-il que tu aies tant de projets tournant autour des langues, en particulier anciennes (Phoseg, Dchars, Logotheras, Phokaia) ? Est-ce un hobby, un besoin professionnel ou associatif ?
Aurais-tu entendu parler d'une méthode de translittération simple du coréen (hangul) ?
[^] # Re: félicitations !
Posté par Xavier Faure (site web personnel) . Évalué à 4.
Bonjour Sygne,
je suis enseignant de "Lettres Classiques" (français, latin, grec) au collège et cela fait longtemps que je veux contribuer au libre pour faciliter la transmission des langues que j'enseigne et que j'aime. La situation actuelle est pour moi comparable à celle de la Renaissance : les technologies évoluent et seuls les contenus qui s'adaptent aux nouvelles technologies survivront. J'essaye, très modestement, de participer à ce grand mouvement.
Merci pour ta remarque sur la qualité de la documentation dans mon code : ayant jusqu'à peu codé seul, j'avais besoin d'aide pour être capable de relire mon code; écrire de la doc' me permet surtout de réutiliser mon code, sinon, je n'y comprends rapidement plus rien.
Quant au hangul, désolé, je n'y connais rien. Mais tout est relatif, si je peux t'aider sur un projet concernant le coréen, peut-être puis-je t'aider ?
Trust the Python !
[^] # Re: félicitations !
Posté par Sygne (site web personnel) . Évalué à 4.
Tout s'explique ! On devrait former un club de lettrés développeurs !
Quant au coréen, je me demandais si tu n'avais pas travaillé dessus pour Dchars. C'est une écriture alphabétique, et il existe une translittération officielle vers l'alphabet occidental, mais celle-ci est fortement marquée par la phonétique, et prend en compte des changements de prononciation qui ne s'écrivent pourtant pas. Et de fait cette translittération officielle me semble difficile à implémenter, outre qu'elle est perturbante pour moi qui ai un peu l'habitude du coréen écrit.
J'ai cherché après d'autres potentielles translittérations, mais sans résultat, d'où ma question.
# Correspondance béta-code -> hexa
Posté par barmic . Évalué à 4.
Je trouve dommage d'avoir codé en dur la correspondance. En utilisant un fichier, il me semble qu'il aurait pu facilement devenir un traducteur beta code générique (de ce que je vois sur wikipedia le beta code ne sert pas qu'au grec ancien). Le traitement d'un fichier serait trop long face au traitement des fichiers ? Ou pense-tu qu'il vaudrait mieux coder en dur l'ensemble du beta code (ça doit être limité et stable) ?
Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)
[^] # Re: Correspondance béta-code -> hexa
Posté par Sygne (site web personnel) . Évalué à 4.
C'est vrai que ce serait pratique que ce soit configurable, surtout s'il s'agit d'ajouter des méthodes de translittérations vers d'autres langages.
Le traitement d'un fichier ne serait pas trop long : il suffirait de produire à la volée la même structure que celle qui est ici codée en dur, ce qui ne coûte pas grand chose, d'autant plus qu'il n'y a aucune raison de courir après les performances. Il faut juste voir si cela reste pertinent s'il y a de nombreuses méthodes de translittérations. Et il faudrait aussi que je sois un peu plus confiant en ma capacité à gérer la mémoire en C pour me lancer dans l'aventure. Mais ce serait une bonne occasion de m'entraîner.
Quant au beta-code, c'est une norme un peu bizarre: Elle est en vérité très longue (cf. deuxième liens externe de wikipédia) et de nombreux caractères sont codés par un numéro, ce qui fait qu'il n'y a pas grand intérêt à préférer la valeur beta-code plutôt que la valeur unicode pour ces caractères ; et même pour le grec ancien, les majuscules sont indiquées par "*A" et les minuscules par "A", ce qui n'est pas des plus pratique.
Tchars s'inspire de beta-code plus qu'il ne le respecte, et Dchars, s'il sait en interpréter correctement une partie, propose d'autres méthodes de translittérations plus intuitives.
# 33% d'optimisation
Posté par Ignatz Ledebur . Évalué à 3.
J'avoue avoir eu une petite suée en lisant qu'UTF-8 se codait sur jusqu'à 6 octets. Heureusement, la page wikipédia m'a vite confirmé dans mes certitudes (faut pas m'faire des peurs comme ça):
Tu peux donc te passer du traitement des 2 derniers octets, rien n'étant codé sur plus de 4 octets.
Sinon, je suis bien content de voir que le projet Utroff continue à se développer. :)
[^] # Re: 33% d'optimisation
Posté par Sygne (site web personnel) . Évalué à 3.
Heureusement que tu es là, et que, contrairement à moi, tu lis autrement qu'en diagonale.
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.