Buscando documentación acerca de UTF-8 en www.unicode.org

ejemplos de utf8

La imagen de la izquierda aparece en un documento al que puede accederse en versions/Unicode5.1.0, última versión de Unicode 5.1 con documentación de 5.0 Book Front Matter, donde se expone la documentación de Unicode desglosada en capítulos. Del Capítulo 2 General Structure, en su página 37, hemos extraído esta información que compara los distintos formatos UTF para codificar cuatro códigos Unicode: 65, 937, 35486 y 66436 en decimal (41, 3A9, 8A9E y 10384 en hexadecimal). En esta imagen los caracteres son a su vez imágenes, pues puede suceder que ciertos caracteres que aparecen más abajo no se presenten en el monitor, detalle que luego explicaremos.

El propósito es analizar como se realiza la codificación en UTF-8 y proponer algoritmos en VBScript para codificar y decodificar en UTF-8. Aunque ahora no trataremos los casos de UTF 16, 32, se observa que con UTF-32BE la disposición de los códigos es la misma que la del original en hexadecimal: 41, 3A9, 8A9E, 10384. En cuanto a BE, LE no nos adentraremos en este tema pues no afecta a UTF-8, bastando decir que se usan para serializar los bytes. Así con LE nos llegarían los bytes de forma invertida, con el de menor peso (Little Endian) primero, mientras que con BE nos llegan de forma normal, con el de mayor peso (Big Endian) primero. Como UTF-32 no necesita ninguna transformación, entonces con UTF-32BE sucede que la disposición de los bytes es la natural de lectura izquierda-derecha, coincidiendo visualmente con la disposición de los códigos Unicode.

La siguiente tabla contiene la información relacionada con cada caracter:

  • Decimal Unicode: Unicode codifica unos 1.114.111 posiciones de caracteres, que en hexadecimal van de 0 a 10FFFF. Exceptuando un bloque de 2.048 posiciones (D800-DFFF) restringidas, quedan 1.112.064 posiciones para codificar caracteres. Una parte está ocupada por caracteres, otra parte están reservadas, pero de todas formas cada una de estas posiciones le corresponde un número de orden único.
  • Hexadecimal Unicode: La posición decimal se expresa con más conveniencia en hexadecimal debido a que un btye (8 bits) se puede representar con 2 dígitos hexadecimales. Así por ejemplo, FF se corresponde con el decimal 255, es decir, 8 bits puestos a 1, el binario 11111111.
  • Carácter: En esta columna presentamos directamente el caracter usando la referencia a caracteres de HTML. Por ejemplo, la letra A la incluimos como A, donde en este caso el código es el decimal Unicode. De igual forma hacemos para Ω, 語, 𐎄. Los dos primeros caracteres A y Ω pueden aparecer correctamente en el navegador sin más problemas. El tercero es una caracter del grupo CJK (Chino, Japonés, Coreano). Para que se presente es necesario instalar los archivos de códigos en Windows, en el Panel de Control, Configuración regional y de idioma, Idiomas, Compatibilidad con idioma adicional, Instalar archivos para idiomas de Asia Oriental. En cuanto al último pertenece al Ugarítico que, la verdad, no nos importa nada saber lo que es y, aunque en Windows no aparece, tampoco es necesario instalarlo. Lo importante es que en esta columna se ubica el caracter tal cual, aunque no pueda ser visualizado.
  • Imagen: De todas formas en esta columna ponemos la imagen de cada caracter que hemos copiado del cuadro superior. Si pudo instalar los idiomas que faltan podrá observar que se corresponde la imagen con el caracter, aunque como hemos dicho, no será necesario como veremos más adelante.
  • Definición: En la documentación de Unicode podemos obtener una lista completa de todos los códigos con la definición de cada caracter. Aquí la hemos trasladado.
  • Hexadecimal UTF-8: Observando el cuadro anterior, anotamos aquí la codificación en la transformación a UTF-8 de cada caracter.
  • Decimal UTF-8: Lo mismo pero en decimal.
Decimal UnicodeHexadecimal UnicodeCarácterImagenDefiniciónHexadecimal UTF-8Decimal UTF-8
6541Autf8 letra maýuscula A0041;LATIN CAPITAL LETTER A4165
9373A9Ωutf8 letra omega03A9;GREEK CAPITAL LETTER OMEGACE,A9206,169
354868A9Eutf8 caracter kbigfive8A9E; kBigFive (CJK Unified Ideographs)E8,AA,9E232,170,158
6643610384𐎄uft8 caracter ugarítico10384;UGARITIC LETTER DELTAF0,90,8E,84240,144,142,132

Ahora tenemos los siguientes objetivos:

  1. Por un lado comprobar si nuestro navegador tradujo correctamente la orden de inserción de caracteres realizada con la referencia a caracteres de HTML, hecha con &#N; siendo N el decimal Unicode original sin transformar.
  2. Aplicarlo en los casos en que leamos un fichero con el manejador de VBScript llamado FileSystemObject, con las siguientes acciones:
    • Intentar crear un algoritmo para leer documentos en UTF-8. Es decir, dados lo códigos transformados de un documento en UTF-8, obtener los códigos Unicode originales. Con estos códigos originales podemos volcarlos en nuestro editor mediante la función de VBScript denominada unescape("%uXXXX").
    • Realizar la función inversa, transformar desde el código original de Unicode a UTF-8, con el objeto de poder guardar los cambios en el fichero.

