Convertendo ISO-8859-1 para UTF-8 de forma segura em PHP

Artigo que apresenta a implementação de uma função em PHP para converter caracteres ISO-8859-1 para UTF-8 de forma segura, preservando possíveis sequências UTF-8 presentes no texto original e garantindo que a saída seja 100% UTF-8.

Introdução

Já falamos várias vezes sobre Unicode, desde a definição de unicode, funções para manupular unicode, como utilizar unicode em todas camadas do sistema e como representar os símbolos na forma de html entities.

Sabemos que ISO-8859-1 foi uma codificação muito utilizada antes do surgimento do Unicode, mas que a tendência é que tudo seja migrado para Unicode e, no caso do ISO-8859-1, preferencialmente migrado para UTF-8. Porém, aplicações web estão sujeitas a situações das mais adversas, quando se trata de dados enviados pelo usuário. Existem casos em que um usuário consegue colar um texto em que parte dele deveria ser ISO-8859-1 e parte dele deveria ser UTF-8. Isso pode ocorrer ao copiar e colar trechos de aplicações para aplicações em sistemas de "origem duvidosa", que acabam gerando uma bagunça de bytes que, por acaso, acabam sendo enviadas para nossa aplicação.

Para resolver este problema, elaborei uma função parecida com utf8_encode, ou seja, converte os caracteres de ISO-8859-1 para UTF-8, porém, caso a função identifique um caractere UTF-8 no texto, ela o mantem intacto. Portanto, ela é útil para garantir que o texto final seja 100% UTF-8 válido.

Linguagem: PHP

Copyright 2012 Rubens Takiguti Ribeiro

Licença: LGPL 3 ou superior

/**
 * Função que converte caracteres ISO-8859-1 para UTF-8, mantendo os caracteres UTF-8 intactos.
 * @param string $texto
 * @return string
 */
function sanitizar_utf8($texto) {
    $saida = '';

    $i = 0;
    $len = strlen($texto);
    while ($i < $len) {
        $char = $texto[$i++];
        $ord  = ord($char);

        // Primeiro byte 0xxxxxxx: simbolo ascii possui 1 byte
        if (($ord & 0x80) == 0x00) {

            // Se e' um caractere de controle
            if (($ord >= 0 && $ord <= 31) || $ord == 127) {

                // Incluir se for: tab, retorno de carro ou quebra de linha
                if ($ord == 9 || $ord == 10 || $ord == 13) {
                    $saida .= $char;
                }

            // Simbolo ASCII
            } else {
                $saida .= $char;
            }

        // Primeiro byte 110xxxxx ou 1110xxxx ou 11110xxx: simbolo possui 2, 3 ou 4 bytes
        } else {

            // Determinar quantidade de bytes analisando os bits da esquerda para direita
            $bytes = 0;
            for ($b = 7; $b >= 0; $b--) {
                $bit = $ord & (1 << $b);
                if ($bit) {
                    $bytes += 1;
                } else {
                    break;
                }
            }

            switch ($bytes) {
            case 2: // 110xxxxx 10xxxxxx
            case 3: // 1110xxxx 10xxxxxx 10xxxxxx
            case 4: // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
                $valido = true;
                $saida_padrao = $char;
                $i_inicial = $i;
                for ($b = 1; $b < $bytes; $b++) {
                    if (!isset($texto[$i])) {
                        $valido = false;
                        break;
                    }
                    $char_extra = $texto[$i++];
                    $ord_extra  = ord($char_extra);

                    if (($ord_extra & 0xC0) == 0x80) {
                        $saida_padrao .= $char_extra;
                    } else {
                        $valido = false;
                        break;
                    }
                }
                if ($valido) {
                    $saida .= $saida_padrao;
                } else {
                    $saida .= ($ord < 0x7F || $ord > 0x9F) ? utf8_encode($char) : '';
                    $i = $i_inicial;
                }
                break;
            case 1:  // 10xxxxxx: ISO-8859-1
            default: // 11111xxx: ISO-8859-1
                $saida .= ($ord < 0x7F || $ord > 0x9F) ? utf8_encode($char) : '';
                break;
            }
        }
    }
    return $saida;
}

13 comentários

Anônimo disse...

Muito bom esse script, eu sempre precisei de algo assim, mas não tinha a espertize para criar, parabéns Rubens, pelo excelente trabalho nesse blog

Mah disse...

Amigo, desculpe a ignorância, estou precisando fazer essa conversao mas, onde coloco esse codigo que voce passou?

Rubens Takiguti Ribeiro (autor do blog) disse...

Mah, se não sabe onde colocar, sugiro que coloque no mesmo arquivo onde irá usar a função (pode ser no início ou no final do arquivo).

No entanto, o ideal é que a função fique em um arquivo separado e você inclua o arquivo usando "include" ou "require".

Mah disse...

Rubens, obrigado pela luz... só mais uma coisa... nesse codigo passado tem que editar alguma coisa? rsrrs

Rubens Takiguti Ribeiro (autor do blog) disse...

Mah, não precisa editar nada. É só incluir a função e usá-la. Se conhece bem a sintaxe de funções em PHP, segue um link útil:
http://www.php.net/manual/pt_BR/functions.user-defined.php

Rubens Takiguti Ribeiro (autor do blog) disse...

Olá, Éric
O caractere "&" é representado da mesma forma tanto no ISO-8859-1 quanto no UTF-8, então seu problema deve ser com entities html.
Dê uma olhada nestes artigos:
http://rubsphp.blogspot.com.br/2011/02/html-entities.html
http://rubsphp.blogspot.com.br/2010/10/unicode.html

Anônimo disse...

Parabéns, não sei dizer o quanto de tempo que economizei!! Ótimo trabalho.

Anônimo disse...

Poderia me explicar como utilizo essa função? To quebrando cabeça tentando usa-la a dias.
Eu preciso usa-la nessas variáveis:
$title = $this->db->quote($data['title']);
$keywords = $this->db->quote($data['keywords']);
$desc = $this->db->quote($data['desc']);
$pagetext = $this->db->quote($data['page']);

Rubens Takiguti Ribeiro (autor do blog) disse...

Olá, Anônimo
Para usar a função, basta incluí-la no arquivo e usá-la:

$title = $this->db->quote(sanitizar_utf8($data['title']));
$keywords = $this->db->quote(sanitizar_utf8($data['keywords']));
$desc = $this->db->quote(sanitizar_utf8($data['desc']));
$pagetext = $this->db->quote(sanitizar_utf8($data['page']));