dimanche 27 mai 2012

Les secrets pour développer Rashumon

Lorsque j'ai développé Rashumon, il n'y avait pas de support intégré pour le texte multilingue / bi-directionnel et j'ai dû le développer from scratch.

Etat de l'existant

Entre 1989 et 1994 j'ai développé Rashumon, le premier traitement de texte graphique multilingue pour l'Amiga. Rashumon présentait alors des caractéristiques uniques:
  • Sélection Mulitple de texte (sélectionnant en même temps des parties non continues de texte)
  • Générateur de Table
  • Support multiple de cartes index (jusqu'à 5 simultanément)
  • Chercher et remplacer avec les filtres de couleur, de style et de police 
    Rashumon - Search / Replace
  • Chaînes multilingues à utiliser pour créer et renommer des fichiers, des dessins, etc.
  • Import et export de fichiers ASCII multilingues de et vers PC et MAC
  • Ecran ultra plat défilant
  • Support graphique IFF (import et export).
  • Accès direct à chacun des 256 caractères pour chaque police.

Utiliser le code

Les échantillons de code utilisés dans cet article sont issus du code source de Rashumon;  ils peuvent être lus par n'importe quel compilateur C++, quand bien même ils ont été mis au point d'abord et avant tout pour Amiga Aztec C compiler.

Points intéressants

De nos jours, nous avons tendance à oublier un peu de la complexité qui faisait partie du codage d'il y a 20 ans; aujourd'hui tout est intégré dans n'importe quelle partie de n'importe quel système d'exploitation SDK, y compris: modification bi-directionnelle, modification de texte en général, défilement du texte et  retour à la ligne. A l'époque (1989), développer un Word graphique multilingue pour Amiga était une gageure et avait requis de la programmation de ce qui aujourd'hui ferait partie d'un véritable système d'exploitation.

Développer Rashumon

L'Amiga était et est toujours un bon ordinateur, présentant de grandes capacités,  en particulier concernant le son et la vidéo. Cependant, il manquait des éléments basiques comme  une boîte de dialogue de recherche de fichiers, sans mentionner le support pour les langues se lisant de droite à gauche. Aujourd'hui, tous les systèmes d'exploitation contiennent les éléments principaux requis pour accepter la modification de texte multilingue. Le texte est stocké dans l'ordre où il a été tapé, et affiché à l'envers lorsqu'il s'agit d'une langue se lisant de droite à gauche. Cela le rend simple et facile à modifier et à manipuler, étant donné que le stockage reflète l'ordre logique du flux de texte. Auparavant, il fallait au contraire développer tous ces blocs, et cela rendait mon traitement de texte trop lent, car j'affichais le texte différemment de la façon dont il était stocké. En lieu et place de cela, j'ai donc décidé de développer un outil de retour à la ligne. Le retour à la ligne désigne un mécanisme qui permet de couper des lignes sans couper les mots. A l'inverse d'une vieille machine à écrire, où vous arriviez à la fin de la ligne, et parfois cassiez un mot au milieu, les traitements de texte peuvent déplacer le dernier mot tapé vers la prochaine ligne, dans le cas où il n'y aurait pas assez de place pour celui-ci dans la ligne actuelle. Cela devient encore plus complexe lorsque vous avez affaire à des polices proportionnelles, où chaque caractère possède sa propre largeur, et lorsque vous autorisez la combinaison de plusieurs polices. Tout cela ne faisait pas partie d'une API de haut niveau, mais nécessitait le calcul de la longueur prévisionnelle d'un texte, en pixels, en prenant compte de chaque largeur de caractère basée sur le caractère et sur la police, (gras, italique) et la taille utilisée, en plus des marges sélectionnées. Donc même si nous ne nous occupions que d'un sens de modification(de gauche à droite), c'était tout de même compliqué à développer from scratch. Pour bien comprendre, voici la façon de calculer la longueur d'une ligne donnée que j'ai écrite:

int LLen=(mode==GRAPHICS)?ln->Len:(ln->CharLen=strlen(ln->Txt))*8;

Comme vous pouvez le voir, il y a un scénario simple où le "mode" n'est pas "GRAPHICS", et la longueur est alors calculée en fonction du nombre de caractères multiplié par 8 (qui est la longueur d'un caractère lorsqu'une police mono-espace est utilisée). Concernant la modification d'un texte bi-directionnel en utilisant des polices proportionnelles et multiples, c'est plus compliqué même pour insérer un simple caractère:

// C'est une règle pour ajouter un caracère simple, en partant du code source Rashumon

static put_char(wrap,chr)
BOOL wrap;
UBYTE chr;
{
       UBYTE c[2];
       BOOL update=FALSE;

       c[1]='\0';
       c[0]=chr;
       if(ENGLISH_H) // Texte de gauche à droite
       {
              if(chr>='a' && chr<='z' && map[Header->format >> 11]<2) chr=ucase(chr);
              if(Header->Insert || !HCL->Txt[CR])
              {
                     if(!wrap && HCL->Len+font_width(HCL,CR)>HCL->MaxLen) return();
                     char_insert(HCL,c[0],CR);
                     HCL->CharLen++;
                     CR++;
                     HCL->Len+=font_width(HCL,CR-1);  
// Ici nous ajoutons la taille additionnelle de la ligne globale
// taille en pixels
                     HCL->CursLen+=font_width(HCL,CR-1);
              }
              else /* OVERWRITE IN ENGLISH */
              {
                     // Maintenant nous traitons le mode Overwrite
                     HCL->Txt[CR]=c[0];
                     HCL->Format[CR]=Header->format;
                     if(c[0]==9)
                     {
                           SetFlag(HCL->Format[CR],TAB);
                           HCL->Txt[CR]=SPACE_CHR;
                           SetFlag(MD,TABS);
                     }
                     CR++;
                     calc_charlen(HCL);
                     calc_all(HCL);
                     Clear(HCL);
              }
       }
       else
       // Mode Hebreu ( ou de droite à gauche)
       {
              if(!Header->Insert && CR)
              {
                     CR--;
                     HCL->CursLen-=font_width(HCL,CR+1);
                     HCL->Txt[CR]=c[0];
                     HCL->Format[CR]=Header->format;
                     if(c[0]==9)
                     {
                           SetFlag(HCL->Format[CR],TAB);
                           HCL->Txt[CR]=SPACE_CHR;
                           SetFlag(MD,TABS);
                     }
                     calc_all(HCL);
                     Clear(HCL);
              }
              else
              {
                     if(!wrap && HCL->Len+font_width(HCL,CR)>HCL->MaxLen) return();
                     char_insert(HCL,c[0],CR);
                     HCL->Len+=font_width(HCL,CR);
                     HCL->CharLen++;
              }
       }
       if(HCL->Mode & TABS) calc_all(HCL);
       if(c[0]!=SPACE_CHR && fonts[Header->format >> 11]->tf_YSize>LH(HCL))
       {
              HCL->LineHeight=Header->LineHeight=fonts[Header->format >> 11]->tf_YSize;
              HCL->BaseLine=Header->BaseLine=fonts[Header->format >> 11]->tf_YSize-fonts[Header->format >> 11]->tf_Baseline;
              update=TRUE;
       }
       else
       if(c[0]==SPACE_CHR && HCL->prev && !(HCL->Mode & PAR) && wrap)
       {
              WrapLine(HCL->prev,!(update));
       }
       if(HCL->Len<=HCL->MaxLen && !(update))
       {
              showtext(HCL);
              SetCursor();
       }
       else
       if(wrap)
              FixLine(HCL,!update);

       if(update)
              update_lh(HCL,TRUE);


}


L'étape suivante consistait à réaliser un retour à la ligne pour les lignes bi-directionnelles. Comme je l'ai expliqué, les lignes étaient affichées telles qu'elles étaient stockées en mémoire. Le texte "abc???" était stocké exactement tel qu'il avait l'air. Rashumon utilisait des caractères double octets, signifiant que chaque caractère était stocké en utilisant 2 octets. C'était avant que l' UNICODE soit inventé, et donc le premier octet était suffisant pour stocker n'importe quel caractère dans n'importe quelle langue acceptée. A l'époque, les caractères ASCII avaient deux formes, une forme utilisait les valeurs de 0 à 127 et la forme étendue utilisait  les valeurs de 0 à 255. J'utilisais la forme étendue, et je devais décider où placer les langues de droite à gauche. 

Il n'y avait pas de standard pour les langues de droite à gauche. IBM a utilisé des places de 128 à 154, mais j'ai trouvé cela problèmatique et j'ai choisi les places démarrant à 224, ce qui semble le bon choix aujourd'hui, car c'est identique à la façon dont les langues de droite à gauche sont représentées via l'utilisation de l'encodage double octets. Ainsi, si j'ouvre une disquette de 1989 (fichier .ADF), tous les documents Rashumon en Hébreu possèderont un encodage correct. 

Concernant le 2ème octet, il était utilisé pour stocker la couleur du caractère (3 types, ce qui fait jusqu'à 8 couleurs), les caractéristiques de la police (GrasItalique et souligné, ou toute combinaison entre les 3), le langage (droite à gauche ou gauche à droite) et la police, en indiquant un sommaire de cette police dans une  liste locale créée à partir de la liste entière des polices utilisées pour chaque document.

/* Structure de ligne */
#define COLOR_BIT_1 1             /* 1  */
#define COLOR_BIT_2 2             /* 2  */
#define COLOR_BIT_3 4             /* 3  */
#define UNDL 8                    /* 4  */
#define BOLD 16                   /* 5  */
#define ITAL 32                   /* 6  */
#define SELECT 64          /* 7  */
#define LANG 128           /* 8  */
#define TAB 256                   /* 9  */
#define UNUSED_1 1024             /* 10 */
#define UNUSED_2 2048             /* 11 */
#define FONT_BIT_1 4096    /* 12 */
#define FONT_BIT_2 8192           /* 13 */
#define FONT_BIT_3 16384   /* 14 */
#define FONT_BIT_4 32768   /* 15 */
#define FONT_BIT_5 65536   /* 16 */


Mappage de clés et encodage

Le mappage de clés (key mapping) était utilisé en tant que tableau de tous les caractères par places commençant de "1" à la fin du tableau. Voici une autre partie prise du code source Rashumon, où les mappages sont définis:

/* CARTES ANGLAISES ET HEBREUSES */
unsigned char regmap[] =
       ";1234567890-=\\ 0/'-˜€ˆ...??"[] 123(tm)ƒ‚‹'‰‡ŒŠ",  456 †'?„?Ž-š•. .789 ";

unsigned char engmap[] =
       "`1234567890-=\\ 0qwertyuiop[] 123asdfghjkl;'  456 zxcvbnm,./ .789 ";

unsigned char shiftmap[] =
       "~!@#$%^&*()_+| 0QWERTYUIOP{} 123ASDFGHJKL:\"  456 ZXCVBNM<>? .789 ";

unsigned char shiftrus[] =
       "~!@#$%^&*()_+| 0°¶?±³¸´¨(r)¯{} 123 ²£¥¦§(c)׫:\"  456 ¹·¢µ¡­¬<>? .789 ";

unsigned char rusmap[] =
       “`1234567890-=\\
0׀ײִׁ׃״װָ־ֿ[] 123ְֳֵֶַֹֺֻׂ;’  456 ׳ֲױֱּֽ,./ .789 “;




Comme vous pouvez le voir, "regmap" est l'encodage hébreu, "engmap" est pour le texte latin, "shiftmap" est pour les caractères tapés avec le bouton SHIFT, et il y avait aussi une carte index pour le Russe (et par la suite, une pour l'Arabe aussi, grâce à John Hajjer, de Chicago, qui a passé beaucoup de temps pour m'aider à publier une version Arabe).
Le changement entre les deux sens a été fait en utilisant une règle unique avec deux versions : de gauche à droite et de droite à gauche:

En appuyant sur la flèche, cela changeait le sens de la modification.

Défilement du texte

Même les choses les plus évidentes comme le défilement ont du être inventés dans le temps. Cela comprend l'établissement du nombre de lignes de texte à afficher, en fonction de la taille de fenêtre actuelle (la fenêtre Amiga avait la possibilité d'être redimensionnée par l'utilisateur final, ainsi que maximisée et réduite), affichant une barre de défilement, et calculant la taille de la jauge de la barre de défilement qui devait être proportionnelle au mouvement possible et au défilement disponible.

scroll(ln,lines)
struct Line *ln;
int lines;
{
       register SHORT distance,
                     top=TOP,
                     bot=BOT;
#if DEBUG
       printf("BEFORE: top=%ld (%ld <> TOP=%ld) ",
                     Header->top->num,
                     Header->top->y,TOP+Header->shift);
       printf("bottom=%ld (%ld <> BOT=%ld)\n",
                     Header->bottom->num,
                     Header->bottom->y+LH(Header->bottom),BOT+Header->shift);
#endif
       if(lines>0)
       {
              distance=Header->bottom->next->y+LH(Header->bottom->next)-Header->shift-Header->Height;
              Header->shift+=distance;
              while(Header->top->y<Header->shift)
                     Header->top=Header->top->next;
              Header->bottom=Header->bottom->next;
       }
       else
       {
              distance=-(Header->shift-Header->top->prev->y);
              Header->shift+=distance;
              Header->top=Header->top->prev;
              while(Header->bottom->y+LH(Header->bottom)>Header->Height+Header->shift)
                     Header->bottom=Header->bottom->prev;
       }
       if(distance<100)
              ScrollRaster(rp,0,distance,0,TOP,640,BOT);
       else
              calc_top_bottom(TRUE,0,0);
#if DEBUG
       printf("AFTER: top=%ld (%ld <> TOP=%ld) ",
                     Header->top->num,
                     Header->top->y,TOP+Header->shift);
       printf("bottom=%ld (%ld <> BOT=%ld)\n",
                     Header->bottom->num,
                     Header->bottom->y+LH(Header->bottom),BOT+Header->shift);
#endif
}


Retour à la ligne de texte bi-directionnel

Mais maintenant revenons au retour à la ligne du texte bi-directionnel. A la base, l' algorithme, développé par Tamer Even –Zohar, son mari Nimrod et moi, était basé sur l'examen d'une ligne donnée et si elle était plus longue que la taille entre les deux marges (calculant la largeur de la ligne en pixels, prenant en compte chaque caractère, basé sur ses caractéristiques indépendantes), nous devions en retirer le dernier mot, puis vérifier de nouveau la nouvelle longueur, et ainsi de suite, jusqu'à ce que la ligne soit dans la largeur des marges. La première question à poser est où est le "dernier" mot? Si c'est un paragraphe de droite à gauche, le dernier mot apparaîtra en premier, dans le tampon. Dans un tel cas, j'ai utilisé la fonction suivante, qui en réalité mesure la taille  (en pixels) du premier mot dans un tampon donné. Les calculs suivants sont basés sur une police à espace simple, ce qui est déjà assez compliqué…

/* returns the len of the first word in s */
#define BLNK(c)      ((c)==' ' || (c)=='\n')
first_wordlen(s,margin,blnks1,blnks2)
char *s;
int margin, *blnks1, *blnks2;
{
  register int i, j;
/*     for (i=strlen(s)-1; BLNK(s[i]) && i; i--);
       if ((strlen(s)-(i+1))>1)  s[i+2] = '\0'; */
       for (i=margin; BLNK(s[i]) && s[i]; i++);
       *blnks1 = i;
       for (; !(BLNK(s[i])) && s[i]; i++);
       for (j=i; BLNK(s[j]) && s[i]; j++);
       *blnks2 = j-i;
       return(i);
}


Si la ligne est une ligne de la gauche vers la droite, une fonction différente était utilisée:

last_wordlen(s,blnks1,blnks2,maxlen)
char *s;
int *blnks1, *blnks2, maxlen;
{
  register int i, j;
       if (!strlen(s)) return(0);
       for (i=strlen(s)-1; BLNK(s[i]) && i; i--);
       if (i==0) return(0);
       *blnks1 = (strlen(s)-(i+1));
       for (i=min(maxlen,strlen(s)-1); BLNK(s[i]) && i; i--);
       for (; !(BLNK(s[i])) && i; i--);
       for (j=i; BLNK(s[j]) && j; j--);
       i++;
       *blnks2 = i - j;
       return(strlen(s) - i);
}


Bien sûr nous ne retirons pas que le dernier mot d'une ligne, mais nous replaçons aussi le premier mot de la ligne suivante, lorsqu'il y a de l'espace disponible (par exemple, si le premier mot dans la ligne actuelle est supprimé, et que de l'espace devient disponible), alors un autre bloc de construction replacerait le mot suivant (en partant du début de la ligne suivante) à la fin de la ligne actuelle.

/* copies first word of length len & trailing blanks blnks fron s2 to s1 */
copy_first(s1,s2,len,blnks)
char *s1,*s2;
int len,blnks;
{
       append(s2,s1,strlen(s1)+len+(blnks ? 1 : 0));
       delete1(s2,0,len+(blnks ? 1 : 0));
}


Dans Rashumon, le sens du texte du paragraphe était automatiquement calculé en examinant l'encodage de chaque caractère sur chaque ligne, et en déterminant quel sens dominait. Tout en réfléchissant avec Tamar Even Zohar et son mari Nimrod, nous avons compris que même le caractère d'espace " " peut avoir un sens, et nous devions décider si nous souhaitions avoir le caractère d'espace Hébreu en plus du caractère Latin . Et bien, cette demande est devenue une "obligation", car elle nécessitait un retour à la ligne  des paragraphes avec plusieurs langues combinées. Par exemple: 

"This is an example of a paragraph with opposed direction languages. ו דוגמה לפסקה עם שילוב של שתי שפות עם כיוונים מנוגדים" 

La vidéo ci-dessous montre comment le texte bi-directionnel est modifié par Rashumon.

Désormais, si vous changez les marges, quel mot "sautera" à la ligne suivante, ou "sautera" en arrière vers la ligne actuelle? La seule façon de le savoir, c'est en connaissant le sens de chaque caractère (soit de droite à gauche ou de gauche à droite), y compris les caractères spéciaux comme les tabulations, les espaces, les virgules, etc. Rashumon peut être télécharger sur Aminet sur this link.
An Article about Rashumon (UK)

Pour aller plus loin

Mon Blog (Hebrew)
Mon Blog (English)

Aucun commentaire:

Enregistrer un commentaire