Sonntag, 27. Mai 2012

Entwicklung von Rashumon - Die Geheimnisse

Als ich Rashumon entwickelte, gab es keine Unterstützung für mehrsprachige / bidirektionale Texte und ich musste diese von Grund auf neu programmieren.

Hintergrund

Während der Jahre 1989 bis 1994 entwickelte ich Rashumon, das erste mehrsprachige, grafische Textverarbeitungsprogramm für den Amiga. Rashumon brachte einige einzigartige Funktionen:
  • Mehrfache Auswahl von Text (gleichzeitige Auswahl von nicht zusammenhängenden Textteilen)
  • Tabellengenerator
  • Unterstützung mehrfacher Tastaturbelegungen (bis zu 5 gleichzeitig)
  • Suchen und Ersetzen einschließlich Farbe, Stil und Fontfilter
    Rashumon - Search / Replace
  • Mehrsprachige String Gadgets zur Erzeugung und Umbenennung von Dateien, Schubladen etc.
  • Import und Export mehrsprachiger ASCII Dateien von und zu PC und MAC
  • Ultraschnelle Bildschirmaktualisierung und Scrolling
  • IFF Grafik Unterstützung (Import und Export)
  • Direkter Zugriff auf jedes der 256 Zeichen jedes einzelnen Fonts

Verwendung des Codes

Die in diesem Artikel verwendeten Codebeispiele wurden aus dem Rashumon Sourcecode entnommen und können von jedem C++ Compiler geprüft werden, obwohl sie für den Amiga Aztec C compiler erstellt wurden.

Sehenswertes

Heutzutage neigen wir dazu, einiges von der Komplexität zu vergessen, welche Teil des Progammierens vor 20 Jahren war und heute als Teil im SDK eines jeden Betriebssystems mit eingebaut ist, darunter: bidirektionales Editieren, Textbearbeitung im Allgemeinen, Scrollen von Text und Zeilenumbrüche. Die Entwicklung eines mehrsprachigen Textverarbeitungsprogramm für den Amiga damals in 1989, erforderte in der Tat, das Schreiben von Teilen, die heute Teil eines Betriebssystems sind, aber damals fehlten.

Entwicklung von  Rashumon

Der Amiga war und ist noch immer ein großartiger Computer mit großartigen Fähigkeiten, insbesondere wenn es um Sound und Video geht. Wie dem auch sei, grundlegende Element, wie eine Dateiauswahl-Dialogbox fehlten, ganz zu schweigen vom Support für linksläufige Sprachen. Heute beinhalten alle Betriebssysteme die notwendigen Kernelemente, um mehrsprachige Textbearbeitung zu unterstützen. Der Text wird in der Reihenfolge abgespeichert, in der er geschrieben wurde und rückwärts angezeigt, wenn es sich um eine linksläufige Sprache handelt. Dies macht das Editieren und Manipulieren einfach und leicht, da der Speicher die logische Reihenfolge des Textflusses widerspiegelt. Damals war es notwendig, solche Bausteine zu entwickeln und es hätte meine Textverarbeitung zu langsam gemacht, wenn ich den Text anders dargestellt hätte, als er abgespeichert wurde. Stattdessen wollte ich meinen eigenen Zeilenumbruchmechanismus entwickeln. Zeilenumbruch ist der Mechanismus, der das Umbrechen der Zeilen erlaubt, ohne die Wörter zu zertrennen. Anders als die alte Schreibmaschine, wo man an das Ende einer Zeile kommt und manchmal ein Wort in der Mitte trennen muss, sind Textverarbeitungsprogramme in der Lage, das zuletzt geschriebene Word in die nächste Zeile zu verschieben, sollte in der aktuellen Zeile nicht genug Platz sein. Dies wird sogar noch komplexer, wenn man es mit proportionalen Schriften zu tun hat, bei denen jeder Buchstabe seine eigene Breite hat und wenn man die Kombination mehrerer Fonts erlaubt. All dies war nicht Teil einer hochentwickelten API, aber erforderte die Berechnung der voraussichtlichen Länge eines gegebenen Textes in Pixeln, unter Berücksichtigung jeder Buchstabenbreite basierend auf dem Buchstaben, dem Font, dem Attribut (fett, kursiv) und der verwendeten Größe zusammen mit den gewählten Rändern. Selbst wenn wir es nur mit einer Richtung (links nach rechts) der Bearbeitung zu tun haben, war das immer noch kompliziert von Grund auf zu programmieren. Für den Anfang, schrieb ich eine Routine, um die Länge einer gegebenen Zeile zu berechnen:

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