Comprobación de la transformación de caracteres realizada por el navegador

Recordemos que este documento que está leyendo está codificado en UTF-8. Pues bien, cuando incluímos en la tercera columna de la tabla anterior la referencia Ω, el navegador tendría que sustituir dicho caracter por el correspondiente según Unicode. Sin embargo no podemos observar la codificación directamente en este documento, pues si lo abrimos como texto sólo aparece la referencia ellos. Pero si hay una forma de comprobarlo.

Copiamos los caracteres de la tercera columna, uno a uno, y los pegamos en un nuevo documento de texto que permita guardar en UTF-8, como el NotePad de Windows. Así lo hemos hecho y ese contenido está en el archivo de texto codigos-utf8.txt. Hemos dejado un espacio entre caracter y caracter al copiarlos en el NotePad. No importa que no se presenten algunos caracteres si no se han instalado las fuentes del idioma, pues a pesar de no verse, esos caracteres están ahí.

Luego usamos VBScript, que comentaremos más abajo, para extraer estos códigos: EF,BB,BF,41,20,CE,A9,20,E8,AA,9E,20,F0,90,8E,84

Esa cadena de bytes se corresponde con los 4 caracteres que hemos copiado desde la tercera columna en la tabla de esta página y hemos pegado en el NotePad, donde se observa que la transformación a UTF-8 realizada desde el navegador al NotePad fue correcta según la tabla expuesta más arriba:

  • EF,BB,BF: Esta es una marca llamada BOM que inserta NotePad al inicio del archivo para designar que se guardó como UTF-8. No nos adentraremos ahora en esto.
  • 41: Corresponde al caracter A. Los códigos Unicode menores o iguales que 127 (7F en hexadecimal), no sufren ninguna transformación UTF-8 y se quedan tal cual como ASCII, tal que cada caracter ocupa un byte.
  • 20: El código 20 corresponde al espacio que hemos insertado entre caracter y caracter al copiarlos en el NotePad.
  • CE,A9: Corresponde al caracter Ω. Se observa que ocupa 2 bytes.
  • 20: Otro espacio.
  • E8,AA,9E: Corresponde al caracter 語. Ocupa 3 bytes.
  • 20: Espacio.
  • F0,90,8E,84: Corresponde al caracter 𐎄. Ocupa 4 bytes

Por lo tanto el primer objetivo está cumplido: el cuadro extraído de la documentación de Unicode con los ejemplos para esos cuatro caracteres coincide en la codificación UTF-8 con la que se ha guardado en el NotePad copiándolos directamente del navegador Explorer, como era de esperar por otro lado.

Seguridad sobre el uso de fileSystemObject

Antes de entrar en el objetivo de codificar y descodificar en UTF-8, conviene resaltar algunos detalles sobre el manejo de ficheros en VBScript.

VBScript es un lenguaje para uso de scripts con el navegador Internet Explorer. El que tenga capacidad para acceder al sistema de archivos supone un riesgo de seguridad. Por eso no ponemos ejemplos de ejecución de lo desarrollado aquí. Si lo desea puede descargar una carpeta comprimida con los archivos (ocupa unos 20 KB) y ejecutar en su ordenador, de forma local, la aplicación de ejemplo donde se observan estos algoritmos en ejecución. Y de paso también hay algunos usos relacionados con el manejo de ficheros mediante el fileSystemObject de VBScript. Después de descargar y descomprimir la carpeta, puede usar filesutf8.html con el navegador Explorer o bien usar filesutf8.hta directamente, pues es un ejecutable de VBScript.

Leer bytes desde un archivo de texto

Extraer cada byte es relativamente fácil usando FileSystemObject. A continuación reproducimos el código para extraer los bytes. Veáse que se abre el archivo en el modo 0, en ASCII, de tal forma que lee byte a byte. Con Hex(Asc(Mid(...))) extraemos un caracter, luego su código ASCII y por último lo convertimos en hexadecimal.

...        
Set gfso = CreateObject("Scripting.FileSystemObject")  
...
Set garchivoFile = gfso.GetFile(ruta)        
...
Set archivoTexto = garchivoFile.OpenAsTextStream(1, 0)
If archivoTexto.AtEndOfStream Then Exit Sub
todo = archivoTexto.ReadAll
For i = 1 To Len(todo)
    todoHex = todoHex & Hex(Asc(Mid(todo, i, 1))) & ","
Next
'Ya tenemos el resultado en todoHex
...        
    

De UTF-8 a Unicode: Algoritmo para leer documentos UTF-8

En el mismo origen de la documentación de Unicode que mencionábamos más arriba versions/Unicode5.0.0, podemos encontrar el Capítulo 3 Conformance, donde en la página 78 encontramos el siguiente cuadro:

utf8 claves

