Cours PLURITAL 2009-2010

Cours n° 4 (24 novembre 2009)

Jean-François Perrot

Unicode

    1. Trois axes principaux
      1. Universalité
      2. Abandon de l'équation un caractère = un octet
      3. Une véritable élaboration théorique du concept de caractère

    2. Organisation du catalogue Unicode
      1. Plans et blocs
      2. Structure du BMP
      3. Vue d'ensemble
      4. Moyens exploratoires

    3. Promenade dans le catalogue Unicode
      1. Un coup d'œil aux plans supérieurs
        1. Im Westen nichts Neues
        2. Ex Oriente Lux
      2. Le plan multilingue de base (BMP)
        1. L'alphabet latin et ses extensions
        2. L'alphabet grec, ancien et moderne, et ses dérivés
        3. Les alphabets notant des langues sémitiques : arabe, syriaque, hébreu et éthiopien
        4. Les écritures indiennes
        5. Le cas du japonais
        6. Les caractères chinois et la question de la Han Unification

    4. Représentation en machine : généralités
      1. UTF-32, UTF-16, UTF-8
      2. Quel est le but poursuivi ? Travail local ou communication à distance ?

    5. UTF-8 : principe et mise en œuvre
      1. Principe
      2. Notation et expérimentation :
      3. Mise en œuvre



Résumé
: Qu'est-ce qu'Unicode ? (par lui-même) et Wikipedia (en anglais : la page française est plus développée, voir plus loin)

