A vueltas con la Codificación de Caracteres

Veamos...¿qué diferencia hay entre los siguientes textos?

01. Niño, ¿cuántos €uros te costó?

02. Niño, ¿cuántos €uros te costó?
03. Ni¤o, ¨cu ntos �uros te cost¢?
04. Ni±o, ┐cußntos Çuros te cost¾?
05. Ni�o, �cu�ntos �uros te cost�?

Pues es sencillo. La frase es la misma, pero hay una diferencia entre la codificación (o juego de caracteres) escogida para guardar el texto y el juego de caracteres con el que ese texto se presenta en pantalla.

Investigando...

Ya llevo cierto tiempo estudiando diferentes aspectos sobre Criptografía.

En el camino de estos estudios, me topé con los algoritmos que generan valores HASH (las "funciones Hash", o "funcionaes resumen", generan una secuencia de números que "resumen" un conjunto de información; normalmente se usan para corroborar que un texto u otra información no ha sido alterada, ya que un simple cambio en la información produce un valor de hash totalmente diferente).

Concretamente he profundizado en el estudio del algoritmo SHA-1, llegando incluso a desarrollar una implementación de este algoritmo en Javascript, como comento en detalle en este otro post.

Precisamente en este trabajo con el algoritmo SHA-1 me encontré que un mismo texto generaba Hash diferentes si se calculaba con la rutina que había desarrollado comparado con el resultado de la función SHA1 de PHP.

Tras comprobar que no se trataba de bugs en mi desarrollo (o tras depurar los bugs encontrados), seguía sin conseguir el mismo Hash que generaba PHP al intentar cifrar textos que contenían ciertos caracteres, como el símbolo de uro.

Así que tocaba investigar.

Y tras unas cuantas horas, y días, "googleando" por aquí y por allá, creo haber llegado a entender bastantes aspectos de la codificación de caracteres.

Una de las páginas que me ha dado la clave para entender por qué aparecen "caracteres extraños" en los textos, y que me inspiró para escribir este artículo, es esta, en la que el autor hace un estudio de las diferentes formas en las que aparece el nombre de la ciudad "A Coruña" en las cartas y envios que recibe.

He llegado a la conclusión, por cierto, que es éste un tema muy, muy embarullado. Existen un sinfín de codificaciones, aparecidas en las últimas décadas, buscando dar cobertura a las necesidades de los diferentes idiomas. En ocasiones buscando la unificación y la homogeneidad, y en otras haciendo lo que se puede dentro de la limitación de los 8 bits. Muchas de ellas muy parecidas pero no iguales, para complicar más la cosa.

Diferentes Codificaciones (juegos de caracteres)

Así tenemos a: ASCII / ANSI, DOS-850, Windows 1252, ISO-8859-1, Unicode UTF-16 ó Unicode UTF-8 entre muchos otros. La verdad es que la Wikipedia está muy bien nutrida de artículos sobre este tema y se puede extraer mucha información al respecto.

En cada Juego de Caracteres un valor numérico representa un caracter imprimible (o un caracter de control).

En unos juegos de caracteres, como ISO-8859-1, el valor que representa cada caracter está comprendido entre 0 y 255 (en decimal) (entre 0x00 y 0xFF hexadecimal) ya que solo se utiliza un byte por caracter.

En otras codificaciones como UTF-16 se usan hasta 4 bytes para cada caracter, con lo que los valores posibles son entre 0 y 4.294.967.296 (en decimal) (0x00 a 0xFFFFFFFF en hexadecimal), aunque en este caso no se usa todo este rango.

UTF-8, por su parte, utiliza un número variable de bytes para representar cada caracter, pudiendo necesitar entre 1 y 4 bytes.

Volvamos sobre la pregunta

Bueno, OK. Y tras todo este rollo ¿cuáles son las diferencias en la codificación de la frase anterior?

Niño, ¿cuántos €uros te costó?

Pues veamos su codificación en el juego de caracteres en el que fue grabada esta página: guardar-como notepad++ Windows 1252, también llamado "ANSI" al editar y grabar la página con el Bloc de Notas de Windows y en otras herramientas de edición como el Notepad++ ... guardar-como bloc notas windows

Nota. Actualización mayo-2019.
Esta página es en realidad un PHP. Para que todo funcione he tenido que meter la siguiente directiva al principio de la página, para forzar a que se abra esta página en ISO-8859-1.

< ? php header ('Content-type: text/html; charset=iso-8859-1'); ? >

  Niño, ¿cuántos
Codific.
(HEX)
Windows 1252 4E69F16F2C20BF6375E16E746F73
  uros te costó?
Codific.
(HEX)
Windows 1252 8075726F7320746520636F7374F33F

Y ahora veamos los diferentes resultados presentados en la cabecera de este post, que ocurren cuando la codificación usada para grabar el texto no es la misma que la usada para presentarlo.

Veamos cada ejemplo...:

02. Grabado en UTF-8. Presentado en Windows-1252

  Niño, ¿cuántos
Codific.
(HEX)
Grabado
UTF-8
4E69C3B16F2C20C2BF6375C3A16E746F73
Visualizado
Windows 1252
Niño, ¿cuántos
  uros te costó?
Codific.
(HEX)
Grabado
UTF-8
E282AC75726F7320746520636F7374C3B33F
Visualizado
Windows 1252
€uros te costó?

03. Grabado en DOS-850. Presentado en Windows-1252

  Niño, ¿cuántos
Codific.
(HEX)
Grabado
DOS-850
4E69A46F2C20A86375A06E746F73
Visualizado
Windows 1252
Ni¤o, ¨cu ntos
  uros te costó?
Codific.
(HEX)
Grabado
DOS-850
075726F7320746520636F7374A23F
Visualizado
Windows 1252
uros te cost¢?

Una curiosidad:
El símbolo del €uro no existe en la codificación DOS-850, pero sí en la DOS-858. Precisamente esta codificación, la DOS-858, fue creada en el año 1998 a partir de la DOS-850 solo para introducir el símbolo del €uro.
Se utilizó el valor hexadecimal "D5", que en la codificación DOS-850 se usa para el caracter "ı".

04. Grabado en Windows-1252. Presentado en DOS-850

  Niño, ¿cuántos
Codific.
(HEX)
Grabado
Windows 1252
4E69F16F2C20BF6375E16E746F73
Visualizado
DOS-850
Ni±o, cußntos
  uros te costó?
Codific.
(HEX)
Grabado
Windows 1252
8075726F7320746520636F7374F33F
Visualizado
DOS-850
Çuros te cost¾?

05. Grabado en Windows-1252. Presentado en UTF-8

  Niño, ¿cuántos
Codific.
(HEX)
Grabado
Windows 1252
4E69F16F2C20BF6375E16E746F73
Visualizado
UTF-8
Nio, cuntos
  uros te costó?
Codific.
(HEX)
Grabado
Windows 1252
8075726F7320746520636F7374F33F
Visualizado
UTF-8
uros te cost?

Un poco de codificación

Y para finalizar vamos a ver como he conseguido generar mediante programación en PHP las tablas anteriores que representan las diferentes codificaciones.

Para ello he hecho uso de la función iconv de PHP, que convierte cadenas de caracteres de una codificación a otra.

Esta función "iconv" toma un texto considerando que se encuentra en una determinada codificación original, y busca los mismos caracteres considerando una codificación destino. Lo que devuelve por tanto es el mismo caracter, pero con un valor numérico diferente.

Esto es, que si utilizamos la función ord() de PHP antes y después de la conversión de "iconv", el valor devuelto debe ser diferente, si es que en las codificaciones usadas en "iconv" el caracter tiene un código diferente.

Ejemplo:
Supongamos que tenemos el caracter "ñ" y nuestra página está grabada en Windows-1252.
Este caracter tiene el valor 0xF1 (hexadecimal) en codificación Windows-1252.
Entonces, si utilizamos la función ...

	$c_utf8 = iconv("Windows-1252", "UTF-8", "ñ");

... el valor devuelto será el mismo caracter, una "ñ", pero con un valor diferente ya que el valor en codificación UTF-8 para este caracter es diferente.
De hecho devuelve 2 bytes, ya que el valor de la "ñ" en UTF-8 es 0xC3B1 (hexadecimal)

Para no volvernos locos con esta función, lo mejor es pensar en ella como una función que trabaja con valores numéricos en vez de caracteres.
O sea, en nuestro ejemplo, pensar que le entra un 0xF1 y devuelve un 0xC3B1, porque internamente es precisamente lo que hace.

... Y si luego ese valor va a ser representado como caracter en una página web, pensar en el "charset" de nuestra página y pensar como va a representarse este valor 0xC3B1 en ese charset (en nuestro ejemplo, con charset=windows-1252 ó iso-8859-1 la representación será "ñ").

	<!-- Ejemplo etiqueta META con charset -->
	<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" /> 

Nota. Actualización mayo-2019.
Esta página es en realidad un PHP. Para que todo funcione he tenido que meter la siguiente directiva al principio de la página, para forzar a que se abra esta página en ISO-8859-1.

< ? php header ('Content-type: text/html; charset=iso-8859-1'); ? >

La función "iconv" puede manejar valores por defecto para la conversión. Se pueden recuperar con la función iconv_get_encoding(). En el servidor donde está alojada esta página están vigentes los siguientes:
input_encoding = ISO-8859-1
output_encoding = ISO-8859-1
internat_encoding = ISO-8859-1

Ejemplo: 02.Grabado en UTF-8. Presentado en Windows-1252

Código fuente para presentar el texto ...

<?php
	$txt1 = "Niño, ¿cuántos";
	$txt2 = "€uros te costó?";
?>