Este cuadro contiene la clave para extraer el código original desde un código transformado en UTF-8. Observamos que se clasifican las posibles posiciones de Unicode de 0 a 10FFFF, es decir, todas las posiciones posibles en el universo de códigos Unicode. Pero dependiendo de unos rangos se aplican de uno a cuatro bytes. Los primeros 128 bytes (0 a 127, 0 a 7F en hexadecimal) se corresponden con el ASCII no extendido. A partir de ahí se necesita más de un byte para expresar un caracter.

Hemos tomado los datos de esta tabla para crear la siguiente:

byte 1byte 2byte 3byte 4rango 1rango 2rango 3rango 4Rango totalSaltoDe Dec.A Dec.De Hex.A Hex.
deadeadeadea
0127128111128012707F
19422312819130641119201282047807FF
2242241601911281911326464204820484095800FFF
2252361281911281911264641491524096532471000CFFF
23723712815912819113264120485324855295D000D7FF
238239128191128191264641819220485734465535E000FFFF
240240144191128191128191148646419660865536262143100003FFFF
2412431281911281911281913646464786432262144104857540000FFFFF
2442441281431281911281911166464655361048576111411110000010FFFF
Total posiciones1112064

Hemos convertidos los valores hexadecimales de la tabla anterior en decimales para efectuar los cálculos que se explican a continuación, por lo que todas las columnas están en decimales a excepción de las dos últimas columnas que están en hexadecimal y son la conversión de las dos columnas anteriores.

Se observan 9 filas correspondientes a la división en sectores de todo el universo Unicode. En total 1.112.064 posiciones de código. En las columnas De Dec., A Dec. se exponen los códigos primero y último de cada rango. Se observan que son correlativos, 0-127, 128-2047, 2048-4095, etc. Sin embargo se produce un salto entre la posiciones 55296 y 57343 (D800 a DFFF en hexadecimal). Son exactamente 2048 posiciones de código que no son extraíbles. Se usan para transformaciones en UTF-16 que no vamos a explicar ahora, pero que para nuestros efectos es como si no existieran. Por eso hay un total de 1.112.064 posiciones, pero van desde la 0 a la 1.114.111 debido a ese salto.

Las columnas rango n son el resultado de calcular cuantas posiciones haya en cada byte. Por ejemplo, el primer byte de la primera fila va desde 0 a 127, por tanto hay 127-0+1 = 128 posiciones. El rango total es el producto de rango 1 x rango 2 x rango3 x rango 4, lo que nos da el total de posiciones que pueden expresarse en cada fila.

Veámos ahora con nuestros caracteres de ejemplo 65, 937, 35486 y 66436 en decimal (41, 3A9, 8A9E y 10384 en hexadecimal) que usamos más arriba. Su transformación en UTF-8 daba los códigos 41 para el original 41, CEA9 para el original 3A9, E8AA9E para el original 8A9E y F0908E84 para el 10384. Como vemos se trata de 4 ejemplos con 1 a 4 bytes. Partiendo de la transformación y, mediante la tabla anterior, queremos obtener los códigos originales. La cadena que leeríamos desde un archivo de texto sería en hexadecimal:

41,CE,A9,E8,AA,9E,F0,90,8E,84

y en decimal

65,206,169,232,170,158,240,144,142,132
  • Leemos un byte, el 65. Se comprueba en la tabla anterior, a que fila pertenece, viendo que el 65 está en el primer byte en 0-127, entonces le corresponde la fila primera y no hay que leer más bytes. Restamos el código del inicial de la fila: 65-0 = 65. Como no hay más bytes aquí acaba el cálculo. El código original es el mismo que el UTF, el 65.
  • Leemos el siguiente byte, el 206. El primer byte 206 está en la 2ª fila según vemos, entre 194 y 223. Por lo tanto este caracter tiene un segundo byte que habrá que leer, el 169. El primer byte ocupa la posición 206-194 = 12. Como por cada posición del primer byte hay 64 bytes en el segundo, entonces la posición relativa será 12 x 64 = 768. Además hay que sumar la posición inicial donde empieza la fila (128) más la posición relativa que ocupa el segundo byte 169-128 = 41. En total 128 + 768 + 41 = 937. Este 937 es el código original Unicode para este caracter.
  • Leemos el siguiente byte, el 232. Se sitúa en la fila 4ª, en las posiciones 225-236 de byte 1, exactamente en la posición 232-225 = 7. Vemos que hemos de leer 2 bytes más. El segundo será el 170 y el tercero el 158. Por cada posición del byte 1º existen 64 posiciones en el byte 2º y 64 en el 3º. Por lo tanto el byte 1º está en la posición relativa 7 x 64 x 64 = 28672. El byte 2º se sitúa en la posición 170-128 = 42. Por cada posición del byte 2º existen 64 posiciones en el byte 3º. Por lo tanto el byte 2º se sitúa en la posición relativa 42 x 64 = 2688. El byte 3º está en la posición 158-128 = 30 y es el último pues no hay byte 4º. Por lo tanto la posición final será, sumando la inicial de la fila 4096 (en la columna De Dec.), 4096 + 28672 + 2688 + 30 = 35486. Este 35486 es el código original para el tercer caracter.
  • Leemos el siguiente byte, el 240. Se sitúa en la fila en la 7ª fila, entre 240-240. Hay que leer tres bytes más, 144, 142 y 132. El byte 1º está en la posición 240-240 = 0 y la relativa 0 x 48 x 64 x 64 = 0. El byte 2º está en la posición 144-144 = 0 y la relativa 0 x 64 x 64 = 0. El byte 3º está en la posición 142 - 128 = 14 y la relativa 14 x 64 = 896. El byte 4º está en la posición 132 - 128 = 4 y no hay más bytes. Siendo la posición inicial de la fila la 65536 entonces sumamos 65536 + 0 + 0 + 896 + 4 = 66436. Este 66436 es el código original Unicode del cuarto caracter.

