terça-feira, 21 de outubro de 2008

Porque usar UTF-8 - Codificando/Decodificando

Bom galera, como prometido venho hoje aqui demonstrar como o UTF-8 realmente funciona por debaixo dos panos, e vamos criar uma implementação do mesmo utilizando JavaScript.

Como eu já havia dito antes, o UTF-8 usa uma codificação de tamanho variável, onde cada caractere pode ocupar entre 1 e 4 bytes, para que isso seja possível, existem bits que são usados para verificar isso, vamos ver o primeiro caso:

Eu falei para vocês anteriormente que para os valores ASCII padrão o UTF-8 mantém a compatibilidade, ou seja, não existe nenhum transformação para os caracteres entre 0 e 127, se você olhar o binário disso, vai perceber que isso varia entre:
00000000
01111111

É importante notar que o primeiro bit do byte não é modificado, isso é importante porque ele é flag que indica se vamos precisar de mais bytes no caractere, então, a partir do valor 128 temos que utilizar algum tipo de transformação.

Antes de proseguirmos com os detalhes vamos iniciar nosso script, comecem ele da seguinte forma:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Conversor de UTF-8</title>
<script type="text/javascript" charset="utf-8">

get = function(id) {
return document.getElementById(id);
};

UTF8 = {
encode: function(content) {
return content;
},

decode: function(content) {
return content;
}
}

encodeFromTo = function(source, destiny, method) {
var value = get(source).value;

get(destiny).value = method(value);
};

window.onload = function() {
get('encode_button').onclick = function() {
encodeFromTo('original_area', 'encoded_area', UTF8.encode);
};

get('decode_button').onclick = function() {
encodeFromTo('encoded_area', 'original_area', UTF8.decode);
};
};

</script>
<style type="text/css" media="screen">

textarea {
width: 300px;
height: 200px;
}

</style>
</head>
<body>
<textarea id="original_area"></textarea><br />
<button type="button" id="encode_button">Codificar para UTF-8</button>
<button type="button" id="decode_button">Decodificar de UTF-8</button><br />
<textarea id="encoded_area"></textarea>
</body>
</html>

Esse é um simples script que por hora não faz nada :P

Mas ele pega o valor do campo de cima e mostra esse valor codificado no campo de baixo, assim como pega o campo de baixo e joga decoficidado no campo de cima. A partir de agora só iremos trabalhar nos métodos encode e decode do objeto UTF8 que criamos acima.

Voltando ao UTF-8, vamos criar primeiro o codificador, então vamos continuar o entendimento dos bytes.

A partir do momento que o primeiro bit deveria ser usado, então a coisa muda, imaginando um caso simples, o caractere de numero 128, se continuassemos usando a maneira simples de somar bits teriamos a seguinte sequencia de bits:
10000000
Mas para o UTF-8 a sequencia correta seria:
11000010 10000000
Parece complicado, mas não é para tanto, quando passamos dos 128 caracteres iniciais temos alguns "moldes" onde devemos encaixar nossos bits, que no caso são os seguintes (por uso de bytes):
1 byte - 0xxxxxxx
2 bytes - 110xxxxx 10xxxxxx
3 bytes - 1110xxxx 10xxxxxx 10xxxxxx
4 bytes - 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

Esses moldes não são ao acaso, e como pode-se ver eles seguem um padrão, esses moldes são importantes para que o precesso de decodificação, notem que a partir dos primeiros bits do primeiro byte é possivel saber exatamente quantos bytes tem o caractere atual.

Para criamos o processo de encoding, o primeiro passo é descobrir quantos bytes serão nescessários para criar nosso caractere no formato UTF-8, para saber isso é só contar os limites a partir do número de bits que podem ser alocados em cada nível. Mas como eu sou muito legal eu já vou disponibilar esses números para você, e são eles: 128, 2048, 65536

Pra quem nunca executou operações bit a bit pode parecer um pouco complicado sair mexendo eles por ai, mas é facil, primeiro mantenha a calculadora do windows aberta (em modo científico, assim você pode converter entre decimal, hexadecimal e binario sempre que precisar, e você vai precisar), então vamos criar a função de encode:
var encoded = ''; //iniciamos com uma string vazia