Wie Sie sehen können, ist dies ein einfaches Szenario wo "mode" ungleich "GRAPHICS" ist, dann wird die Länge auf Basis der Zeichenanzahl multipliziert mit 8 berechnet (was die Länge eines jeden Zeichens bei unproportionalen Schriften ist). Wenn es an die Bearbeitung bidirektionaler Texte unter Verwendung proportionaler und unterschiedlicher Fonts geht, ist es komplizierter sogar einen einzelnen Buchstaben einzufügen:

// Dies ist eine Routine für das Hinzufügen eines einzelnen Characters, entnommen vom Rashumon Quellcode

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

       c[1]='\0';
       c[0]=chr;
       if(ENGLISH_H) // Left to right text
       {
              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);  
// Hier addieren wir die zusätzliche Größe zur gesamten
// Zeilenlänge in Pixeln
                     HCL->CursLen+=font_width(HCL,CR-1);
              }
              else /* ÜBERSCHREIBEN IN ENGLISCH */
              {
                     // Nun behandeln wir den Überschreibemodus
                     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
       // Hebräischer ( oder Rechts nach Links) Modus
       {
              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);


}


Der nächste Schritt war das Durchführen des Zeilenumbruchs bei bidirektionalen Zeilen. Wie ich erklärte, wurden die Zeilen so dargestellt, wie sie im Speicher abgelegt waren. Der Text  "abcאבג" wurde exakt so abgespeichert, wie er aussieht. Rashumon verwendete Doppelbyte-Zeichen, was bedeutet, dass jedes Zeichen unter Verwendung von 2 Bytes abgespeichert wurde. Dies war, bevor UNICODE erfunden wurde, also war das erste Byte ausreichend um jedes Zeichen in jeder unterstützten Sprache zu speichern. Zu dieser Zeit gab es ASCII-Zeichen in zwei Formen, eine nutzte die Werte von 0 bis 127 und die erweiterte Form nutzte die Werte 0 bis 255. Ich verwendete die erweiterte Form und musste entscheiden, wo die linksläufigen Sprachen zu platzieren waren.

Es gab keinen Standard für linksläufige Sprachen. IBM verwendete die Stellen 128 bis 154, aber ich fand dies problematisch und wählte die Stellen ab 224, was heute die richtige Wahl zu sein scheint, da dies identisch mit der heutigen Darstellung von linksläufigen Sprachen unter Verwendung von 2 Byte Codierungen ist. Also falls ich ein Diskettenimage von 1989 (.ADF-Datei) öffne, erscheinen alle hebräischen Rashumon Dokumente in der korrekten Codierung.

Was das zweite Byte angeht, dies wurde verwendet, um die Zeichenfarbe abzuspeichern (3 Typen, bedeuten bis zu 8 Farben), die Zeichenattribute (fett, kursiv und unterstrichen oder jede Kombination dieser 3), Sprache (links- oder rechtsläufig) und den Font durch Verweis auf einen Index des Fonts innerhalb einer lokalen Liste, die aus der Gesamtliste der verwendeten Fonts für jedes Dokument erstellt wurde.

/* Line structure */
#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 */


Tastenbelegung und Codierung

Die Tastenbelegung wurde als ein Array aller Zeichen je Platz beginnend bei "1" und bis zum Ende des Arrays verwendet. Hier ist ein anderer Teil aus dem Rashumon Quellcode, in dem die Tastenbelegungen definiert werden:

/* HEBREW AND ENGLISH MAPS */
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 ";



Wie Sie sehen können, ist "regmap" die hebräische Codierung, "engmap" ist für lateinischen Text, "shiftmap" für die mittels SHIFT-Taste geschriebenen Zeichen und es gab auch eine Tastaturbelegung für Russisch (und später auch eine für Arabisch dank John Hajjer aus Chicago, der eine Menge Zeit darauf verwendete, um mir bei der Herausgabe einer arabischen Version zu helfen).
Das Umschalten zwischen den beiden Schreibrichtungen wurde mit einem einzigartigen Lineal mit zwei Versionen durchgeführt: links nach rechts  und rechts nach links:


Drücken den Pfeils wechselte die Schreibrichtung.

Das Scrollen von Text