Se observa que podemos extraer el código original partiendo de la tabla. Entonces para la implementación en VBScript almacenamos la tabla en arrays. Sólo necesitamos 9 columnas de datos para los rangos de cada byte y el inicio del código de cada fila. Para cargar los datos sólo una vez hemos dispuesto el procedimiento cargaArrysUtf8. Así por ejemplo gini(n, m) es el array que contiene el valor de inicio del byte m en la fila n, mientras que gfin(n, m) contiene el valor final del byte m en la fila n. Por último el array ginicio(n) contiene el valor del inicio de la fila n.

'CODIFICAR Y DESCODIFICAR CARACTERES UTF8
'byte1      byte2       byte3       byte4       De Dec.
'  0-127                                        0
'194-223    128-191                             128
'224-224    160-191     128-191                 2048
'225-236    128-191     128-191                 4096
'237-237    128-159     128-191                 53248
'238-239    128-191     128-191                 57344 (salta D800-DFFF)
'240-240    144-191     128-191     128-191     65536
'241-243    128-191     128-191     128-191     262144
'244-244    128-143     128-191     128-191     1048576        
Sub cargaArraysUtf8()
    Dim arr, i
    arr = Array(194,224,225,237,238,240,241,244)
    For i = 0 To 7
        gini(0, i) = arr(i)
    Next
    arr = Array(223,224,236,237,239,240,243,244)
    For i = 0 To 7
        gfin(0, i) = arr(i)
    Next 
    arr = Array(128,160,128,128,128,144,128,128)
    For i = 0 To 7
        gini(1, i) = arr(i)
    Next
    arr = Array(191,191,191,159,191,191,191,143)
    For i = 0 To 7
        gfin(1, i) = arr(i)
    Next
    arr = Array(0,128,128,128,128,128,128,128)
    For i = 0 To 7
        gini(2, i) = arr(i)
    Next
    arr = Array(0,191,191,191,191,191,191,191)
    For i = 0 To 7
        gfin(2, i) = arr(i)
    Next
    arr = Array(0,0,0,0,0,128,128,128)
    For i = 0 To 7
        gini(3, i) = arr(i)
    Next
    arr = Array(0,0,0,0,0,191,191,191)
    For i = 0 To 7
        gfin(3, i) = arr(i)
    Next
    ginicio = Array(128,2048,4096,53248,57344,65536,262144,1048576)
    gcargadoArrayUtf8 = True
End Sub
    

El código del descodificador de UTF-8 a Unicode se expone a continuación. Como argumentos se pasa la cadena en UTF-8 desde donde queremos extraer sus códigos Unicode originales.

El argumento mostrarUnicode puede tomar valores 0, 1 o 2. Aparte de descodificar una cadena en UTF-8 (con el valor 0), esta función también sirver para extraer una muestra de los códigos UNICODE de cada caracter en decimal o hexadecimal con los valores 1 o 2 para ese argumento. En este caso se pone un límite al número de códigos extraídos, lo cual se especifica en la variable hasta.

El proceso es el mismo que el expuesto en el ejemplo. Si el caracter sólo tiene un byte se muestra tal cual. En otro caso se toma un byte y se busca la fila para saber cuantos bytes más hay que leer. Luego se hace el proceso de cálculo igual a como hicimos en el ejemplo anterior. El resultado se extrae cuando ya no hay más bytes que leer.