<?php
	// Muestra el texto en una fila de la tabla
	for ($i=0; $i<strlen($txt1); $i++) {
		$c = substr($txt1, $i, 1);
		if ($c == " ") $c = " ";
		echo "<td>" . $c . "</td>";
	}
?>

Resultado ...

  Niño, ¿cuántos

Código fuente para mostrar codificación UTF-8 ...

<?php
	// Grabado en UTF-8
	for ($i=0; $i<strlen($txt1); $i++) {
		$c = substr($txt1, $i, 1);
		$c_utf8 = iconv("Windows-1252", "UTF-8", $c);
		$c_utf8_hex = "";
		for ($j=0; $j<strlen($c_utf8); $j++) {
			$c_utf8_hex .= strtoupper(dechex(ord(substr($c_utf8, $j, 1))));
		}
		echo "<td>" . $c_utf8_hex . "</td>";
	}
?>

Resultado ...

Grabado
UTF-8
4E69C3B16F2C20C2BF6375C3A16E746F73

Código fuente para mostrar visualización Windows 1252 ...

<?php
	// Visualizado en Windows 1252
	for ($i=0; $i<strlen($txt1); $i++) {
		$c = substr($txt1, $i, 1);
		// Como esta página se muestra en Windows-1252 (o ISO-8859-1)
		// al convertir el caracter a UTF-8 y luego mostrarlo
		// normalmente en la página, tiene el efecto de que un caracter
		// codificado en UTF-8 se muestre como Windows-1252
		// Por ejemplo, el caracter "¿" es el 0xBF en Windows-1252, que
		// al convertirlo a UTF-8 es el 0xC2BF, que al mostrar un 0xC2BF en 
		// Windows-1252 aparece los caracteres "¿".
		echo "<td>" . iconv("Windows-1252", "UTF-8", $c) . "</td>";	
	}
?>

Resultado ...

Visualizado
Windows 1252
Niño, ¿cuántos

Otro Ejemplo: 04.Grabado en Windows-1252. Presentado en DOS-850

Código fuente para mostrar codificación Windows 1252 ...

<?php
	// Grabado en Windows 1252
	for ($i=0; $i<strlen($txt1); $i++) {
		$c = substr($txt1, $i, 1);
		// Código hexadecimal del caracter en Windows 1252
		$c_hex = strtoupper(dechex(ord($c)));	
		echo "<td>" . $c_hex . "</td>";
	}
?>

Resultado ...

Grabado
Windows 1252
4E69F16F2C20BF6375E16E746F73

Código fuente para mostrar visualicación DOS-850 ...

<?php
	// Visualizado en DOS-850
	for ($i=0; $i<strlen($txt1); $i++) {
		$c = substr($txt1, $i, 1);
		// Se considera como si el caracter estuviera codificado en CP850 y se toma el correspondiente en UTF-16
		// Por ejemplo, el caracter "¿" guardado en Windows 1252 es 0xBF hexadecimal.
		// Este código interpretado en CP-850 es el caracter 0x2510 en UNICODE y UTF-16 (9488 en decimal), y es un caracter no imprimible en Windows 1252 
		// (Ver tabla DOS-850 en la Wikipedia)
		// UTF-16 aporta 4 caracteres, por lo que se coge el 1ro y el 2do, 
		// (la "LE" de "UTF-16LE" quiere decir "little endian", o sea, los bits de menor peso primero
		// o sea, 0x2510 se lee como 4 bytes de valores: 0x10 0x25 0x00 0x00 en este orden)
		// y se representa directamente en decimal con el formato de html &#ddd;
		$c_850_utf16 = iconv("CP850", "UTF-16LE", $c);	
		$c1 = substr($c_850_utf16, 0, 1);
		$c2 = substr($c_850_utf16, 1, 1);
		$c_dec = ord($c2) * 256 + ord($c1);
		echo "<td>" . "&#" . $c_dec . ";" . "</td>";
	}
?>

Resultado ...

Visualizado
DOS-850
Ni±o, cußntos

Ojo con el "Little endian" y "Big endian"

Como veréis en el código, la conversión a "UTF-16" se ha forzado al código "UTF-16LE" donde "LE" quiere decir "Little Endian", o sea, los bits menos significativos se leen primero.
Esto es, un valor UNICODE 0x2510 se lee en "UTF-16LE" como 4 bytes en el siguiente orden: 0x10 0x25 0x00 0x00
También se podría haber usado "UTF-16BE" (Big Endian).
En todo caso lo mejor es no dejarlo al azar, o sea, evitar la codificación directa a UTF-16 sin especificar LE ni BE, porque cada servidor puede operar de manera diferente al ejecutar la función "iconv" (como me pasó a mí la subir la página al servidor después de haberla probado en local).

... y se acabó ...

Bueno... Pues aquí lo dejo. Espero que haya sido de utilidad para alguno de vosotros.

Contactar

Si te interesa contactar, siempre puedes enviar un e-mail a jfvv@jfvv.xyz.