Références livresques :
Références en ligne :
  1. Trois axes principaux

    Le standard Unicode met en exergue 10 principes, dont l'exégèse ne manque pas d'intérêt
    (Desgraupes, chap. 1,  Haralambous, chap. 2).
    De notre point de vue d'utilisateurs, je retiens les trois points suivants :

    1. Universalité

      • Tous les systèmes d'écriture textuelle du monde (pas tous les répertoires de signes).
      • Convergence avec la norme ISO/IEC 10646.
      • Négociation permanente avec les instances réglementaires,
        mais le consortium reste fermement entre des mains américaines...
    2. Abandon de l'équation un caractère = un octet

      • Un total de 1 114 112 caractères potentiels (= 17 ×  65.536, soit 17 plans de 216 places chacun)
        En hexadécimal, de 0x000000 à 0x10FFFF.
        (Donc en principe 3 octets, mais les  ordinateurs ne raisonnent que par puissances de 2)

      • Pour la pratique courante le premier plan suffit ! Il s'appelle BMP = Basic Multilingual Plane
        Le BMP est caractérisé par le fait que les numéros Unicode correspondants tiennent dans deux octets.
        C'est heureux, car la plupart des langages de programmation qui fondent sur Unicode leur notion de caractère,
        à commencer par Java, se limitent à 16 bits - ce qui correspond à l'intention initiale !

      • Mais des domaines commencent à émerger où le recours aux plans supplémentaires s'impose.
        Nous en verrons un exemple avec le traitement des caractères nôm en vietnamien.

      • Plusieurs formats de représentation en machine employant de 1 à 4 octets.
    3. Une véritable élaboration théorique du concept de caractère

      • Un catalogue de caractères qui est une base de données accessible en ligne.
        Cette base est partagée en deux, la masse homogène des caractères chinois étant traitée à part.

      • Chaque caractère y est identifié par son numéro (en décimal ou en hexa -
        Desgraupes préfère employer le terme anglais code-point),
        et aussi par son nom (en anglais et en petites capitales - ne s'applique pas aux caractères chinois) :
        p. ex. n° 304 = x0130 LATIN CAPITAL LETTER I WITH DOT ABOVE

      • Ces données sont exploitées par des algorithmes complètement spécifiés dans la norme.

  2. Organisation du catalogue Unicode

  3. Promenade dans le catalogue Unicode

    Nous proposons ci-après une sélection de sujets qui nous intéressent particulièrement.
    Pour chacun d'entre eux, nous renvoyons aux pages d'Alan Wood qui traitent les différents blocs concernés.
    Si vous constatez que votre machine ne possède pas de police pouvant afficher les caractères en question,
    empressez-vous d'y remédier, en suivant les conseils d'Alan Wood !

  4. Représentation en machine : généralités

    1. UTF-32, UTF-16, UTF-8

      Une fois abandonnée l'équation un caractère = un octet, la question se pose du format de représentation.
      Pour des raisons historiques, les différents formats en usage portent le nom générique de UTF, abréviation de
      Unicode (ou Universal) Transfer (ou Transformation) Format.

      • Si on veut un format de taille fixe (nombre fixe d'octets pour chaque caractère),
        le seul choix compatible à la fois avec le nombre de caractères à représenter (1 114 112)
        et avec la technologie informatique est celui de 4 octets, ou 32 bits, appelé UTF-32.
        Il suffit d'écrire le numéro du caractère en tant que nombre entier sur 32 bits, donc (vu la taille de ce nombre)
        avec un premier octet nul dans tous les cas, et un second octet également nul pour tous les caractères
        du Basic Multilingual Plane.
        Ce procédé a l'avantage de la simplicité conceptuelle, et l'inconvénient de prendre de la place
        (4 fois plus qu'un codage sur 8 bits).

      • Si on accepte un format de taille variable (nombre d'octets dépendant du caractère considéré),
        on a le choix entre un module de base de deux octets et un module de base d'un seul octet.

        1. Si on choisit un module de deux octets, soit 16 bits : UTF-16
          on utilise un seul module pour  tous les caractères du BMP,
          et deux modules (quatre octets) pour ceux des autres plans.

          Plus compliqué, mais plus économique en place, surtout si on sort peu du BMP.

        2. Si on choisit un module d'un octet, soit 8 bits : UTF-8
          on utilise un seul module pour les caractères ASCII (7 bits),
          et deux, trois ou quatre modules (octets) pour les autres.

          Nettement plus compliqué, mais encore plus économique en place,
          et surtout compatible avec l'ASCII !
          Tout fichier ne contenant que des caractères ASCII 7 bits est ipso facto un fichier UTF-8.
          Avantage considérable en pratique...
    2. Quel est le but poursuivi ? Travail local ou communication à distance ?

      Il faut distinguer deux orientations dans la représentation des caractères Unicode en machine, selon le but poursuivi :
      • travail local (édition, au sens anglais du terme,
        ou plus généralement toute espèce de calcul sur le texte)
      • communication : réalisation d'un fichier qui sera envoyé à travers le réseau à d'autres machines.

      Ces deux ordres d'activité ne sont pas soumis aux mêmes contraintes techniques !
      • le travail local ne connaît à peu près aucune entrave, vu la puissance de calcul des machines modernes ;
      • la communication en revanche doit se préoccuper
        1. des différences de comportement entre machines (hétérogénéité des processeurs)
        2. des conventions en vigueur sur le réseau.

        1. L'hétérogénéité des processeurs se manifeste dans l'ordre de traitement des octets
          (processeurs gros-boutiens et petit-boutiens) ce qui oblige à des contorsions pour indiquer
          l'option choisie dans un fichier codé en UTF-16 ou en UTF-32 (à l'aide d'un BOM : Bit Order Mark).
          Par exemple, le logiciel de traitement de corpus Unitex travaille sur des fichiers codés en UTF-16 petit-boutien.

        2. Les conventions en vigueur visent les jeux de caractères exigés ou attendus par les
          différents protocoles de transmission (par exemple l'ASCII 7 bits pour le protocole SMTP).
          En ce qui nous concerne, le point important est le choix d'UTF-8 comme jeu de caractères par défaut
          dans le format XML.


        3. Il faut enfin observer que les attentes des logiciels de traitement ne sont pas toujours clairement exprimées.
          C'est le cas notamment pour les compilateurs et interprètes des langages de programmation.
          Par exemple, un programme Java contenant des lettres accentuées a des chances de ne pas s'exporter correctement
          d'une machine à l'autre - ce qui se comprend aisément.
          Le cas de JavaScript est très net : le code contenu dans les fichiers JavaScript (extension ".js") doit être écrit
          en ASCII pur (7 bits). Tout autre caractère doit être donné par son n° Unicode, sous la forme "\uHHHH"
          (voir plus loin).
          Il y a donc en fait deux genres de textes bien différents :
          1. les textes documentaires destinés à des lecteurs humains, par le truchement de logiciels comme les navigateurs Web,
            les éditeurs de textes ou les gestionnaires de courrier
          2. les textes-programmes destinés à des compilateurs et interprètes.
          Nous ne dirons rien de la seconde catégorie pour l'instant.

      Nous laisserons de côté les préoccupations relatives au travail local, en nous bornant à constater que
      les logiciels traitant des textes utilisent souvent UTF-16 ou UTF-32 pour représenter les caractères
      en mémoire centrale (buffer). Pour en savoir plus sur ces codages, voyez Wikipedia en anglais
      (UTF-16, UTF-32).
      Nous nous concentrons ici sur les questions de communication, pour lesquelles UTF-8 est le codage
      de choix, en raison de sa diffusion croissante (tout Wikipedia !) et de sa compatibilité avec l'ASCII "pur".

  5. UTF-8 : principe et mise en œuvre

    1. Principe

      1. On part de l'idée de représenter les 128 premiers caractères (ASCII) sur un seul octet, comme par le passé.
        Ces octets ont comme caractéristique d'avoir un premier bit nul.

      2. Tous les autres caractères seront représentés par au moins deux octets.
        Il faut alors distinguer le premier octet du (ou des) octet(s) suivant(s).
        Ils ne peuvent pas commencer par "0" car c'est le privilège des octets ASCII.
        On décide que :
        • les octets suivants commenceront par "10"
        • le premier octet commencera par "110" ou par "1110" ou par "11110", etc,
          le nombre de "1" initiaux donnant le nombre total d'octets dans la représentation du caractère
          (en pratique, 2, 3 ou 4) .
        • Exemples : trois octets 11100000 10100100 10001011
          deux octets 11001110 10110001
          mais un seul octet 01111000 (ASCII : on reconnaît la lettre minuscule "x")

      3. Ainsi, chacun des octets suivants peut porter 6 bits utiles (les 2 premiers étant fixés)
        tandis que le premier octet peut en porter 5, 4 ou 3, suivant le nombre de ses suivants.
        Avec ce système,
        • un code UTF-8 de deux octets porte 11 bits utiles (5+6),
          (suivant le "squelette" 110xxxxx 10yyyyyy)
        • un code UTF-8 de trois octets porte 16 bits utiles (4+6+6),
          (suivant le "squelette" 1110xxxx 10yyyyyy 10zzzzzz)
        • et un code UTF-8 de quatre octets porte 21 bits utiles (3+6+6+6),
          (suivant le "squelette" 11110xxx 10yyyyyy 10zzzzzz 10tttttt)
        ce qui suffit largement pour les 17 plans Unicode.

      4. Pour déterminer le code UTF-8 d'un numéro Unicode donné, il suffit de :
        1. écrire ce numéro en binaire, et supprimer les zéros initiaux ;
          en fonction du nombre de bits utiles, déterminer le nombre d'octets UTF-8 ;
        2. décomposer le binaire de droite à gauche en tranches de 6 bits,
          plus une tranche de 5, 4 ou 3 (en complétant au besoin par des zéros à gauche) ;
        3. et "remplir" le squelette décrit ci-dessus en 3.

      5. Exemples :
        • DEVANAGARI LETTER VOCALIC R, n° 2315 = x090B
          en binaire 00001001 00001011, 12 bits utiles, il faut donc 3 octets UTF-8
          squelette  1110xxxx 10yyyyyy 10zzzzzz
          décomposition 4+6+6 : 0000 + 1001 00 + 00 1011
          résultat : 11100000 10100100 10001011, en hexa 0xE0A48B.

        • GREEK SMALL LETTER ALPHA, n° 945 = x03B1
          en binaire 00000011 10110001, 10 bits utiles, donc 2 octets UTF-8,
          squelette  110xxxxx 10yyyyyy
          décomposition 5+6 : 011 10 + 11 0001
          résultat : 11001110 10110001, en hexa 0xCEB1.

      6. Propriétés du codage UTF-8 :

        1. Le dernier chiffre hexadécimal du numéro Unicode d'un caractère se retrouve identique
          comme dernier chiffre de sa représentation en UTF-8.

        2. Plus le numéro Unicode est grand, plus le code UTF-8 correspondant, vu comme un nombre entier,
          est grand.
          En termes mathématiques, le codage UTF-8 est une application injective des entiers dans les entiers
          qui est croissante par rapport à l'ordre naturel.

        3. Conséquences pratiques
          • Les caractères de numéros inférieurs à 128 sont représentés en UTF-8 sur un octet (ASCII).

          • Ceux dont les numéros sont compris entre 128 et 2047 (= x07FF = 0000 0111 1111 1111),
            par deux octets :
            c'est le cas de toutes les versions de l'alphabet latin énumérées ci-dessus
            (donc de toutes nos "lettres accentuées"), du grec monotonique (mais pas polytonique),
            du cyrillique, de l'arménien (mais pas du géorgien),
            de l'hébreu et de l'arabe (mais pas de l'éthiopien).

          • Tout le reste du BMP (entre 2048 et  65535) est codé sur trois octets.
            Voyez par exemple le cas de l'écriture devanâgari.
            Ces triplets d'octets sont aisément reconnaissables sur un dump en hexadécimal,
            car le premier octet commence par "E" (en binaire 1110).
            Exemple : le caractère géorgien Ⴋ (U+10AB = GEORGIAN CAPITAL LETTER MAN), en UTF-8 0xE182AB.

          • Les caractères appartenant aux plans supérieurs sont codés sur quatre octets,
            avec un octet initial commençant par "F" (en binaire 1111).
            Exemple : le caractère gotique 𐌰 (U+10330 = GOTHIC LETTER AHSA), en UTF-8 0xF0908CB0.
    2. Notation et expérimentation :

      • Répétons qu'il faut bien distinguer

        1. la désignation du caractère (nommer le caractère)

          • par son n° (en décimal ou en hexa) suivnt une syntaxe qui dépend du contexte :

            • en HTML/XML : &#lenumérodec; ou &#xlenumérohex;
              sont des entités permettant de désigner un caractère Unicode dans un contexte "tout ASCII".

              Ce procédé a l'avantage de ne rien supposer du côté du logiciel de traitement
              (voir ci-après les question de mise en œuvre d'UTF-8).
              Il est donc préféré des traditionnalistes attachés à iso-8859-1 !
              Pour l'observer, demandez à voir le code-source de la page.
              Exemples :

              Bien évidemment, son emploi systématique conduit à occuper beaucoup de place en mémoire
              (7 octets par caractère !). Mais des textes même substantiels ne pèsent rien à côté
              des mégaoctets exigés par les images ou par la vidéo !

            • en Java : '\ulenumérohex' est une constante de type char
              Attention ! le type primitif char est limité à la plage 0-6536 (2 octets),
              sa valeur maximale est donc '\uFFFF'.
              Réciproquement, tout entier de cette plage peut être affecté à une variable de type char,
              avec conversion automatique : si on exécute
              char x = 2315; char y = '\u090B';
              les deux variables x et y contiennent le même caractère
              (l'expression booléenne x==b vaut true).

            • en JavaScript (qui ne connaît pas les constantes de type caractère) :
              la forme \ulenumérohex peut (doit) être employée dans une constante de chaîne,
              et pour calculer on emploie la fonction chr appliquée au numéro
              vu comme une valeur entière (sur 16 bits !), fonction qui renvoie une chaîne monocaractère
              donc chr(lenumérodec) ou chr(0xlenumérohex)

            • Le problème des plans supplémentaires pour Java & JavaScript.
              Les caractères dont le numéro Unicode dépasse deux octets sont représentés comme
              des couples de pseudo-caractères (surrogates) dont chacun est codé sur 16 bits.
              Ce choix introduit quelques difficultés techniques, dont le détail diffère d'un langage à l'autre.
              Pour en savoir davantage...

            • en Perl : dans une constante de chaîne entre doubles-quotes : \x{lenumérohex},
              pour le BMP comme pour les plans supplémentaires,
              sans avoir à se préoccuper de la taille du nombre.


          • par son nom officiel (en Perl), à condition d'avoir effectué les réglages nécessaires..


          • Exemple : le caractère Unicode n° 304 = x0130 LATIN CAPITAL LETTER I WITH DOT ABOVE
            peut être désigné en HTML/XML par İ ou par İ, en Java par '\u0130'
            et en JavaScript par chr(304) ou par chr(0x130)

        2. sa réalisation matérielle
          laquelle prend deux aspects (dissymétriques) :

          • codage en octets (ici, UTF-8)
            Exemple : LATIN CAPITAL LETTER I WITH DOT ABOVE ==> sur 2 octets C4B0

          • affichage via une police (à condition qu'elle soit présente sur la machine !)
            Exemple : LATIN CAPITAL LETTER I WITH DOT ABOVE ==> İ

        Pour marquer cette distinction, nous écrirons désormais les octets des réalisations en UTF-8
        entre chevrons (comme ils sont produits par certains logiciels) :
        Exemple : LATIN CAPITAL LETTER I WITH DOT ABOVE ==> en UTF-8 sur 2 octets <C4><B0>

      • Exemple à bien comprendre : Voici le même texte
        Ça, c'est mon frère René et ça c'est ma sœur Iñès.
        d'abord en deux codages à 8 bits, Mac Roman et iso-8859-1 (Windows) :
        On peut constater que, dans les deux cas, les caractères non-ASCII sont représentés
        par un seul octet, mais qu'à chaque fois ce sont des octets différents !
        • <82>a, c'est mon fr<8F>re Ren<8E> et <8D>a c'est ma s<CF>ur I<96><8F>s.
        • <C7>a, c'est mon fr<E8>re Ren<E9> et <E7>a c'est ma s<9C>ur I<F1><E8>s.

        Et le voici en UTF-8, montrant clairement les deux octets nécessaires pour tout caractère non-ASCII :

        <C3><87>a, c'est mon fr<C3><A8>re Ren<C3><A9> et <C3><A7>a c'est ma s<C5><93>ur I<C3><B1><C3><A8>s.

      • Pour faciliter l'expérimentation, voici un outil qui vous permettra de confronter les différents aspects
        d'un caractère Unicode :
        1. son numéro que vous pouvez donner en décimal ou en hexa, et voir en binaire
        2. sa représentation UTF-8, en hexa (notation à chevrons) et en binaire
        3. son nom officiel (en lecture seule).
        Notez que les informations que vous tapez dans un des champs ouverts en écriture
        ne sont prises en compte que lorsque vous cliquez une première fois en dehors de ce champ.

        Le logiciel UnicodeChecker vous permettra de faire les mêmes expériences....
    3. Mise en œuvre

      Entrer & sortir :
      les machines modernes avec écran et clavier nous laissent croire que lire et écrire ne font qu'un
      c'est faux !
      L'écriture (fabrication d'un fichier) et la lecture (interprétation) sont deux processus dissymétriques.
      Cette dissymétrie est masquée par la puissance des machines et par la bonne adéquation des outils (clavier - écran - logiciels).
      Mais la puissance est relative et l'adéquation dépend du but poursuivi !

      • Côté entrée : deux étapes successives

        1. Écriture dans un outil d'édition par tout moyen à votre disposition
          (ils sont nombreux : menu d'insertion, donnée explicite du nom du caractère, clavier, palette, etc).
          • Note technique : cette écriture fait entrer les caractères dans le buffer de l'outil,
            qui loge en mémoire centrale en le recodant.
            Notez que l'utilisateur n'est en général pas informé de ce codage interne,
            (l'éditeur JEdit faisant exception : il déclare ouvertement
            que sa représentation interne est UTF-16).

        2. Sauvegarde dans un fichier sur disque en spécifiant le codage UTF-8.

          Notez que les modalités de cette spécification varient beaucoup avec le logiciel employé.
          Par exemple, sur Mac OS X :

          • L'éditeur de textes TextEdit répond à la commande Fichier>Enregistrer sous...
            par une fenêtre de dialogue où la question du codage est posée clairement.

          • L'éditeur HTML Netscape Composer en revanche distingue Fichier>Enregistrer sous...
            de Enregistrer en changeant de codage
            et il adopte un comportement diversifié suivant le choix de l'utilisateur.
            L'analyse de ce comportement n'est pas sans intérêt...

          • La version 1 de Microsoft Word pour Mac OS X (déjà ancienne) répond à la commande Fichier>Enregistrer sous...
            par une fenêtre de dialogue où la question du codage est posée comme cas particulier de modèle de document,
            le choix de Texte unicode conduisant à une sauvegarde en format texte UTF-16.

          • L'éditeur de textes JEdit distingue explicitement
            • le buffer en mémoire centrale (où les caractères sont représentés en UTF-16)
            • du fichier sur disque, dont le codage courant est affiché au bord inférieur de la fenêtre
              et peut être modifié par une commande adéquate (voir la documentation)

          Il convient donc de se renseigner soigneusement sur l'usage du logiciel employé,
          de préférence avant d'avoir à sauvegarder son travail en urgence,
          car un mauvais choix de codage peut conduire à la perte irrémédiable de l'information acquise...

      • Côté sortie : avertir le logiciel de traitement qu'il doit "lire" en UTF-8

        1. pour un éditeur prenant un fichier sur le disque,
          à l'ouverture du fichier (en général, codage par défaut donné dans les préférences de l'outil).
          Là aussi les mœurs des logiciels sont diverses, et il convient de chercher où l'information pertinente doit être fournie.
          Tous n'ont pas la limpidité de TextEdit...

        2. pour un logiciel recevant le fichier par le réseau,
          par un message spécifique avec un type MIME comme Content-Type: text/plain ou text/xml
          assorti de la mention "charset=UTF-8".
          C'est d'ordinaire le serveur de fichiers qui en a la charge, il est bon de contrôler son action.

          • Pour un courrier envoyé par SMTP, le logiciel émetteur devra envoyer un en-tête (header) :
            "Content-Type: text/plain; charset=UTF-8"

          • Pour un fichier HTML envoyé par un serveur Web comme Apache, c'est la déclaration du serveur
            qui a priorité. Tout dépend alors de sa configuration, qui peut fort bien imposer iso-8859-1
            (voir le cours 3). Si tel est le cas, il faut demander au responsable changer la directive en
            AddDefaultCharset Off.
            En supposant que le serveur soit muet sur ce point, l'auteur du fichier doit faire sa déclaration
            en ajoutant comme premier élément de la partie <head> de son fichier un élément ainsi conçu:
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
            et en observant que tous les caractères précédents, à savoir <DOCTYPE....> <html xmlns:...><head>
            sont de l'ASCII 7 bits, donc déchiffrables sans ambiguïté.

          • Toujours en HTML, dans un formulaire (balise <form>) pour s'assurer que les chaînes tapées
            par l'utilisateur sont transmises au serveur en UTF-8 (et non pas selon la fantaisie du navigateur),
            il convient d'ajouter dans la balise l'attribut accept-charset="UTF-8".

          • Enfin, la norme HTML prévoit qu'un lien hypertexte peut porter un attribut charset
            destiné à informer le navigateur du jeu de caractères employé dans la page-cible
            (http://fr.selfhtml.org/html/liens/lienstypes.htm#langue_caracteres)
            ce qui est fort utile lorsque le lien désigne une page en texte simple (ni HTML, ni XML),
            car en ce cas le navigateur applique son codage par défaut, qui est souvent Latin-1.
            Hélas, les navigateurs n'en ont cure... et il faut méditer la note de SelfHTML :
            "Il n'est pas établi si et comment un navigateur WWW affiche de telles mentions."
            C'est bien dommage !

          • En ce qui concerne les chaînes traitées, et notamment affichées, par JavaScript ou par Java,
            il convient de noter deux points :

            1. Pour JavaScript comme pour Java, tous les caractères d'une chaîne sont en Unicode .
              En particulier, la longueur d'une chaîne est le nombre de ses caractères et non pas
              celui de ses octets (sous réserve que ces caractères ne sortent pas du BMP).
              De même pour les fonctions d'accès charAt() et charCodeAt().
              JavaScript traitera donc correctement les chaînes que lui transmet le navigateur.
              Pour la communication avec ce dernier, voir ci-dessus.

            2. Le code JavaScript proprement dit (celui qui s'écrit dans les fichiers ".js" envoyés au
              navigateur en même temps que le HTML), comme il a été dit plus haut, doit être rédigé
              entièrement en ASCII, pour éviter les malentendus avec des navigateurs imprévisibles.
              Des caractères non-ASCII peuvent apparaître aussi bien dans les identificateurs
              que dans les constantes de chaînes, mais ils doivent ête donnés sous la forme
              "\ulenumérohex" comme en Java.
              On écrira par exemple, en supposant que la variable chn contient une chaîne :
              alert ('"'+ chn + '" ne repr\u00E9sente pas un nombre d\u00E9cimal !');
              pour obtenir :

              alert