Function descodificaUtf8(cadenaUtf8, mostrarUnicode)
    Dim cod(4), i, j, k, todo, lonCadena, unicod, prod, hunicod, fila
    Dim hayError, preguntar, preguntarOtraVez, nodoCar, codMas, hasta
    If Not gcargadoArrayUtf8 Then cargaArraysUtf8
    'Este valor límita el número de muestras de códigos extraídos cuando mostrarUnicode vale 2 o 3
    hasta = 50
    lonCadena = Len(cadenaUtf8)
    hayError = False
    preguntar = True
    preguntarOtraVez = False
    i = 1
    'comprueba BOM para UTF8 si lo tiene puesto, en cuyo caso lo ignora y comienza a partir del 
    'siguiente caracter. El BOM es EF BB BF que se corresponde con los códigos 239,187,191
    gbom = ""
    If lonCadena>=3 Then
        If Asc(Mid(cadenaUtf8, 1, 1)) = 239 And Asc(Mid(cadenaUtf8, 2, 1)) = 187 And _
        Asc(Mid(cadenaUtf8, 3, 1)) = 191 Then
            gbom = Chr(239) & Chr(187) & Chr(191)
            i = 4
        End If
    End If
    Do While i <= lonCadena
        If mostrarUnicode > 0 And  i > hasta Then Exit Do
        cod(0) = Asc(Mid(cadenaUtf8, i, 1))
        If cod(0) < 128 Then
            If mostrarUnicode = 0 Then
                todo = todo & Chr(cod(0))
            ElseIf mostrarUnicode = 1 Then 'código Unicode en decimal
                todo = todo & cod(0) & ", " 
            Else '2, código Unicode en hexadecimal
                todo = todo & Hex(cod(0)) & ", "
            End If
        ElseIf cod(0) < 194 Or cod(0) > 244 Then
            todo = todo & Chr(cod(0))
            hayError = True
        Else 'estará en la tabla 194 a 244
            'busca en el primer byte la fila a la que pertenece
            fila = -1
            For j = 0 To 7 
                If cod(0) >= gini(0, j) And cod(0) <= gfin(0, j) Then
                    fila = j
                    Exit For
                End If
            Next
            If fila = -1 Then
                todo  = todo & Chr(cod(0))
                hayError = True
            Else
                'extrae y comprueba los siguientes bytes
                unicod = ginicio(fila)
                For j = 1 To 4
                    If j=4 Or gini(j, fila) = 0 then 'ya no hay más bytes
                        unicod = unicod + cod(j-1) - gini(j-1, fila)
                        hunicod = "" & Hex(unicod)
                        If mostrarUnicode = 0 Then
                            If len(hunicod)>4 Then 'es unicode con más de 2 bytes
                                'ponemos la referencia HTML al caracter porque la función unescape() no
                                'permite más de 2 bytes
                                Set nodoCar = Document.GetElementById("car")
                                nodoCar.innerHtml = "&#" & unicod & ";"
                                codMas = nodoCar.innerHtml
                            ElseIf len(hunicod)<4 Then 'menos de 2 bytes usamos unescape("%uXXXX")
                                codMas = unescape("%u" & String(4-Len(hunicod), "0") & hunicod)
                            Else 'exactamente 2 bytes, usamos unescape("%uXXXX")
                                codMas = unescape("%u" & hunicod)
                            End If
                            todo = todo & codMas
                        ElseIf mostrarUnicode = 1 Then 'unicode en decimal
                            todo = todo & unicod & ", " 
                        Else '2, unicode en hexadecimal
                            todo = todo & hunicod & ", "
                        End If
                        Exit For
                    Else
                        i = i + 1
                        If i>LonCadena Then
                            todo  = todo & Chr(cod(0))
                            hayError = True
                            Exit For
                        End If
                        cod(j) = Asc(Mid(cadenaUtf8, i, 1))
                        If cod(j) >= gini(j, fila) And cod(j) <= gfin(j, fila) Then
                            prod = 1
                            For k = j To 3
                                prod = prod * (gfin(k,fila)-gini(k,fila)+1)
                            Next
                            unicod = unicod + (cod(j-1)-gini(j-1, fila))*prod
                        Else
                            todo  = todo & Chr(cod(0))
                            hayError = True
                            Exit For
                        End If
                    End If
                Next
            End If
        end if
        If hayError And preguntar Then
            If MsgBox("Se ha encontrado un error: Puede que el texto no esté en UTF-8, " & _
            "¿Seguir descodificando?",4) = 7 Then
                hayError = False
                Exit Do
            Else 
                preguntarOtraVez = True
                preguntar = False
            End If
        End If
        i = i + 1
    Loop
    descodificaUtf8 = todo
    If hayError Then
        Alert("No se efectuó correctamente la descodificación del texto desde UTF-8. " & _
        "Puede que el texto tuviera otra codificación, inténtelo con UTF-16, ASCII o Sistema.")
    End If
End Function

Se controlan los errores pues si algún byte no está en los rangos de la primera columna da un error con la posibilidad de no seguir descodificando. Aunque lee la marca BOM de entrada que nos permite detectar si un archivo está codificado en UTF-8 (EF BB BF), hay que tener en cuenta que no siempre estará presente esta marca. De hecho se recomienda no incluir esta marca en los documentos, pues supone un estorbo para ciertas aplicaciones. Por ejemplo, si creamos un XHTML en un NotePad en UTF-8, el NotePad agrega esta marca al inicio. Luego no podremos validar el DTD del documento pues un XML debe tener al inicio, antes que nada, algo como <?xml version="1.0" encoding="UTF-8"?>. Por lo tanto no pondremos como condición necesaria que un documento en UTF-8 tenga la marca, aunque si la tiene hemos de detectarla para no manejarla como un caracter.

