Problemas com emojis e caracteres especiais em transmissões de dados no JS

Em cenários empresariais, é comum utilizar sockets para transmitir conteúdo textual entre pontos A e B. No entanto, ao lidar com emojis ou símbolos especiais, foi relatado que, após a transmissão:

  • No iOS, os emojis são substituídos por interrogações.
  • Em dispositivos PC, os caracteres aparecem como texto corrompido.

O problema está relacionado à má manipulação do conjunto de caracteres UTF-8 durante o processamento de emojis e outros símbolos complexos. Esses elementos pertencem a áreas específicas da tabela Unicode que requerem quatro bytes para representação adequada.

Entendendo o Unicode

O Unicode é um padrão global criado para atribuir uma codificação exclusiva a todos os caracteres conhecidos, desde letras até hieróglifos antigos. Abaixo estão alguns conceitos importantes associados ao Unicode:

  • Glyphs (Caracteres): Representam unidades de escrita, como letras, números e sinais de pontuação.
  • Pontos de código (Code Points): São identificadores numéricos únicos atribuídos a cada caractere Unicode, geralmente expressos na forma U+XXXX.
  • Formas de codiifcação: Métodos para converter esses pontos de código em sequências de bytes, incluindo UTF-8, UTF-16 e UTF-32.

Planos Unicode

O Unicode organiza seus caracteres em planos distintos:

  • BMP (Plano Multilíngue Básico): Contém caracteres amplamente utilizados, de U+0000 a U+FFFF.
  • SMP (Plano Multilíngue Suplementar): Inclui símbolos antigos e musicais, de U+10000 a U+1FFFF.
  • SIP (Plano Ideográfico Suplementar): Focado em ideogramas chineses, japoneses e coreanos, de U+20000 a U+2FFFF.

Codificações Unicode Comuns

  • UTF-8: Codificação variável de 1 a 4 bytes, compatível com ASCII.
  • UTF-16: Usa 2 ou 4 bytes, dependendo se o caractere está dentro ou fora do BMP.
  • UTF-32: Usa sempre 4 bytes, sendo mais simples mas menos eficiente em termos de espaço.

Análise do Problema

A raiz do problema está na conversão inadequada dos emojis para ArrayBuffer antes do envio via socket. O método utilizado emprega uma lógica personalizada baseada em charCodeAt, que não suporta corretamente caracteres que ultrapassam o BMP.

Aqui está uma versão simplificada do algoritmo original:


function encodeString(value) {
    let buffer = [];
    for (let i = 0; i < value.length; i++) {
        let code = value.charCodeAt(i);
        
        if (code <= 0x7F) {
            buffer.push(code);
        } else if (code <= 0x7FF) {
            buffer.push(0xC0 | (code >> 6), 0x80 | (code & 0x3F));
        } else if (code <= 0xFFFF) {
            buffer.push(0xE0 | (code >> 12),
                       0x80 | ((code >> 6) & 0x3F),
                       0x80 | (code & 0x3F));
        }
    }
    return new Uint8Array(buffer);
}

Como solução alternativa, pode-se usar o método codePointAt para tratar corretamente caracteres multibyte:


function encodeWithCodePoint(value) {
    let buffer = [];
    let position = 0;
    while (position < value.length) {
        let code = value.codePointAt(position);
        position += (code > 0xFFFF) ? 2 : 1;

        if (code <= 0x7F) {
            buffer.push(code);
        } else if (code <= 0x7FF) {
            buffer.push(0xC0 | (code >> 6), 0x80 | (code & 0x3F));
        } else if (code <= 0xFFFF) {
            buffer.push(0xE0 | (code >> 12),
                       0x80 | ((code >> 6) & 0x3F),
                       0x80 | (code & 0x3F));
        } else if (code <= 0x10FFFF) {
            buffer.push(0xF0 | (code >> 18),
                       0x80 | ((code >> 12) & 0x3F),
                       0x80 | ((code >> 6) & 0x3F),
                       0x80 | (code & 0x3F));
        }
    }
    return new Uint8Array(buffer);
}

Solução Mais Simples

A abordagem ideal seria utilizar APIs modernas como TextEncoder e TextDecoder para lidar com a codificação de forma nativa:


// Codificação
const encoder = new TextEncoder();
const encodedData = encoder.encode('😀 Olá');
console.log(encodedData); // Uint8Array contendo os bytes codificados

// Decodificação
const decoder = new TextDecoder('utf-8');
const decodedString = decoder.decode(new Uint8Array([240, 159, 152, 128, 79, 108, 97]));
console.log(decodedString); // "😀 Olá"

Considerações Finais

Para evitar problemas futuros:

  • Não use charCodeAt ao lidar com emojis ou caracteres fora do BMP. Opte por codePointAt.
  • Prefira APIs como TextEncoder e TextDecoder para garantir uma codificação robusta e padronizada.

Tags: javascript Unicode TextEncoder

Publicado em 8-5 18:36