Unicode et les langages de programmation

  1. Exemple illustratif
  2. Généralités
  3. Réalisation en Perl
  4. Réalisation en PHP-5
  5. Réalisation en JavaScript
  6. Réalisation en Java
  7. Conclusion

Manipulation des caractères Unicode dans quatre langages de programmation :
Perl, PHP-5, JavaScript et Java.
qui figurent dans le cursus du Master Plurital.

  1. Exemple


    On prend comme fil conducteur la réalisation d'un tout petit programme
    Voici notre texte-source favori  (fichier  Kieu.txt),
    où il s'agit de distinguer les caractères nôm des caractère chinois classiques - motivation issue du cours n° 4 :


    Les premiers vers du "Kim Vân Kiều"
    de Nguyễn Du

    𤾓𢆥𥪝𡎝𠊛嗟
    𡦂才𡦂命窖羅恄饒
    𣦆戈沒局𣷭橷
    仍調𥉩𧡊罵忉疸𢚸
    邏之彼嗇私豐
    𡗶青慣退𦟐紅打慳



    et voici le résultat de la transformation (fichier  Kieu.html)
    Les premiers vers du "Kim Vân Kiều"
    <br />
    de Nguy&#7877;n Du
    <br />

    <br />
    <b>&#151443;</b><b>&#139685;</b><b>&#154269;</b><b>&#136093;</b><b>&#131739;</b>&#21983;
    <br />
    <b>&#137602;</b>&#25165;<b>&#137602;</b>&#21629;&#31382;&#32645;&#24644;&#39250;
    <br />
    <b>&#145798;</b>&#25096;&#27794;&#23616;<b>&#146925;</b>&#27255;
    <br />
    &#20173;&#35519;<b>&#152169;</b><b>&#161866;</b>&#32629;&#24521;&#30136;<b>&#140984;</b>
    <br />
    &#37007;&#20043;&#24444;&#21959;&#31169;&#35920;
    <br />
    <b>&#136694;</b>&#38738;&#24931;&#36864;<b>&#157648;</b>&#32005;&#25171;&#24947;
    <br />



    et son interprétation par Safari :

    Kieu

  2. Généralités


    Programmer cette transformation demande
    1. de contrôler le codage des caractères du fichier d'entrée
    2. de lire ledit fichier ligne à ligne
    3. de trouver dans chaque ligne la séquence des numéros Unicode
    4. le reste va de soi (en principe, car nous verrons que la réaité est un peu plus compliquée...).
    Pour réaliser ces opérations, nos quatre langages s'y prennent de manières assez différentes.
    Afin de faciliter la comparaison, nos quatre réalisations suivront strictement le même patron, sans chercher une meilleure adaptation à chaque langage.
    1. Le cœur du programme est une fonction nommée trans,
      • qui prend comme argument un tableau de chaînes (en Unicode)
      • qui renvoie comme résultat le tableau des chaînes codées en ASCII, avec les éventuelles balises <b>.
    2. Cette fonction est lancée par une autre fonction extrans
      • qui reçoit deux noms de fichiers, pour l'entrée et pour la sortie,
      • qui lit le fichier d'entrée (en UTF-8) et convertit son contenu en un tableau  de chaînes nommé tab
      • qui appelle trans(tab) avec comme résultat un tableau de chaînes res
      • et enfin transcrit le tableau res dans le fichier de sortie.
  3. Réalisation en Perl


    Perl considère que les éléments d'une chaîne sont des caractères (et non pas des octets).
    Il obtient le n° Unicode d'un caractère par la fonction ord.
    Pour accéder aux différents caractères qui composent une chaîne, il convient de la transformer au préalable en un tableau par un appel à split sur séparateur vide : my @tabcar = split(//, $ligne);.
    Le transfert du contenu (lignes) d'un fichier de nom $fichIn dans un tableau @tab se fait par la séquence
    open(ENTREE, "<$fichIn");
    @tab = <ENTREE>;

    Moyennant quoi notre programme s'écrit (fichier utf8ToHTML.pl)


    use strict;
    use warnings;
    use open ':utf8';

    sub trans (\@){ # arg. tableau de lignes , renvoie un tableau de lignes
    my ($tablignes) = @_ ;
    my @rtab;
    my $k = 0;
    foreach my $ligne ( @$tablignes ){
    my @tabcar = split(//, $ligne);
    my $rligne = "";
    foreach my $car ( @tabcar ){
    my $nb = ord($car); # décimal
    if( $nb < 128 ){ # ASCII
    $rligne .= $car;
    }elsif( $nb < 65536 ){ # BMP
    $rligne .= "&#".$nb.";";
    }else{ # Plan supplémentaire
    $rligne .= "<b>&#".$nb.";</b>";
    }
    }
    $rtab[$k] = $rligne."<br />\n";
    $k++;
    }
    return @rtab;
    } # trans


    sub extrans($$){ # arg 2 noms de fichier
    my ($fichIn, $fichOut) = @_ ;

    open(ENTREE, "<:utf8", $fichIn); # UTF-8
    open(SORTIE, ">$fichOut"); # ASCII
    my @tab = <ENTREE>;
    my @res = trans(@tab);

    foreach my $lgn ( @res ){
    print(SORTIE "$lgn");
    }
    } # extrans

    extrans($ARGV[0], $ARGV[1]); # en ligne de commande


    L'exécution de ce programmme par
    jfp% perl utf8ToHTML.pl Kieu.txt Kieu.html
    donne exactement le résultat attendu.
  4. Réalisation en PHP-5


    Nous passerons de Perl à PHP-5 en deux temps :
    1. d'abord les questions de syntaxe, superficielles mais incontournables
    2. ensuite les questions de fond.
    PHP connaît comme Perl une fonction ord qui donne le n° d'un caractère, mais contrairement à Perl il sait accéder au caractère de rang i dans une chaîne par une notation indexée (avec accolades) : $car = $ligne{$i};.
    Le transfert du contenu (lignes) d'un fichier de nom $fichIn dans un tableau $tab se fait en une seule instruction
    $tab = file(
    $fichIn);
    Moyennant quoi notre programme Perl se traduit "mécaniquement" en PHP comme suit (fichier utf8ToHTML.php) :


    <?php

    function trans ($tablignes){ // arg. tableau de lignes , renvoie un tableau de lignes

    $k = 0;
    $rtab = array();
    foreach ( $tablignes as $ligne ){
    $rligne = "";
    for( $i=0; $i<strlen($ligne); $i++ ){
    $car = $ligne{$i};
    $nb = ord($car); // décimal
    if( $nb < 128 ){ // ASCII
    $rligne .= $car;
    }else{
    if( $nb < 65536 ){ // BMP
    $rligne .= "&#".$nb.";";
    }else{ // Plan supplémentaire
    $rligne .= "<b>&#".$nb.";</b>";
    }
    }
    }
    $rtab[$k] = $rligne."<br />\n";
    $k++;
    }
    return $rtab;
    } // trans


    function extrans($fichIn, $fichOut){ // arg 2 noms de fichier

    $SORTIE = fopen($fichOut, "w");
    $tab = file($fichIn);
    $res = trans($tab);

    foreach( $res as $lgn ){
    fputs($SORTIE, $lgn);
    }
    } // extrans

    extrans($argv[1], $argv[2]);

    ?>


    L'exécution par
    jfp% php utf8ToHTML.php Kieu.txt Kieu.html
    s'effectue sans erreur, mais... elle ne donne pas exactement le résultat attendu.

    KieuP


    Les premiers vers du "Kim V&#195;&#162;n Ki&#225;&#187;&#129;u"
    <br />
    de Nguy&#225;&#187;&#133;n Du
    <br />

    <br />
    &#240;&#164;&#190;&#147;&#240;&#162;&#134;&#165;&#240;&#165;&#170;&#157;&#240;&#161;&#142;&#157;&#240;&#160;&#138;&#155;&#229;&#151;&#159;
    <br />
    &#240;&#161;&#166;&#130;&#230;&#137;&#141;&#240;&#161;&#166;&#130;&#229;&#145;&#189;&#231;&#170;&#150;&#231;&#190;&#133;&#230;&#129;&#132;&#233;&#165;&#146;
    <br />
    &#240;&#163;&#166;&#134;&#230;&#136;&#136;&#230;&#178;&#146;&#229;&#177;&#128;&#240;&#163;&#183;&#173;&#230;&#169;&#183;
    <br />
    &#228;&#187;&#141;&#232;&#170;&#191;&#240;&#165;&#137;&#169;&#240;&#167;&#161;&#138;&#231;&#189;&#181;&#229;&#191;&#137;&#231;&#150;&#184;&#240;&#162;&#154;&#184;
    <br />
    &#233;&#130;&#143;&#228;&#185;&#139;&#229;&#189;&#188;&#229;&#151;&#135;&#231;&#167;&#129;&#232;&#177;&#144;
    <br />
    &#240;&#161;&#151;&#182;&#233;&#157;&#146;&#230;&#133;&#163;&#233;&#128;&#128;&#240;&#166;&#159;&#144;&#231;&#180;&#133;&#230;&#137;&#147;&#230;&#133;&#179;
    <br />



    Que s'est-il passé ?
    Tout bonnement que PHP-5 considère encore que les éléments d'une chaîne sont des octets, et non des caractères multi-octets.
    Vu sous un autre angle, on dira que PHP utilise le codage ISO-8859-1 (Latin-1) pour ses chaînes de caractères.
    (La mention de la version 5 est importante, ce point de vue archaïque doit changer avec PHP-6).

    Tous les traitements de chaînes reposent donc sur l'équivalence 1 octet = 1 caractère.
    Notamment, la fonction ord renvoie la valeur entière de l'octet, celle que nous avons manipulée en C, entre 0 et 255.
    Dans notre programme, nos subtiles questions sur la sortie du BMP sont donc restées lettre morte :
    bon exemple de programme parfaitement correct du point de vue syntaxique, mais complètement idiot quant à la sémantique...

    Si on ne souhaite que de transmettre la chaîne, cette manière de faire est sans importance :
    une chaîne en UTF-8, par exemple, sera transmise octet par octet sans inconvénient.
    Le texte codé en ASCII ci-dessus illustre parfaitement ce phénomène.

    Si au contraire on demande à PHP de calculer sur la chaîne  (par exemple, la renverser par la fonction strrev())
    il faut absolument se ramener à l'équivalence octets-caractères.

    Pour ce faire PHP propose le couple de fonctions utf8_decode / utf8_encode.
    Hélas, cette transformation ne s'applique qu'aux chaînes représentables en Latin-1 ! Nous sommes loin du compte...

    Que faire ? Adapter à PHP la technique de décodage des octets que nous avons mise au point en C.
    Les mêmes causes engendrant les mêmes effets, nous sommes sûrs d'obtenir le résultat désiré !
    (nouveau fichier utf8ToHTML.php)
    <?php

    function entites( $chn ) {
    // traduction en PHP du programme C du Cours6 , cf Plurital/Cours6/TransC/LirUTF8.html
    $res = "";
    $k = strlen($chn); // nombre d'octets
    for( $i = 0; $i < $k; $i++ ){
    $val = ord($chn{$i});
    if( $val<128 ){ //ASCII
    $res .= $chn{$i};
    }else{
    if( $val >= 240 ){ // 4 octets
    $contr1 = ($val-240) * 262144;
    $contr2 = (ord($chn{++$i})-128) * 4096;
    $contr3 = (ord($chn{++$i})-128) * 64;
    $contr4 = ord($chn{++$i})-128;
    $res .= "<b>&#".($contr1+$contr2+$contr3+$contr4).";</b>"; // en gras
    }else{
    if( $val >= 224 ){ // 3 octets
    $contr1 = ($val-224) * 4096;
    $contr2 = (ord($chn{++$i})-128) * 64;
    $contr3 = ord($chn{++$i})-128;
    $res .= "&#".($contr1+$contr2+$contr3).";"; // en maigre
    }else{ // 2 octets
    $contr1 = ($val-192) * 64;
    $contr2 = ord($chn{++$i})-128;
    $res .= "&#".($contr1+$contr2).";"; // en maigre
    }
    }
    }
    }
    return $res;
    }// function entites

    function trans ($tablignes){ // arg. tableau de lignes , renvoie un tableau de lignes
    $k = 0;
    $rtab = array();
    foreach ( $tablignes as $ligne ){
    $rligne = entites($ligne);
    $rtab[$k] = $rligne."<br />\n";
    $k++;
    }
    return $rtab;
    } // trans

    ........ extrans et ligne de commande comme ci-dessus ..............
    ?>

  5. Réalisation en JavaScript


    En JavaScript il n'y a plus de fonction ord, mais un couple de méthodes de l'objet String permettant d'obtenir
    D'autre part, le code JavaScript est normalement interprété par un navigateur, sur un texte pris dans une fenêtre textarea,
    et non pas en ligne de commande avec deux fichiers.
    La fonction de lancement extrans va donc être assez différente de celle que nous avons vue en Perl et en PHP :
    Sur ces bases, notre programme s'écrit sans difficulté :
    (fichier utf8ToHTML.js)

    function trans(tablignes){ // renvoie le tableau des lignes recodées
    var nblignes = tablignes.length;
    var rtab = new Array(nblignes);

    for( var j = 0; j<nblignes; j++ ){ //boucle sur le tableau des lignes
    var ligne = tablignes[j];
    var rligne = "";
    for ( var i = 0; i<ligne.length; i++ ) {
    var nb = ligne.charCodeAt(i);
    if( nb < 128 ){ // ASCII
    rligne += ligne.charAt(i);
    }else if( nb < 65536 ){ //BMP
    rligne += "&#"+nb+";";
    }else{ // Plan supplémentaire
    rligne += "<b>&#"+nb+";</b>";
    }
    }
    rtab[j] = rligne+"<br />\n";
    }//for
    return rtab;
    }//trans

    function extrans (txt) { // adaptée à un élément de formulaire "textarea"
    var lignes = txt.split("\n"); //le tableau des lignes
    var rlignes = trans(lignes);

    var fenRes = window.open("");
    for( var j = 0; j<rlignes.length; j++ ){
    fenRes.document.write(rlignes[j]);
    }
    fenRes.document.close();
    }//extrans


    La page Web assurant la mise en scène peut se réduire à (fichier utf8ToHTML.html):
    [notez, dans le formulaire, l'attribut accept-charset="utf-8", qui garantit que le texte est bien transmis en UTF-8 ;
     en revanche, inutile de prendre des précautions dans la fenêtre-résultat, puisqu'on lui envoie de l'ASCII !]

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>

    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Distinguer les plans supplémentaires</title>

    <script type="text/javascript" src="utf8ToHTML.js"></script>
    </head>
    <body>
    <p> Donnez votre texte en UTF-8,<br /> puis cliquez sur "go"</p>

    <form action="" accept-charset="utf-8">
    <p> <textarea name="donn" rows="10" cols="20"></textarea> </p>
    <p> <input type="button" value="go" onclick="extrans(donn.value);"> </p>
    </form>

    </body>
    </html>


    Voyez :

    Donnée      --------->       Résultat

    Voila qui est superbe.... mais les caractères gras ne ressortent pas très nettement.
    Voyons le code-source produit :

    Les premiers vers du "Kim V&#226;n Ki&#7873;u"<br />
    de Nguy&#7877;n Du<br />
    <br />
    &#55379;&#57235;&#55368;&#56741;&#55382;&#56989;&#55364;&#57245;&#55360;&#56987;&#21983;<br />
    &#55366;&#56706;&#25165;&#55366;&#56706;&#21629;&#31382;&#32645;&#24644;&#39250;<br />
    &#55374;&#56710;&#25096;&#27794;&#23616;&#55375;&#56813;&#27255;<br />
    &#20173;&#35519;&#55380;&#56937;&#55390;&#56394;&#32629;&#24521;&#30136;&#55369;&#57016;<br />
    &#37007;&#20043;&#24444;&#21959;&#31169;&#35920;<br />
    &#55365;&#56822;&#38738;&#24931;&#36864;&#55385;&#57296;&#32005;&#25171;&#24947;<br />
    <br />

    Mais c'est pas du tout ça !

    Que s'est-il passé ?
    Eh bien, si on lit attentivement la documentation sur JavaScript, on voit (parfois) mentionné que
    maChaine.charCodeAt(i) renvoie un entier sur 16 bits ! Nous en éprouvons ici les conséquences ...

    En effet, quand on sort du BMP, le numéro Unicode vu comme un entier ne peut plus se loger dans 16 bits.
    Un conflit se produit alors entre
    1. la position de principe, pour qui la taille de l'entier est sans importance,
      par exemple, les entités HTML restent valables :
      • Le caractère gotique 𐌰, U+10330, est fidèlement désigné par l'entité &#x10330; : 𐌰
      • Le caractère nôm 𤾓, U+024F93, est fidèlement désigné par l'entité &#x024F93; : 𤾓
       
    2. la réalité de nombreuses implémentations pour lesquelles l'unité de compte est de 16 bits.
      ainsi, en JavaScript, sans que ce soit documenté, les méthodes de l'"objet" String
      • la fonction charCodeAt(i) renvoie un entier sur 16 bits
      • la fonction length() renvoie le nombre de blocs de 16 bits et non pas le nombre de caractères
    Pour surmonter cette contradiction, la représentation UTF-16 acquiert droit de cité :
    chaque numéro Unicode qui dépasse FFFF est assorti de sa représentation comme un couple de nombres sur 16 bits
    1. le premier appartenant à la plage U+D800 à U+DBFF (demi-zone haute d’indirection)
    2. le second appartenant à la plage U+DC00 à U+DFFF (demi-zone basse d’indirection)
    Exemples :
    Il suffit alors d'admettre que les caractères des plans supplémentaires apparaissent aux yeux de JavaScript comme des couples de caractères spéciaux, qu'il faut manipuler ensemble.
    Cette remarque permet d'interpréter complètement le résultat précédent, et de rectifier notre programme.

    Nous voyons en effet que le test "if( nb < 65536 )" est rendu inopérant par la restriction à 16 bits,
    et qu'il faut donc le remplacer par :
                  if(  nb>=0xD800 && nb<=0xDBFF ){ //DHI, 1er composant plan supplémentaire
                     rligne += "<b>&#"+nb+";&#"+ligne.charCodeAt(++i)+";</b>"; // en gras
                  }else{ // BMP
                     rligne += "&#"+nb+";"; // en maigre
                  }

     Moyennant quoi tout rentre dans l'ordre.

  6. Réalisation en Java

    Pour rester le plus près possible du cadre adopté pour les trois précédents langages, notre conception en Java restera fort peu "objet".
    Nous écrirons une classe Utf8ToHTML qui n'aura point d'instance.
    La fonction trans aura le statut de méthode de classe (static), et la fonction de lancement extrans sera remplacée par la méthode exécutable main. Les tableaux non typés des autres langages seront ici des instances de java.util.Vector<String>.

    Java insiste beaucoup sur le fait que les éléments des chaînes sont des caractères, accessibles par
    Enfin, la spécification d'UTF-8 comme codage d'entrée se traduira par la construction
             BufferedReader entree =
                 new BufferedReader (
                    new InputStreamReader(new FileInputStream (args[0]), "UTF-8") );

    du BufferedReader qui, par lecture ligne à ligne via entree.readLine() va servir de base à la confection du tableau des lignes du texte initial.
    (fichier Utf8ToHTML.java)

    import java.io.BufferedReader;
    import java.io.PrintWriter;
    import java.io.InputStreamReader;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.util.Vector;

    class Utf8ToHTML {

    public static Vector<String> trans (Vector<String> tablignes) throws Exception{

    int nblignes = tablignes.size();
    Vector<String> rtab = new Vector<String>(nblignes);

    for( String ligne : tablignes ){
    String rligne = "";

    for( int i = 0; i< ligne.length(); i++ ){
    int nb = ligne.codePointAt(i);

    if( nb < 128 ){ // ASCII
    rligne += ligne.charAt(i);
    }else{
    if( nb < 65536 ){ // BMP
    rligne += "&#"+nb+";";
    }else{ // Plan supplémentaire
    rligne += "<b>&#"+nb+";</b>";
    }
    }
    }
    rtab.addElement(rligne+"<br />\n");
    }
    return rtab;
    } // trans


    public static void main(String[]args) throws Exception{

    BufferedReader entree =
    new BufferedReader (
    new InputStreamReader(new FileInputStream (args[0]), "UTF-8") );
    PrintWriter sortie =
    new PrintWriter(new FileOutputStream (args[1])); // ASCII

    Vector<String> lignes = new Vector<String>();
    String ligne = entree.readLine();
    do {
    lignes.addElement(ligne);
    ligne = entree.readLine();
    }while ( ligne != null );

    Vector<String> rlignes = trans(lignes);

    for( String rligne : rlignes ){
    sortie.print(rligne);
    }
    sortie.close();
    }// main

    }// Utf8ToHTML


    Essayons :
    jfp% java Utf8ToHTML Kieu.txt KieuJ.html

    Java Vous avez dit bizarre ?

    Voyons le code engendré :

    Les premiers vers du "Kim V&#226;n Ki&#7873;u"<br />
    de Nguy&#7877;n Du<br />
    <br />
    <b>&#151443;</b>&#57235;<b>&#139685;</b>&#56741;<b>&#154269;</b>&#56989;<b>&#136093;</b>&#57245;<b>&#131739;</b>&#56987;&#21983;<br />
    <b>&#137602;</b>&#56706;&#25165;<b>&#137602;</b>&#56706;&#21629;&#31382;&#32645;&#24644;&#39250;<br />
    <b>&#145798;</b>&#56710;&#25096;&#27794;&#23616;<b>&#146925;</b>&#56813;&#27255;<br />
    &#20173;&#35519;<b>&#152169;</b>&#56937;<b>&#161866;</b>&#56394;&#32629;&#24521;&#30136;<b>&#140984;</b>&#57016;<br />
    &#37007;&#20043;&#24444;&#21959;&#31169;&#35920;<br />
    <b>&#136694;</b>&#56822;&#38738;&#24931;&#36864;<b>&#157648;</b>&#57296;&#32005;&#25171;&#24947;<br />
    <br />



    Curiouser and curiouser, comme aurait dit Alice !
    Le fond du problème est évidemment que Java, comme JavaScript, considère que ses caractères ne doivent pas dépasser 16 bits.
    C'est dit explicitement dans la JavaDoc de la classe java.lang.String :

    A String represents a string in the UTF-16 format in which supplementary characters [scil. ceux des plans supplémentarires] are represented by surrogate pairs [...].
    Index values refer to char code units, so a supplementary character uses two positions in a String.

    On voit ici apparaître une distinction subtile entre character et char code unit, qui est détaillée dans la JavaDoc de la classe java.lang.Character.
    Il en ressort que le type de base char est limité  16 bits, et que les caractères des plans supplémentaires sont représentés par des couples de char
    suivant le format UTF-16 (d'où l'appellation char code unit qui se réfère au codage UTF-16).
    Comme les constantes de la forme '\ulenumérohex' sont des constantes de type char, il s'ensuit qu'une écriture comme '\u024F93' est incorrecte.
    Mais cette limitation n'affecte que la collection des valeurs du type char (et par conséquent la collection des instances de la classe Character),
    elle n'empêche pas de manipuler tous les caractères Unicode à partir de leur numéro donné comme un entier.

    Par exemple, la classe Character définit une méthode statique charCount permettant de savoir si un caractère donné par son numéro (int codePoint)
    est représenté par un ou deux char en UTF-16, ainsi qu'une méthode statique toCodePoint donnant le n° du caractère représenté par deux surrogates...

    Notamment, comme le résultat de maChaine.codePointAt(i) est un entier et non un char, il ne faut pas être surpris qu'elle renvoie le numéro Unicode correct
    même s'il dépasse 16 bits, comme l'expérience le montre. Mais il ne faut pas non plus s'étonner qu'en pareil cas la demande maChaine.codePointAt(i+1) 
    renvoie non pas le n° Unicode du caractère suivant, mais bel et bien l'entier correspondant aux 16 bits du second surrogate (de la demi-zone basse d'indirection),
    comme on le vérifie en confrontant le résultat ci-dessus avec ce que JavaScript nous a produit précédemment.

    Quant à l'affichage observé, l'entité HTML correspondant à un surrogate (p. ex. &#57235;) n'est pas interprétée par le navigateur,
    et de plus, lorsqu'elle précède une entité valable, comme à la fin de la première ligne : &#56987;&#21983;,
    l'interprétation de cette dernière est inhibée - d'où les lacunes observées dans l'image.

    Mais que faire pour résoudre notre petit problème ?
    Il suffit d'éliminer les entités parasites en incrémentant le compteur lorsqu'on constate que l'on sort du BMP :
                       if( nb < 65536 ){ // BMP                 
                           rligne += "&#"+nb+";";
                       }else{ // Plan supplémentaire
                           rligne += "<b>&#"+nb+";</b>";
                           i++; // sauter le DBI
                       }

    Et tout rentre dans l'ordre !
    On peut faire plus chic, en remplaçant le test "nb < 65536" qui demande explication par "Character.isSupplementaryCodePoint(nb)"
                       if( Character.isSupplementaryCodePoint(nb) ){ // Plan supplémentaire    
                            rligne += "<b>&#"+nb+";</b>";
                            i++; // sauter le DBI

                       }else{
    // BMP
                            rligne += "&#"+nb+";";
                       }

    Si vous trouvez que cette solution simple ressemble plus à un patch qu'à une construction rationnelle, je vous propose de demander systématiquement le charAt(i),
    et de lui lui poser la question isHighSurrogate (nouveau fichier Utf8ToHTML.java):

    	for( int i = 0; i< ligne.length(); i++ ){
    char carlu = ligne.charAt(i);

    if( (int) carlu < 128 ){ // ASCII
    rligne += ligne.carlu;
    }else{
    if( Character.isHighSurrogate(carlu) ){ // Plan supplémentaire
      char carsuiv = ligne.charAt(++i);
    int nb = Character.toCodePoint(carlu, carsuiv);
    rligne += "<b>&#"+nb+";</b>";
    }else{ // BMP
      rligne += "&#"+(int) carlu+";";
    }
    }

  7. Conclusion

    Les langages de programmation ont suivi le mouvement !
    L'attachement à l'équation caractère = octet reste bien présent chez PHP (en attendant la version 6),
    et JavaScript ne l'a abandonnée que pour en adopter une autre : caractère = 2 octets,
    qui, si elle est moins mutilante, pose des problèmes encore en discussion autour du standard ECMAScript.

    Perl et Java en revanche traitent la question à fond, chacun à sa manière.
    Encore ne nous sommes-nous préoccupés ici que des seuls numéros Unicode !
    Pour que la comparaison soit complète, il faut aussi examiner comment les langages de progammation permettent de parler des caractères,
    quels sont les prédicats disponibles pour ce faire, quelles fonctions leurs sont applicables, etc.
    Comme on l'a vu au cours 5, la voie d'accès principale est celle des classes de caractères utilisables dans les expressions régulières,
    pour Perl comme pour Java.
    Java, armé du concept de classe, y ajoute une nomenclature attachée aux classes java.lang.Character et java.lang.Character.UnicodeBlock.
    Il y a de quoi faire !