Una vez conocido el código original Unicode hemos de trasladarlo al texto, agregando ese caracter a la variable todo que va almacenando los caracteres convertidos, variable que luego devolveremos al final de la función. El código Unicode, variable unicod lo convertimos a hexadecimal, variable hunicod, en forma de un String "XXXX". Se dan los casos:

  • Si el código hexadecimal tiene 4 dígitos, lo convertimos a un caracter con la función de VBScript unescape("%u" & hunicod).
  • Si el código tiene menos de 4 dígitos, formateamos con cero hasta completar los cuatro dígitos, por ejemplo, "00XX" y aplicamos unescape("%u" & "00XX").
  • Si tiene más de 4 dígitos (puede tener hasta 6), no podemos aplicar unescape. Entonces usamos una vía indirecta. Obligamos a que sea el propio navegador quién incruste el caracter en una parte del documento. Para ello disponemos de un elemento <div id="car" style="display:none"></div> en el cuerpo del documento HTML. Este elemento queda oculto y mediante innerHtml recibe el caracter que luego recogemos para añadir a nuestra variable todo.

El algoritmo expuesto antes es, por supuesto, mejorable. Especialmente en lo referente a los cálculos de multiplicaciones con el objeto de conseguir una mejora en el coste de las mismas, pues se podrían almacenar en una tabla para no tener que efectuarlas. Por ejemplo, cuando la posición relativa del primer byte es 0 no sería necesario que realice las multiplicaciones. Además habría que controlar las posiciones no accesibles D800-DFFF e ignorarlas, aunque en un UTF-8 bien formado no deberían encontrarse estas posiciones de código.

Hemos realizado una prueba en VBScript usando el archivo de muestra codigos-utf8.txt, abriéndolo con las tres opciones posibles que nos permite el objeto de VBScript FileSystemObject así como abierto con el algoritmo descodificador utf8 expuesto antes:

Modo de abrirImagen obtenida en pantalla
ASCII con FileSystemObjectmuestra código
Sistema con FileSystemObjectmuestra código
Unicode UTF-16 con FileSystemObjectmuestra código
UTF-8 con nuestro algoritmomuestra código

El modo Sistema del FileSystemObject no es capaz de detectar la marca BOM del UTF-8 ( correspondientes a EF,BB,BF), por lo que el resultado es que abre el archivo como el modo ASCII, aplicando un caracter por byte leído. El modo UTF-16, también del FileSystemObject, no reconoce la marca BOM de UTF-8, por lo que intenta aplicar UTF-16 de tal forma que el texto no es reconocible. Sólo con el algoritmo anterior se consigue convertir adecuadamente los 4 caracteres. Si se han instalado las fuentes para idiomas de Asia Oriental (chino, japonés, coreano,...), se podrá ver el tercer caracter de los cuatro que contiene el archivo, pues los dos primeros se verán sin necesidad de instalar otras fuentes. Recordemos que el cuarto caracter es del grupo Ugaritic en Unicode y que aquí no pudimos instalar su fuente.

De Unicode a UTF-8: Algoritmo para escribir documentos UTF-8

Se trata de tomar los caracteres que hemos escrito en el área de texto y convertirlos a UTF-8. Primero tenemos que saber cuál es el código Unicode de cada caracter. Usando la función escape nos dará el código en UTF-16. Sólo tenemos que saber leer UTF-16 y convertirlo en UTF-8.

Nuestra cadena de ejemplo A, Ω, 語, 𐎄 se correspondía con los códigos Unicode &#65, &#937, &#35486;, &#66436. Entonces aplicando unescape a cada uno de estos caracteres nos daría:

  • escape(A) = "A"
  • escape(Ω) = "%u03A9"
  • escape(語) = "%u8A9E"
  • escape(𐎄) = "%uD800%uDF84"

Las funciones escape/unescape se confirieron para escapar caracteres Unicode en las dirección URI. Por eso los caracteres ASCII hasta 127 que no sean signos de puntuación se pasan tal cual, como en este ejemplo la "A". Su código Unicode es el Asc("A") = 65, es decir, el código ASCII. El resto se codifica con "%uXXXX", donde XXXX es el código Unicode pero en UTF-16. Nosotros detectaremos cuando una conversión comienza con "%" teniendo en cuenta que hasta el código Unicode FFFF (65535 en decimal), en UTF-16 se representa tal cual, sin ninguna tranformación. Asi la cadena "%u03A9" tiene el Unicode hexadecimal 3A9 que es el 937 en decimal. El siguiente "%u8A9E" será el Unicode hexadecimal 8A9E, 35486 en decimal. Los Unicode a partir de FFFF, es decir de 10000 hasta 10FFF se transforman en dos grupos o sustitutos, como en este ejemplo "D800 DF84". El primer sustituto estará en el rango D800-DBFF y el segundo en el rango DC00-DFFF (en decimal 55296-56319 y 56320-57343).

Si num1, num2 son los valores decimales del primero y segundo sustituto respectivamente, entonces hallamos la diferencia que hay con la base de cada rango, es decir, restaSup = num1 - 55296 y restaInf = num2 - 56320. Así el número Unicode final se calcula con num = 65536 + restaSup * 1024 + restaInf. Veámos esto para el ejemplo:

El D800 en hexadecimal es el num1 = 55296
El DF84 en hexadecimal es el num2 = 57220
Por lo tanto restaSup = num1 - 55296 = 55296 - 55296 = 0
Y restaInf = num2 - 56320 = 57220 - 56320 = 900
Finalmente num = 65536 + restaSup * 1024 + restaInf = 66436.

