Nível 1: bits e bytes
O computador só tem 0 e 1. Um bit é um interruptor; 8 bits formam um byte, capaz de representar 28=256 valores diferentes. Somar valores posicionais converte binário em decimal: 01000001 = 64 + 1 = 65. Todo texto acaba sendo uma sequência de bytes, e "codificação de caracteres" é uma tabela que diz qual número representa qual caractere — tabelas diferentes são codificações diferentes.
Níveis 2–3: ASCII
O ASCII, definido em 1963, usava 7 bits para 128 caracteres: 0–31 são caracteres de controle como quebra de linha, tabulação e campainha; 48–57 são os dígitos 0–9; 65–90 são as maiúsculas e 97–122 as minúsculas. A tabela não é arbitrária: maiúsculas e minúsculas diferem em 32 (o 6º bit), então c | 0x20 transforma maiúscula em minúscula, e subtrair 48 de um dígito dá o valor numérico. O hexadecimal é a forma habitual de ler bytes, dois dígitos por byte: 0x48 0x69 = Hi. O ASCII usava só 7 bits e deixava o 8º livre — cada país depois preencheu esse bit do seu jeito (Latin-1, GBK, Shift_JIS…), de forma incompatível entre si, e é aí que está a raiz histórica do mojibake.
Nível 4: pontos de código Unicode
O Unicode numera os caracteres do mundo inteiro em um único esquema, chamado ponto de código (code point) e escrito como U+ mais hexadecimal: A é U+0041, 中 é U+4E2D e 😀 é U+1F600. O espaço vai de U+0000 a U+10FFFF, dividido em 17 "planos", com os caracteres de uso corrente quase todos no plano básico (U+0000–U+FFFF) e os emoji e ideogramas raros nos planos suplementares. O ponto essencial: ponto de código é só numeração, e a forma de transformá-lo em bytes é decidida por "formas de codificação" como UTF-8, UTF-16 e UTF-32. "Unicode" não é uma codificação; utf-8 é.
Níveis 5–8: os modelos de comprimento variável do UTF-8
O UTF-8 (RFC 3629) usa de 1 a 4 bytes por ponto de código, e todas as regras estão escritas no prefixo do primeiro byte:
| Faixa de pontos de código |
Bytes |
Modelo |
Bits de carga |
| U+0000–U+007F |
1 |
0xxxxxxx |
7 |
| U+0080–U+07FF |
2 |
110xxxxx 10xxxxxx |
11 |
| U+0800–U+FFFF |
3 |
1110xxxx 10xxxxxx 10xxxxxx |
16 |
| U+10000–U+10FFFF |
4 |
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
21 |
Basta completar o binário do ponto de código com zeros à esquerda até a quantidade de bits de carga e preencher os x em ordem para obter os bytes. é = U+00E9 = 11101001; completado até 11 bits fica 00011 101001, e preenchendo o modelo de dois bytes chega-se a 11000011 10101001 = C3 A9.
Esse desenho traz três vantagens: texto em ASCII codificado em UTF-8 fica byte a byte idêntico (compatibilidade total para trás); todo byte de continuação começa com 10, então dá para achar o início do próximo caractere em qualquer posição do fluxo (autossincronia, e perder um byte não desalinha o resto); e codificações de comprimentos diferentes nunca se sobrepõem, de modo que um byte zero como 00 só representa U+0000 (seguro para strings em C). O preço é que um caractere chinês ocupa 3 bytes, 50% mais que os 2 do GBK — mas inglês, pontuação e código não são afetados, e por isso o UTF-8 é a codificação de mais de 98% das páginas da web.
Os níveis 7 e 8 pedem que você digite um caractere de 3 bytes e depois um de 4 bytes. Este último revela uma armadilha ainda comum: o conjunto de caracteres utf8 do MySQL, na verdade, só vai até 3 bytes, então gravar um emoji dá erro ou trunca, e é obrigatório usar utf8mb4.
Nível 9: UTF-16 e .length
JavaScript, Java, C# e a API do Windows usam UTF-16 internamente: cada unidade de código (code unit) tem 16 bits. Um caractere do plano básico ocupa uma unidade; um ponto de código de plano suplementar é dividido em duas unidades, chamadas par substituto (surrogate pair), nas faixas D800–DBFF e DC00–DFFF. Por isso "😀".length === 2, por isso "😀".split("") corta o emoji em duas metades sem sentido e por isso str[0] devolve o substituto alto. Para contar caracteres de verdade, use [...str].length (iterando por ponto de código); para contar o que a pessoa percebe como "um caractere" (incluindo emoji combinados e acentos), use Intl.Segmenter.
Níveis 10–11: Base64
O Base64 (RFC 4648) não é criptografia: é uma forma de transportar bytes quaisquer como 64 caracteres ASCII imprimíveis (A–Z a–z 0–9 + /), para levar binário a lugares que só aceitam texto — anexos de e-mail, campos JSON, URLs data:, JWT. O método é pegar 3 bytes = 24 bits, cortar em 4 grupos de 6 bits (26=64) e consultar cada um: Man = 4D 61 6E = 01001101 01100001 01101110 → 010011 010110 000101 101110 → T W F u. O tamanho, portanto, cresce para 4/3 do original.
Quando a quantidade de bytes não é múltipla de 3, o último grupo fica incompleto: falta 1 byte, entra um =; faltam 2, entram dois =, assim o comprimento da saída é sempre múltiplo de 4. O byte único A tem 8 bits, é completado com zeros até 12 e cortado em 010000 01(0000) → QQ, seguido de ==. A variante URL-safe troca + / por - _ (para nada precisar de escape em uma URL) e costuma omitir o =, que o decodificador repõe pelo comprimento.
Nível 12: codificação percentual de URL
Em uma URL só uma pequena parte dos caracteres ASCII pode aparecer como está (RFC 3986 §2). Espaço e os caracteres reservados com sentido sintático, como &, =, ? e /, além de todos os caracteres não ASCII, precisam ser escritos como % mais o hexadecimal de cada byte. Então a codificação de URL é UTF-8 primeiro, % byte a byte depois: 中 → E4 B8 AD → %E4%B8%AD, transformando um caractere chinês em nove caracteres. Em JavaScript, encodeURIComponent codifica de forma mais completa (inclusive / ? & =) e serve para um valor de parâmetro; encodeURI preserva os caracteres estruturais e serve para o endereço inteiro. Um formulário enviado como application/x-www-form-urlencoded ainda escreve o espaço como + — convenção histórica dos formulários HTML, e não algo da RFC 3986.
Níveis 13–14: de onde vem o mojibake e como salvar o texto
Mojibake não é dado corrompido: é escrever com a codificação A e ler com a codificação B. O 中 em UTF-8 é E4 B8 AD:
- Lendo como Windows-1252 / Latin-1 (um byte, um caractere), os três bytes viram as três letras ocidentais
ä¸ — uma sequência de letras com acentos é a marca de "UTF-8 lido como codificação ocidental".
- Lendo como GBK (dois bytes por caractere chinês),
E4 B8 forma um caractere chinês e AD fica pela metade — "ideogramas estranhos e meio caractere" é a marca de "UTF-8 lido como GBK", e 浣犲ソ e 涓枃 são desse tipo.
- No sentido contrário, texto em GBK lido como UTF-8 encontra sequências de bytes inválidas e produz uma enxurrada do caractere de substituição
� (U+FFFD).
Enquanto os bytes originais não se perderem, reler com a codificação correta restaura o texto por completo. Por isso a primeira regra para consertar mojibake é guardar o arquivo original e não converter várias vezes: cada conversão pode trocar um byte inválido por ? ou �, e aí sim há perda real de informação. O nível 14 pede uma operação inversa: ler o mojibake 浣犲ソ como GBK para voltar aos bytes E4 BD A0 E5 A5 BD e então ler como UTF-8 para obter 你好. A solução moderna é declarar UTF-8 de forma consistente no cabeçalho HTTP, no <meta charset> do HTML, na conexão do banco e na caixa de salvar arquivo, para que a incompatibilidade não tenha chance de acontecer.
BOM e ordem de bytes
O UTF-16 tem duas ordens de bytes, big-endian e little-endian, e o arquivo costuma começar com uma marca de ordem de bytes (BOM), FE FF ou FF FE. O UTF-8 não tem esse problema, mas o Bloco de Notas do Windows costumava gravar o BOM do UTF-8, EF BB BF, por padrão, o que quebra a primeira linha de um script de shell e coloca um caractere invisível na frente do nome da primeira coluna de um CSV. Se a visualização de decomposição do UTF-8 mostra esses três bytes no começo, você já sabe a origem.