Sogar offensichtliche Dinge, wie das Scrollen mussten damals erfunden werden. Dies beinhaltet die Bestimmung, wie viele Zeilen Text darzustellen sind, basierend auf der Fenstergröße (Amiga Fenster hatten die Fähigkeit, vom Endbenutzer in der Größe verändert sowie maximiert und minimiert zu werden), Darstellung eines Scrollbalkens und Berechnung der Scrollbarteilung, welche proportional zur möglichen Bewegung und dem verfügbaren Raum sein sollte.

scroll(ln,lines)
struct Line *ln;
intlines;
{
       registerSHORT distance,
                     top=TOP,
                     bot=BOT;
#ifDEBUG
       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);
#ifDEBUG
       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
}


Zeilenumbruch von bidirektionalem Text

Aber nun lassen Sie uns zurückkommen auf den Zeilenumbruch von bidirektionalem Text. Im Grunde basierte der von mir zusammen mit Tamer Even-Zohar und ihrem Ehemann Nimrod entwickelte Algorithmus auf der Überprüfung einer gegebenen Zeile und falls diese länger als der Platz zwischen den beiden Spalten ist (Berechnung der Zeilenlänge in Pixeln unter Berücksichtigung jedes Zeichens basierend auf dessen unabhängige Attribute), müssten wir das letzte Wort daraus entfernen und dann erneut die Länge prüfen und so weiter, bis die Zeile innerhalb der Breite der Spalten passt. Die erste zu stellende Frage ist: Wo ist das "letzte" Wort? Wenn es ein linksläufiger Absatz ist, wird das letzte Word als erstes im Buffer erscheinen. In einem solchen Fall verwendete ich die folgende Funktion, welche in der Tat die Größe (in Pixel) es ersten Wortes in einem vorgegebenen Buffer maß. Die folgenden Routinen basieren auf unproportionalem Font, was kompliziert genug ist...

/* returns the len of the first word in s */
#define BLNK(c)      ((c)==' ' || (c)=='\n')
first_wordlen(s,margin,blnks1,blnks2)
char*s;
intmargin, *blnks1, *blnks2;
{
  registerint 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);
}


Falls die Zeile rechtsläufig war, wurde eine andere Funktion verwendet:

last_wordlen(s,blnks1,blnks2,maxlen)
char*s;
int*blnks1, *blnks2, maxlen;
{
  registerint 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);
}


Natürlich entfernen wir nicht nur das letzte Word aus einer Zeile, sondern platzieren das erste Wort der nächsten Zeile zurück, wenn dort Platz verfügbar ist (zum Beispiel, wenn das erste Wort in der aktuellen Zeile gelöscht wird und Platz frei wird), also würde ein weiterer Baustein das nächste Wort (vom Beginn der nächsten Zeile) zurück zum Ende der aktuellen Zeile platzieren.

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


In Rashumon wurde die Schreibrichtung eines Textabsatzes automatisch berechnet, indem die Codierung aller Zeichen in jeder Zeile untersucht und besimmt wurde, welche Richtung vorherrschend ist. Bei den Überlegungen mit Tamar Even Zohar und ihrem Ehemann Nimrod, kamen wir zu der Einsicht, dass sogar das Leerzeichen " " eine Schreibrichtung haben kann und wir mussten entscheiden, ob wir ein hebräisches Leerzeichen zusätzlich zu dem lateinsichen haben wollten. Nun, diese Anforderung wurde ein "Muss", da es für den Zeilenumbruch von Absätzen mit der Kombination mehrerer Sprachen gebraucht wurde. Zum Beispiel:

"Dies ist ein Beispiel für einen Absatz gegenläufigen Sprachen.  זו דוגמה לפסקה עם שילוב של שתי שפות עם כיוונים מנוגדים" 

Der folgende Clip demonstriert, wie bidirektionaler Text von Rashumon editiert wird.

Nun, wenn Sie die Ränder verändern, welches Wort wird in die nächste Zeile "springen" oder in die aktuelle Zeile zurück "springen"? Der einzige Weg dies zu bestimmen ist, zu wissen, welche Richtung jedes Zeichen hat (entweder rechts nach links oder links nach rechts), einschließlich der Sonderzeichen wie Tabulatoren, Leerzeichen, Kommas etc. Rashumon kann immer noch bei Aminet unter diesem Link heruntergeladen werden.
An Article about Rashumon (UK)

Literaturhinweise

Mein Blog (Hebräisch)
Mein Blog (Englisch)

Keine Kommentare:

Kommentar veröffentlichen