En resumen, ya tenemos los códigos Unicode 65, 937, 35486, 66436 de cada caracter leídos directamente desde, por ejemplo, un elemento HTML como <textarea>. La segunda parte es convertirlos en UTF-8. Para ello hacemos la operación inversa de la señalada antes:

  • 65. Buscamos en la tabla la fila a la que pertenece observando por las columnas De Dec. a Dec., por el inicio del rango. En este caso está en la primera fila y devolvemos el código tal cual 65.
  • 937. Le corresponde la segunda fila (De 128 a 2047). Primero buscamos la posición relativa en esa fila 937-128 = 809. Luego Multiplicamos los valores de rango2 x rango3 x rango4 = 64 x 1 x 4 = 64. Realizamos la división entera 809 \ 64 = 12. Este valor más el inicial del primer byte en esa fila, es decir 12 + 194 = 206 será el primer byte UTF-8. Vemos el resto de la división 809 Mod 64 = 41 y lo aplicamos al segundo byte 41 + 128 = 169, siendo este el segundo btye UTF-8. Así el Unicode 937 se convierte en el UTF-8 206, 169 en decimal o el CE, A9 en hexadecimal.
  • 35486. Le corresponde la fila 4096 a 53247. La posición relativa será 35486 - 4096 = 31390. El producto de rangos es 64 x 64 x 1 = 4096. La división entera 31390 \ 4096 = 7, entonces el primer byte será 225 + 7 = 232. El resto 31390 Mod 4096 = 2718. Como este valor supera el margen del segundo byte, volvemos a aplicar el procedimiento, pero ahora reduciendo los rangos de los productos, es decir rango3 x rango 4 = 64 x 1 = 64. La división entera 2718 \ 64 = 42. Segundo byte 42 + 128 = 170. El resto es 2718 Mod 64 = 30, por lo que el tercer byte será 30 + 128 = 158. Entonces el Unicode 35486 se transforma en el UTF-8 232, 170, 158 en decimal, el E8,AA,9E en hexadecimal.
  • 66436. Fila 65536 - 262143. Resta 66436-65536=900. Producto rangos 48x64x64=196608. División 900 \ 196608 = 0. Primer byte 0 + 240 = 240. Resto 900 Mod 196608 = 900. Producto rangos 64x64=4096. División 900 \ 4096 = 0 Segundo byte 0 + 144 = 144. Resto 900 Mod 4096 = 900. Producto rangos 64. División 900 \ 64 = 14. Tercer byte 14 + 128 = 142. Resto 900 Mod 64 = 4. Cuarto byte 4 + 128 = 132. Por tanto el Unicode 66436 se compone en UTF-8 de 4 bytes 240, 144, 142, 132, es decir F0, 90, 8E, 84 en hexadecimal.

Por lo tanto la cadena Unicode 65, 937, 35486, 66436 en decimal o bien 41, 3A9, 8A9E, 10384 en hexadecimal, que hemos extraído del elemento <textarea> y la hemos convertido a UTF-8 en 41, CE, A9, E8, AA, 9E, F0, 90, 8E, 84. Usamos la función siguiente para convertir un caracter Unicode a UTF-8. Pero hay que leer caracter a caracter en el <textarea>, extraer el código Unicode tal como hicimos arriba y luego aplicar esta función para transformar ese código Unicode en 1 a 4 bytes UTF-8. Esta función devuelve un String con los códigos decimales de 1 a 4 bytes separados por comas. En el procedimiento de vuelta reemplaza esos valores decimales con Chr() para insertar un byte ASCII en el archivo final ya en UTF-8.

Function codificaByteUtf8(cod)
    Dim devuelve, fila, codRango, i, j, prod, numero, num, conError, ok
    If Not gcargadoArrayUtf8 Then cargaArraysUtf8
    If Not(IsNumeric(cod)) Then 'Error, no es un número
        codificaBytesUtf8 = ""
        Exit Function
    ElseIf cod<0 Then 'Error, no puede ser negativo
        codificaBytesUtf8 = ""
        Exit Function
    ElseIf cod < 128 Then 'lo devuelve tal cual
        codificaBytesUtf8 = cod
        Exit Function
    ElseIf cod > 1114111 Then 'Error, supera máximo código de Unicode
        codificaBytesUtf8 = ""
        Exit Function
    Else 'busca la fila
        ok = False
        For fila = 1 To 7  
            If ginicio(fila) > cod  Then 
                fila = fila -1 
                ok = True
                Exit For
            End If
        Next
        If Not ok Then fila = 7
    End If
    codRango = cod - ginicio(fila)
    devuelve = ""
    conError = False
    For i = 1 To 4
        If i=4 Or gini(i,fila) = 0 Then 'no hay más bytes o estamos en el cuarto byte y no nay más
            devuelve = devuelve & "," & (gini(i-1,fila)+codRango)
            Exit For
        Else
            prod = 1
            For j = i To 3
                prod = prod * (gfin(j, fila)-gini(j,fila)+1)
            Next
            numero = codRango \ prod
            num = gini(i-1,fila) + numero 
            If gini(i-1,fila) <= num <= gfin(i-1,fila) Then
                If devuelve = "" Then
                    devuelve =  num
                Else
                    devuelve = devuelve & "," & num
                End If
                codRango = codRango Mod prod
            Else 'supone error
                conError = True
                Exit For
            End If
        End If
    Next
    If conError Then
        codificaByteUtf8 = ""
    Else
        codificaByteUtf8 = devuelve
    End If