for (var i = 0; i < content.length; i++) { //iniciando uma iteracao sobre cada caractere da string
var c = content.charCodeAt(i); //pegando o codigo do caractere na posicao atual

//verificando numero de bytes
if (c < 128) { //1 byte
encoded += String.fromCharCode(c); //simplesmente recolocamos o caractere sem modificações
} else if (c < 2048) { //2 bytes
encoded += String.fromCharCode((c >> 6) | 0xC0); //primeiro caractere
encoded += String.fromCharCode((c & 0x3F) | 0x80); //segundo caractere
} else if (c < 65536) { //3 bytes
encoded += String.fromCharCode((c >> 12) | 0xE0); //primeiro caractere
encoded += String.fromCharCode(((c >> 6) & 0x3F) | 0x80); //segundo caractere
encoded += String.fromCharCode((c & 0x3F) | 0x80); //terceiro caractere
} else { //4 bytes
encoded += String.fromCharCode((c >> 18) | 0xF0); //primeiro caractere
encoded += String.fromCharCode(((c >> 12) & 0x3F) | 0x80); //segundo caractere
encoded += String.fromCharCode(((c >> 6) & 0x3F) | 0x80); //terceiro caractere
encoded += String.fromCharCode((c & 0x3F) | 0x80); //quarto caractere
}
}

return encoded;

Pra quem não está acostumado com manipulação de bits isso pode parecer coisa de louco, mas é simples, vou ilustrar como funcionam os operadores bit a bit utilizados nas operações:

right shift (>>): esse operador simplesmente move bits a direita, caso o bit ultrapasse o primeiro ele é descartado, exemplos:
00011000 >> 2
resulta em: 00000110
01101110 >> 3
resulta em: 00001101

ou seja, eh simplesmente mover bits a direita (se lembre, tudo isso é forma de representação, tudo são bits, desde numeros a caracteres, vc não vai digitar em formato binário nunca, pelo menos não em javascript)

E binário (&): executa uma operação E entre binários, a idéia é simples, você coloca os bits que são comparados em uma listagem de 1 para 1, caso o valor seja 1 nos 2, o resultádo será 1, caso contrário será 0, exemplos:
  00110111
& 01100010
----------
  00100010

  01111000
& 00111110
----------
  00111000

esse operador é muito usado para mascarar bits, por exemplo, você quer apenas os 4 últimos bits de um dado binário, então você opera um E contendo 00001111 sobre esse binário, dessa forma você terá o resultado com os 4 últimos valores.

OU binário (|): esse é parecido com o anterior, mas com uma diferença básica, esse retorn 1 exceto se os 2 operadores forem 0, exemplo:
  00110101
| 11100001
----------
  11110101

Isso são alguns operadores binários, caso não tenha ficado muito claro podem tirar suas dúvidas comigo por comentários ou e-mail.

Dever de casa: pegue os números utilizados no algoritmo anterior em formato hexadecimal (que comecam com 0x) e veja suas formas em binário (use a calculadora do windows ou similar), e va executando o algoritmo como se fosse o computador, dessa forma você terá um melhor entendimento do algoritmo.

O encode está pronto, agora precisamos pegar os códigos Unicode a partir disso que geramos, então vamos criar o decode:
var decoded = ''; //string vazia
var i = 0; //iterador
var n = c1 = c2 = c3 = c4 = 0; //buffers

while (i < content.length) {
c1 = content.charCodeAt(i);

if (c1 < 128) { //1 byte
decoded += String.fromCharCode(c1); //sem conversão, padrão ASCII
i++; //proximo byte
} else if (!(c1 & 0x20)) { //2 bytes
c2 = content.charCodeAt(i + 1);
decoded += String.fromCharCode(((c1 & 0x1F) << 6) | (c2 & 0x3F));
i += 2; //pular 2 bytes
} else if (!(c1 & 0x10)) { //3 bytes
c2 = content.charCodeAt(i + 1);
c3 = content.charCodeAt(i + 2);
decoded += String.fromCharCode(((c1 & 0xF) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F));
i += 3; //pular 3 bytes
} else if (!(c1 & 0x8)) { //4 bytes
c2 = content.charCodeAt(i + 1);
c3 = content.charCodeAt(i + 2);
c4 = content.charCodeAt(i + 3);
decoded += String.fromCharCode(((c1 & 0x7) << 18) | ((c2 & 0x3F) << 12) | ((c3 & 0x3F) << 6) | (c4 & 0x3F));
i += 4; //pular 4 bytes
}
}

return decoded;


O processo inverso é exatamente a mesma coisa, é só pegar os bits que você jogou no formato e junta-los novamente. Para descobrir quantos bytes são usados eu utilizei a seguinte idéia:

1 - se for menor que 128, então é padrão ASCII, jogar direto
2 - verificar bit zero dentro do primeiro byte, a partir disso é possível descobrir o número (se baseando nos formatos possíveis).

O único operador novo é o left shift (<<) que simplesmente move os bits para esquerda ;)

Bom galera, termino por aqui, qualquer dúvida entrem em contato comigo por e-mail ou comentários.

Mudarei de assunto no próximo post (assunto indefinido até o momento ;).

See yah!