End Function
    

La función que usa la anterior y que convierte un texto en UTF-8 es la siguiente, donde el argumento "cadena" es el texto extraído del <textarea> y que será devuelto transformado en UTF-8. Primero busca el código Unicode que le corresponde a cada caracter y luego llama a la función anterior (señalado en azul) para transformar a UTF-8 ese caracter. Luego va agregando bytes a la salida que se devolverá.

Function codificaUtf8(cadena)
    Dim i, lonCadena, todo, car, car0, car1, car2, numHex, num, strCod, arrayCod, j
    Dim restaSup, restaInf, hayError
    cadena = escape(cadena)
    lonCadena = Len(cadena)
    i = 1
    hayError = 0
    Do While i<=lonCadena
        car0 = Mid(cadena, i , 1)
        If car0 = "%" Then 
            i = i + 1
            If i>lonCadena Then
                hayError = 1
                Exit Do
            End If
            car1 = Mid(cadena, i, 1)
            If car1 = "u" Then 'otros caracteres con ASCII > 255 se ponen como %uXXXX
                i = i + 4
                If i>lonCadena Then
                    hayError = 2
                    Exit Do
                End If               
                car2 = Mid(cadena, i-3, 4)
                Execute "numHex = &H" & car2
                num = CLng(numHex)
                If num<0 Then
                    num = 65536+num
                End If
                If num>55295  And num<57344 Then 
                    'Será un Unicode en UTF16 para Unicodes >= &H10000; (65536 en decimal) de tal forma 
                    'que se codifica con los dos sustitutos D800-DBFF y DC00-DFFF, en total 4 bytes
                    If num>55295 And num<56320 Then 'El primer byte sustituto estará en D800-DBFF
                        restaSup = num - 55296  'Restamos de D800 = 55296
                        i = i + 6 'extraemos el segundo sustituto como "%uXXXX"
                        If i>lonCadena Then
                            hayError = 3
                            Exit Do
                        End If                          
                        car2 = Mid(cadena, i-5, 6)
                        If Left(car2, 2) = "%u" Then
                            car2 = Right(car2, 4)
                            Execute "numHex = &H" & car2
                            num = CLng(numHex)
                            If num<0 Then
                                num = 65536+num
                            End If
                            If num>56319 And num<57344 Then 'El 2º byte sustituto estará en DC00-DFFF
                                restaInf = num - 56320 'Restamos de DC00 = 56320
                                'Extraemos el número Unicode con esta fórmula
                                num = 65536 + restaSup * 1024 + restaInf
                            Else 'En otro caso será un error de codificación
                                hayError = 4
                                Exit Do
                            End If
                        Else 'será error
                            hayError = 5
                            Exit Do
                        End If
                    Else 'En otro caso será un error de codificación
                        hayError = 6
                        Exit Do
                    End If
                End If
                strCod = codificaByteUtf8(num)
            If strCod="" Then
                hayError = 7
                Exit Do
            Else
                arrayCod = Split(strCod, ",", -1, 1)
                If IsEmpty(arrayCod) Or UBound(ArrayCod)=-1 Then
                    hayError = 8
                    Exit Do
                Else
                    For Each j In arrayCod
                        todo = todo & Chr(j)
                    Next
                End If
            End If
        Else 'Son ASCII <= 255, espacios, puntuaciones y otros ASCII extendidos se ponen como %XX
            i = i + 1
            If i>lonCadena Then
                hayError = 9
                Exit Do
            End If
            car2 = Mid(cadena, i, 1)
            Execute "numHex = &H" & car1 & car2
            num = CLng(numHex)
            If num>=0 And num<=127 Then 'los ASCII<=127 se pasan tal cual
                todo = todo & Chr(num)
            Else 'Para códigos en 128 - 255 los convierte a UTF-8 también
                strCod = codificaByteUtf8(num)
                If strCod="" Then
                    hayError = 10
                    Exit Do
                Else
                    arrayCod = Split(strCod, ",", -1, 1)
                    If IsEmpty(arrayCod) Or UBound(ArrayCod)=-1 Then
                        hayError = 11
                        Exit Do
                    Else
                        For Each j In arrayCod
                            todo = todo & Chr(j)
                        Next
                    End If
                End If
            End If
        End If
    Else 'Los ASCII sin incluir espacios, puntuaciones y otros se pasan tal cual (letras, dígitos)
        todo = todo & car0
    End If
    i = i + 1
Loop
If hayError>0 Then
    Alert("Error al codificar en UTF-8 (Error " & hayError & "). No se codificó a UTF-8.")
    codificaUtf8 = cadena
    gError = True
Else
    codificaUtf8 = todo
    gError = False
End If
End Function