RLE (Run-Length Encoding): sua primeira técnica de compressão

RLE (Run-Length Encoding): sua primeira técnica de compressão

Olá leitor, seja muito bem vindo de volta a mais uma etapa da nossa jornada aqui no Portal da Micilini 😊

Este é o quinto artigo da nossa série de ~20, e o segundo da Fase 2 — Fundamentos de Compressão.

No artigo anterior, a gente entendeu os conceitos fundamentais, onde vimos um pouco mais sobre o que é compressão, a diferença entre lossless e lossy, o que é redundância, e o que é entropia.

Porém, vimos tudo aquilo dentro de conceitos muito teóricos, com pouquíssimo código, sem nenhuma implementação.

Pois bem, acabou a moleza 😄

A partir de agora, vamos começar a colocar a mão na massa e aprender técnicas concretas de compressão.

E a primeira delas é a mais simples, mais intuitiva e mais fácil de entender que existe: o RLE (Run-Length Encoding), ou em português, Codificação por Comprimento de Sequência.

O RLE é tão simples que você provavelmente já usou a lógica dele na vida real sem saber.

E apesar de ser "básico", ele é usado de verdade em formatos reais como o BMP, o PCX, o fax (sim, aquela máquina de fax do escritório), e até como parte interna de CODECs mais complexos.

Então pega seu café ☕, sente-se confortavelmente, e vamos aprender a sua primeira técnica de compressão de dados!

A ideia de compressão de dados mais simples do mundo

Imagina que eu te peça pra anotar a seguinte sequência de cores num papel:

vermelho, vermelho, vermelho, vermelho, vermelho, azul, azul, azul, verde, verde, verde, verde, verde, verde, verde

Quantas palavras você precisou escrever naquele papel?

Obviamente que 15, ou seja, uma pra cada item da sequência.

Agora, se eu te pedisse pra anotar a mesma informação de um jeito mais "esperto", o que você faria?

Provavelmente você poderia fazer algo assim:

5× vermelho, 3× azul, 7× verde

No final, você teria 6 "palavras" (3 contagens + 3 cores) em vez de 15. Mesma informação, menos da metade do tamanho. E você consegue reconstruir a sequência original perfeitamente a partir dessa anotação.

Isso meus amigos é o que eu chamo de RLE 😁

A ideia é absurdamente simples: em vez de armazenar cada valor individualmente, você armazena quantas vezes ele se repete em sequência, seguido do valor em si.

O nome "Run-Length Encoding" vem exatamente dessa ideia, vejamos:

  • Run = uma "corrida" de valores iguais consecutivos
  • Length = o comprimento (quantas vezes) dessa corrida
  • Encoding = a forma de codificar isso

Sendo assim, podemos dizer que o termo "5× vermelho", nada mais é do que um run de comprimento (length) onde o vermelho deve se repetir 5 vezes. Simples assim.

RLE em bytes: como funciona no computador?

É claro que o nosso computador n~~ao trabalha com "vermelho", "verde" ou "azul". No caso dele, ele trabalha com bytes de informação (valores de 0 a 255).

Imagine a seguinte sequência de bytes, representando, por exemplo, uma linha de pixels como na representação abaixo:

Dados originais: 45 45 45 45 45 45 120 120 120 200 200 200 200 200 200 200 200

Contando: o valor 45 aparece 6 vezes seguidas, depois 120 aparece 3 vezes, depois 200 aparece 8 vezes.

No total são 17 bytes de dados originais.

Com RLE, codificamos assim:

Dados comprimidos: [6, 45] [3, 120] [8, 200]

Cada par é: (contagem, valor). São 6 bytes no total (3 pares × 2 bytes por par).

Olhando assim, fizemos uma redução de 17 bytes para 6 bytes, o que equivale a 65% de redução!

E a compressão é lossless: dá pra reconstruir a sequência original perfeitamente a partir dos dados comprimidos.

Se quiséssemos reconstruir, poderíamos ler cada par e "expandir" da seguinte forma:

[6, 45]  → escreve 45 seis vezes:     45 45 45 45 45 45
[3, 120] → escreve 120 três vezes:    120 120 120
[8, 200] → escreve 200 oito vezes:    200 200 200 200 200 200 200 200

E no final juntar tudo em um só: 45 45 45 45 45 45 120 120 120 200 200 200 200 200 200 200 200

Resultado: temos a sequência original, exatamente como era. Sem perda nenhuma.

Quando o RLE brilha (e quando ele falha miseravelmente)?

Agora, antes de você sair achando que o RLE é o melhor algoritmo do mundo, preciso te contar um segredo: ele tem um calcanhar de Aquiles enorme.

O RLE só funciona bem quando existem muitas repetições consecutivas.

Quando os dados têm "runs" longos (muitos valores iguais em sequência), o RLE comprime demais. Mas quando os dados são variados, sem repetição... COSTUMA DAR RUIM 🥴

Vamos ver um exemplo do pior caso:

Dados originais: 10 20 30 40 50 60 70 80

8 bytes, todos diferentes. Nenhuma repetição. Se aplicarmos a estratégia de RLE nele, teríamos o seguinte:

Dados "comprimidos": [1, 10] [1, 20] [1, 30] [1, 40] [1, 50] [1, 60] [1, 70] [1, 80]

Opa.... temos agora 16 bytes! O arquivo "comprimido" ficou o dobro do original! 😱

Isso acontece porque cada valor individual vira um par (1, valor), gastando 2 bytes onde antes gastava 1. O RLE está literalmente adicionando informação em vez de remover.

É como aquela piada: "Doutor, dói quando eu faço isso." — "Então não faça isso."

Se não tem repetição, o RLE não ajuda, pelo contrário, ele só atrapalha.

Pra ficar bem claro, vamos comparar os cenários diferentes.

Melhor caso (muita repetição):

Original:    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0  (20 bytes)
RLE:         [20, 0]                                     (2 bytes)
Compressão:  90%! 🎉

Caso médio (alguma repetição):

Original:    5 5 5 3 3 7 7 7 7 2                        (10 bytes)
RLE:         [3, 5] [2, 3] [4, 7] [1, 2]                (8 bytes)
Compressão:  20% 👍

Pior caso (zero repetição):

Original:    1 2 3 4 5 6 7 8 9 10                       (10 bytes)
RLE:         [1,1] [1,2] [1,3] [1,4] [1,5] [1,6]...     (20 bytes)
Compressão:  -100% (DOBROU!) 💀

🎯 Regra de ouro do RLE: Ele é ótimo pra dados com muita redundância espacial (valores consecutivos iguais). É péssimo pra dados variados e "ruidosos".

Onde o RLE é usado na vida real?

Neste caso, apesar da sua simplicidade (e limitação), o RLE é usado de verdade em vários contextos:

1. Máquinas de Fax

Você sabia que a máquina de fax usa RLE?

Quando você manda um fax, a máquina escaneia a página linha por linha, transformando cada pixel em preto ou branco (1 bit por pixel).

Como a maioria das linhas de um documento tem muitos pixels brancos seguidos (as margens, os espaços entre palavras, as linhas em branco), o RLE comprime isso brutalmente.

Uma linha de 1728 pixels onde os primeiros 200 são brancos, depois 50 pretos (uma palavra), depois 300 brancos, depois 80 pretos (outra palavra)... vira o seguinte:

[200, branco] [50, preto] [300, branco] [80, preto]...

Em vez de transmitir 1728 bits, transmite muito menos.

É por isso que faxes de documentos de texto são rápidos, mas faxes de fotos (que não têm grandes áreas uniformes) demoram uma eternidade.

2. Formato BMP (Bitmap)

O formato de imagem BMP, aquele que o Windows usa desde os anos 90, tem uma opção de compressão RLE embutida (BMP-RLE4 e BMP-RLE8).

Quando a imagem tem grandes áreas de cor uniforme (como ícones, logos simples, ou diagramas), o RLE comprime razoavelmente bem.

Mas pra fotos? Terrível. É por isso que arquivos BMP de fotos são enormes.

3. Dentro de outros CODECs (como o JPEG!)

E aqui vai uma informação que vai conectar este artigo com o futuro da nossa jornada: o RLE é usado dentro do JPEG!

Calma, não diretamente nos pixels. Depois que o JPEG aplica a DCT e a quantização (que veremos na Fase 3), a maioria dos coeficientes resultantes são zeros.

Muitos zeros consecutivos. O JPEG usa uma variação do RLE pra codificar essas sequências de zeros antes de passar pro Huffman.

Ou seja, o RLE que estamos aprendendo aqui não é só um exercício didático.

Ele vai ser efetivamente usado no nosso CODEC quando chegarmos na etapa de serialização dos coeficientes DCT (artigo 11) 😍

4. Telas de loading e splash screens de jogos antigos

Nos jogos de SNES e Mega Drive, as telas de título e loading eram frequentemente imagens com grandes áreas de cor uniforme (fundo preto, logo colorido).

Essas imagens eram comprimidas com RLE pra caber nos cartuchos de memória limitada. O decompressor era tão simples que rodava em poucos ciclos de CPU, o que para aquela época era o ideal para o hardware fraco.

Implementando o RLE em C

Agora vamos ao que interessa: implementar o RLE do zero em C.

Para isso, nós vamos criar um encoder (que comprime) e um decoder (que descomprime), e em seguida testar com dados reais.

Para isso, crie uma nova pasta chamada de rle-c, e lá dentro, crie por enquanto, 3 arquivos em branco:

  • encoder.c
  • decoder.c
  • main.c

O Encoder (Compressor):

O encoder que vamos construir agora, percorre os dados de entrada, identifica sequências de bytes iguais consecutivos (runs), e escreve pares (contagem, valor) na saída.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// ============================================================
// RLE ENCODER — Comprime dados usando Run-Length Encoding
// ============================================================
//
// Recebe:
//   - input[]     → array de bytes originais
//   - input_size  → quantidade de bytes na entrada
//   - output[]    → array onde os dados comprimidos serão escritos
//                    (deve ter espaço suficiente: no pior caso, 2× input_size)
//
// Retorna:
//   - O tamanho dos dados comprimidos (em bytes)
//
// Formato de saída:
//   Cada "run" é codificado como 2 bytes: [contagem] [valor]
//   A contagem máxima por run é 255 (porque cabe em 1 byte).
//   Se um valor se repete mais de 255 vezes, quebramos em múltiplos runs.

int rle_encode(unsigned char *input, int input_size, unsigned char *output) {
    int out_pos = 0;  // posição atual no array de saída
    int i = 0;        // posição atual no array de entrada

    while (i < input_size) {
        // Pega o valor atual
        unsigned char valor = input[i];

        // Conta quantas vezes esse valor se repete consecutivamente
        int contagem = 1;

        // Avança enquanto o próximo byte for igual ao atual
        // E enquanto a contagem não estourar 255 (máximo que cabe em 1 byte)
        while (i + contagem < input_size
               && input[i + contagem] == valor
               && contagem < 255) {
            contagem++;
        }

        // Escreve o par (contagem, valor) na saída
        output[out_pos]     = (unsigned char) contagem;
        output[out_pos + 1] = valor;
        out_pos += 2;

        // Avança a posição de leitura pelo tamanho da run que acabamos de processar
        i += contagem;
    }

    return out_pos;  // retorna o tamanho total dos dados comprimidos
}

O Decoder (Descompressor):

O decoder faz o caminho inverso: lê pares (contagem, valor) e expande de volta pra sequência original.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// ============================================================
// PROTÓTIPOS DAS FUNÇÕES
// ============================================================
//
// Como o encoder está no arquivo encoder.c e o decoder está no
// arquivo decoder.c, o main.c precisa saber que essas funções existem.
//
// Essas duas linhas abaixo são chamadas de "protótipos de função".
// Elas dizem ao compilador:
//   - existe uma função chamada rle_encode
//   - existe uma função chamada rle_decode
//
// A implementação real dessas funções está em outros arquivos.

int rle_encode(unsigned char *input, int input_size, unsigned char *output);
int rle_decode(unsigned char *input, int input_size, unsigned char *output);

// ============================================================
// MAIN — Testa o encoder e o decoder com diferentes cenários
// ============================================================

int main() {
    printf("=== RLE (Run-Length Encoding) ===\n\n");

    // ----- Teste 1: Melhor caso (muita repetição) -----
    printf("--- Teste 1: Melhor caso (muita repetição) ---\n");

    unsigned char teste1[] = {
        45, 45, 45, 45, 45, 45,             // 6× valor 45
        120, 120, 120,                      // 3× valor 120
        200, 200, 200, 200, 200, 200, 200, 200  // 8× valor 200
    };
    int tamanho1 = sizeof(teste1);

    // Buffer pro resultado comprimido (no pior caso: 2× o tamanho original)
    unsigned char comprimido1[100];
    int tam_comprimido1 = rle_encode(teste1, tamanho1, comprimido1);

    printf("Original:   %d bytes  → ", tamanho1);
    for (int i = 0; i < tamanho1; i++) printf("%d ", teste1[i]);
    printf("\n");

    printf("Comprimido: %d bytes  → ", tam_comprimido1);
    for (int i = 0; i < tam_comprimido1; i += 2)
        printf("[%d×%d] ", comprimido1[i], comprimido1[i + 1]);
    printf("\n");

    printf("Compressão: %.1f%%\n",
           (1.0 - (double)tam_comprimido1 / tamanho1) * 100.0);

    // Descomprime pra verificar se volta ao original
    unsigned char descomprimido1[100];
    int tam_descomprimido1 = rle_decode(comprimido1, tam_comprimido1, descomprimido1);

    // Verifica se é idêntico ao original (lossless!)
    int ok1 = (tam_descomprimido1 == tamanho1)
              && (memcmp(teste1, descomprimido1, tamanho1) == 0);
    printf("Lossless:   %s\n\n", ok1 ? "SIM (idêntico ao original)" : "NÃO (ERRO!)");

    // ----- Teste 2: Pior caso (nenhuma repetição) -----
    printf("--- Teste 2: Pior caso (nenhuma repetição) ---\n");

    unsigned char teste2[] = {10, 20, 30, 40, 50, 60, 70, 80};
    int tamanho2 = sizeof(teste2);

    unsigned char comprimido2[100];
    int tam_comprimido2 = rle_encode(teste2, tamanho2, comprimido2);

    printf("Original:   %d bytes  → ", tamanho2);
    for (int i = 0; i < tamanho2; i++) printf("%d ", teste2[i]);
    printf("\n");

    printf("Comprimido: %d bytes  → ", tam_comprimido2);
    for (int i = 0; i < tam_comprimido2; i += 2)
        printf("[%d×%d] ", comprimido2[i], comprimido2[i + 1]);
    printf("\n");

    printf("Compressão: %.1f%% %s\n",
           (1.0 - (double)tam_comprimido2 / tamanho2) * 100.0,
           tam_comprimido2 > tamanho2 ? "(EXPANDIU!)" : "");

    unsigned char descomprimido2[100];
    int tam_descomprimido2 = rle_decode(comprimido2, tam_comprimido2, descomprimido2);
    int ok2 = (tam_descomprimido2 == tamanho2)
              && (memcmp(teste2, descomprimido2, tamanho2) == 0);
    printf("Lossless:   %s\n\n", ok2 ? "SIM (idêntico ao original)" : "NÃO (ERRO!)");

    // ----- Teste 3: Caso realista (uma "linha de pixels" com áreas uniformes) -----
    printf("--- Teste 3: Caso realista (linha de pixels) ---\n");

    // Simula uma linha de 30 pixels onde:
    // - os primeiros 10 são "céu" (valor 180)
    // - os próximos 5 são "montanha" (valor 80)
    // - os últimos 15 são "grama" (valor 100)
    unsigned char teste3[30];
    for (int i = 0;  i < 10; i++) teste3[i] = 180;   // céu
    for (int i = 10; i < 15; i++) teste3[i] = 80;    // montanha
    for (int i = 15; i < 30; i++) teste3[i] = 100;   // grama
    int tamanho3 = 30;

    unsigned char comprimido3[100];
    int tam_comprimido3 = rle_encode(teste3, tamanho3, comprimido3);

    printf("Original:   %d bytes (10 céu + 5 montanha + 15 grama)\n", tamanho3);
    printf("Comprimido: %d bytes  → ", tam_comprimido3);
    for (int i = 0; i < tam_comprimido3; i += 2)
        printf("[%d×%d] ", comprimido3[i], comprimido3[i + 1]);
    printf("\n");

    printf("Compressão: %.1f%%\n",
           (1.0 - (double)tam_comprimido3 / tamanho3) * 100.0);

    unsigned char descomprimido3[100];
    int tam_descomprimido3 = rle_decode(comprimido3, tam_comprimido3, descomprimido3);
    int ok3 = (tam_descomprimido3 == tamanho3)
              && (memcmp(teste3, descomprimido3, tamanho3) == 0);
    printf("Lossless:   %s\n\n", ok3 ? "SIM (idêntico ao original)" : "NÃO (ERRO!)");

    // ----- Teste 4: Caso extremo (255+ repetições) -----
    printf("--- Teste 4: Caso extremo (300 repetições) ---\n");

    unsigned char teste4[300];
    memset(teste4, 77, 300);  // 300 bytes com valor 77
    int tamanho4 = 300;

    unsigned char comprimido4[100];
    int tam_comprimido4 = rle_encode(teste4, tamanho4, comprimido4);

    printf("Original:   %d bytes (tudo valor 77)\n", tamanho4);
    printf("Comprimido: %d bytes  → ", tam_comprimido4);
    for (int i = 0; i < tam_comprimido4; i += 2)
        printf("[%d×%d] ", comprimido4[i], comprimido4[i + 1]);
    printf("\n");

    printf("Compressão: %.1f%%\n",
           (1.0 - (double)tam_comprimido4 / tamanho4) * 100.0);

    unsigned char descomprimido4[600];
    int tam_descomprimido4 = rle_decode(comprimido4, tam_comprimido4, descomprimido4);
    int ok4 = (tam_descomprimido4 == tamanho4)
              && (memcmp(teste4, descomprimido4, tamanho4) == 0);
    printf("Lossless:   %s\n", ok4 ? "SIM (idêntico ao original)" : "NÃO (ERRO!)");

    return 0;
}

O programa principal (testando tudo):

// ============================================================
// MAIN — Testa o encoder e o decoder com diferentes cenários
// ============================================================

int main() {
    printf("=== RLE (Run-Length Encoding) ===\n\n");

    // ----- Teste 1: Melhor caso (muita repetição) -----
    printf("--- Teste 1: Melhor caso (muita repetição) ---\n");

    unsigned char teste1[] = {
        45, 45, 45, 45, 45, 45,       // 6× valor 45
        120, 120, 120,                  // 3× valor 120
        200, 200, 200, 200, 200, 200, 200, 200  // 8× valor 200
    };
    int tamanho1 = sizeof(teste1);

    // Buffer pro resultado comprimido (no pior caso: 2× o tamanho original)
    unsigned char comprimido1[100];
    int tam_comprimido1 = rle_encode(teste1, tamanho1, comprimido1);

    printf("Original:   %d bytes  → ", tamanho1);
    for (int i = 0; i < tamanho1; i++) printf("%d ", teste1[i]);
    printf("\n");

    printf("Comprimido: %d bytes  → ", tam_comprimido1);
    for (int i = 0; i < tam_comprimido1; i += 2)
        printf("[%d×%d] ", comprimido1[i], comprimido1[i+1]);
    printf("\n");

    printf("Compressão: %.1f%%\n",
           (1.0 - (double)tam_comprimido1 / tamanho1) * 100.0);

    // Descomprime pra verificar se volta ao original
    unsigned char descomprimido1[100];
    int tam_descomprimido1 = rle_decode(comprimido1, tam_comprimido1, descomprimido1);

    // Verifica se é idêntico ao original (lossless!)
    int ok1 = (tam_descomprimido1 == tamanho1)
              && (memcmp(teste1, descomprimido1, tamanho1) == 0);
    printf("Lossless:   %s\n\n", ok1 ? "SIM (idêntico ao original)" : "NÃO (ERRO!)");

    // ----- Teste 2: Pior caso (nenhuma repetição) -----
    printf("--- Teste 2: Pior caso (nenhuma repetição) ---\n");

    unsigned char teste2[] = {10, 20, 30, 40, 50, 60, 70, 80};
    int tamanho2 = sizeof(teste2);

    unsigned char comprimido2[100];
    int tam_comprimido2 = rle_encode(teste2, tamanho2, comprimido2);

    printf("Original:   %d bytes  → ", tamanho2);
    for (int i = 0; i < tamanho2; i++) printf("%d ", teste2[i]);
    printf("\n");

    printf("Comprimido: %d bytes  → ", tam_comprimido2);
    for (int i = 0; i < tam_comprimido2; i += 2)
        printf("[%d×%d] ", comprimido2[i], comprimido2[i+1]);
    printf("\n");

    printf("Compressão: %.1f%% %s\n",
           (1.0 - (double)tam_comprimido2 / tamanho2) * 100.0,
           tam_comprimido2 > tamanho2 ? "(EXPANDIU!)" : "");

    unsigned char descomprimido2[100];
    int tam_descomprimido2 = rle_decode(comprimido2, tam_comprimido2, descomprimido2);
    int ok2 = (tam_descomprimido2 == tamanho2)
              && (memcmp(teste2, descomprimido2, tamanho2) == 0);
    printf("Lossless:   %s\n\n", ok2 ? "SIM (idêntico ao original)" : "NÃO (ERRO!)");

    // ----- Teste 3: Caso realista (uma "linha de pixels" com áreas uniformes) -----
    printf("--- Teste 3: Caso realista (linha de pixels) ---\n");

    // Simula uma linha de 30 pixels onde:
    // - os primeiros 10 são "céu" (valor 180)
    // - os próximos 5 são "montanha" (valor 80)
    // - os últimos 15 são "grama" (valor 100)
    unsigned char teste3[30];
    for (int i = 0;  i < 10; i++) teste3[i] = 180;   // céu
    for (int i = 10; i < 15; i++) teste3[i] = 80;    // montanha
    for (int i = 15; i < 30; i++) teste3[i] = 100;   // grama
    int tamanho3 = 30;

    unsigned char comprimido3[100];
    int tam_comprimido3 = rle_encode(teste3, tamanho3, comprimido3);

    printf("Original:   %d bytes (10 céu + 5 montanha + 15 grama)\n", tamanho3);
    printf("Comprimido: %d bytes  → ", tam_comprimido3);
    for (int i = 0; i < tam_comprimido3; i += 2)
        printf("[%d×%d] ", comprimido3[i], comprimido3[i+1]);
    printf("\n");

    printf("Compressão: %.1f%%\n",
           (1.0 - (double)tam_comprimido3 / tamanho3) * 100.0);

    unsigned char descomprimido3[100];
    int tam_descomprimido3 = rle_decode(comprimido3, tam_comprimido3, descomprimido3);
    int ok3 = (tam_descomprimido3 == tamanho3)
              && (memcmp(teste3, descomprimido3, tamanho3) == 0);
    printf("Lossless:   %s\n\n", ok3 ? "SIM (idêntico ao original)" : "NÃO (ERRO!)");

    // ----- Teste 4: Caso extremo (255+ repetições) -----
    printf("--- Teste 4: Caso extremo (300 repetições) ---\n");

    unsigned char teste4[300];
    memset(teste4, 77, 300);  // 300 bytes com valor 77
    int tamanho4 = 300;

    unsigned char comprimido4[100];
    int tam_comprimido4 = rle_encode(teste4, tamanho4, comprimido4);

    printf("Original:   %d bytes (tudo valor 77)\n", tamanho4);
    printf("Comprimido: %d bytes  → ", tam_comprimido4);
    for (int i = 0; i < tam_comprimido4; i += 2)
        printf("[%d×%d] ", comprimido4[i], comprimido4[i+1]);
    printf("\n");

    printf("Compressão: %.1f%%\n",
           (1.0 - (double)tam_comprimido4 / tamanho4) * 100.0);

    unsigned char descomprimido4[600];
    int tam_descomprimido4 = rle_decode(comprimido4, tam_comprimido4, descomprimido4);
    int ok4 = (tam_descomprimido4 == tamanho4)
              && (memcmp(teste4, descomprimido4, tamanho4) == 0);
    printf("Lossless:   %s\n", ok4 ? "SIM (idêntico ao original)" : "NÃO (ERRO!)");

    return 0;
}

Pra compilar e rodar esse app, basta fazer isso no seu terminal:

cd ~/rle-c
gcc main.c encoder.c decoder.c -o rle
./rle

A saída esperada será:

=== RLE (Run-Length Encoding) ===

--- Teste 1: Melhor caso (muita repetição) ---
Original:   17 bytes  → 45 45 45 45 45 45 120 120 120 200 200 200 200 200 200 200 200
Comprimido: 6 bytes   → [6×45] [3×120] [8×200]
Compressão: 64.7%
Lossless:   SIM (idêntico ao original)

--- Teste 2: Pior caso (nenhuma repetição) ---
Original:   8 bytes   → 10 20 30 40 50 60 70 80
Comprimido: 16 bytes  → [1×10] [1×20] [1×30] [1×40] [1×50] [1×60] [1×70] [1×80]
Compressão: -100.0% (EXPANDIU!)
Lossless:   SIM (idêntico ao original)

--- Teste 3: Caso realista (linha de pixels) ---
Original:   30 bytes (10 céu + 5 montanha + 15 grama)
Comprimido: 6 bytes   → [10×180] [5×80] [15×100]
Compressão: 80.0%
Lossless:   SIM (idêntico ao original)

--- Teste 4: Caso extremo (300 repetições) ---
Original:   300 bytes (tudo valor 77)
Comprimido: 4 bytes   → [255×77] [45×77]
Compressão: 98.7%
Lossless:   SIM (idêntico ao original)

Repare no Teste 4: como a contagem máxima por run é 255 (limite de 1 byte), as 300 repetições são divididas em dois runs: [255×77] e [45×77]. O encoder faz isso automaticamente graças ao && contagem < 255 no loop.

Aplicando RLE numa imagem real

Agora vamos fazer algo mais interessante: aplicar o RLE numa imagem RGB24 real e ver como ele se sai.

Vou te adiantar: ele vai se sair mal em fotos, porque fotos têm muita variação pixel a pixel. Mas vai se sair bem em imagens sintéticas com áreas uniformes.,

Mas antes, tem um detalhe importante que preciso te explicar.

Por que separar os canais R, G e B antes de aplicar o RLE?

Lembra que no formato RGB24, os bytes são armazenados entrelaçados: R, G, B, R, G, B, R, G, B...

Se você aplicar o RLE direto nessa sequência entrelaçada, ele vai encontrar pouquíssimas repetições, mesmo numa imagem de cor sólida! Mas Por quê?

Imagine uma linha de 3 pixels vermelhos puros (255, 0, 0):

Entrelaçado: 255 0 0 255 0 0 255 0 0

O RLE olha pra isso e vê: 255 aparece 1 vez, depois 0 aparece 2 vezes, depois 255 aparece 1 vez, depois 0 aparece 2 vezes... Os canais se alternam e quebram as runs!

Agora, se você separar os canais primeiro, da seguinte forma:

Canal R: 255 255 255     → RLE: [3, 255]
Canal G: 0   0   0       → RLE: [3, 0]
Canal B: 0   0   0       → RLE: [3, 0]

Temos agora, um conjunto de Runs enormes e perfeitas! Cada canal vira uma sequência de valores do mesmo tipo, e as repetições ficam evidentes.

É por isso que na prática, os CODECs reais sempre separam os canais antes de aplicar qualquer compressão. E é exatamente isso que vamos fazer aqui.

Pra esse teste, vamos criar duas imagens de 100×100 pixels:

  • Imagem 1: bandeira com 3 faixas horizontais de cor sólida (ótima pro RLE)
  • Imagem 2: degradê suave (péssima pro RLE)

Segue abaixo o código do seu main.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// ============================================================
// RLE ENCODER — Comprime dados usando Run-Length Encoding
// ============================================================
//
// Recebe:
//   - input[]     → array de bytes originais
//   - input_size  → quantidade de bytes na entrada
//   - output[]    → array onde os dados comprimidos serão escritos
//                    (deve ter espaço suficiente: no pior caso, 2× input_size)
//
// Retorna:
//   - O tamanho dos dados comprimidos (em bytes)
//
// Formato de saída:
//   Cada "run" é codificado como 2 bytes: [contagem] [valor]
//   A contagem máxima por run é 255 (porque cabe em 1 byte).
//   Se um valor se repete mais de 255 vezes, quebramos em múltiplos runs.

int rle_encode(unsigned char *input, int input_size, unsigned char *output) {
    int out_pos = 0;  // posição atual no array de saída
    int i = 0;        // posição atual no array de entrada

    while (i < input_size) {
        // Pega o valor atual
        unsigned char valor = input[i];

        // Conta quantas vezes esse valor se repete consecutivamente
        int contagem = 1;

        // Avança enquanto o próximo byte for igual ao atual
        // E enquanto a contagem não estourar 255 (máximo que cabe em 1 byte)
        while (i + contagem < input_size
               && input[i + contagem] == valor
               && contagem < 255) {
            contagem++;
        }

        // Escreve o par (contagem, valor) na saída
        output[out_pos]     = (unsigned char) contagem;
        output[out_pos + 1] = valor;
        out_pos += 2;

        // Avança a posição de leitura pelo tamanho da run que acabamos de processar
        i += contagem;
    }

    return out_pos;  // retorna o tamanho total dos dados comprimidos
}

// ============================================================
// RLE DECODER — Descomprime dados codificados com RLE
// ============================================================
//
// Recebe:
//   - input[]     → array de bytes comprimidos (pares contagem+valor)
//   - input_size  → tamanho dos dados comprimidos
//   - output[]    → array onde os dados descomprimidos serão escritos
//
// Retorna:
//   - O tamanho dos dados descomprimidos (em bytes)

int rle_decode(unsigned char *input, int input_size, unsigned char *output) {
    int out_pos = 0;  // posição atual no array de saída
    int i = 0;        // posição atual no array de entrada

    // Percorre os dados comprimidos de 2 em 2 bytes (cada par = 1 run)
    while (i < input_size) {
        unsigned char contagem = input[i];      // primeiro byte: quantas vezes
        unsigned char valor    = input[i + 1];   // segundo byte: qual valor

        // Escreve o valor "contagem" vezes na saída
        for (int j = 0; j < contagem; j++) {
            output[out_pos] = valor;
            out_pos++;
        }

        i += 2;  // avança pro próximo par
    }

    return out_pos;  // retorna o tamanho total dos dados descomprimidos
}

// ============================================================
// MAIN — Testa o RLE em duas imagens RGB24
// ============================================================

int main() {
    int width = 100, height = 100;
    int total_pixels = width * height;       // 10.000 pixels
    int total_bytes  = total_pixels * 3;     // 30.000 bytes (RGB24)

    // ===== IMAGEM 1: Bandeira com faixas sólidas =====

    unsigned char *bandeira = (unsigned char *) malloc(total_bytes);

    for (int j = 0; j < height; j++) {
        for (int i = 0; i < width; i++) {
            int idx = (j * width + i) * 3;

            if (j < 33) {
                // Faixa superior: verde (0, 180, 50)
                bandeira[idx]   = 0;
                bandeira[idx+1] = 180;
                bandeira[idx+2] = 50;
            } else if (j < 66) {
                // Faixa do meio: amarelo (255, 220, 0)
                bandeira[idx]   = 255;
                bandeira[idx+1] = 220;
                bandeira[idx+2] = 0;
            } else {
                // Faixa inferior: azul (0, 50, 200)
                bandeira[idx]   = 0;
                bandeira[idx+1] = 50;
                bandeira[idx+2] = 200;
            }
        }
    }

    // Separa os canais R, G, B da bandeira
    // (pra evitar o problema do entrelaçamento)
    unsigned char *r1 = (unsigned char *) malloc(total_pixels);
    unsigned char *g1 = (unsigned char *) malloc(total_pixels);
    unsigned char *b1 = (unsigned char *) malloc(total_pixels);

    for (int i = 0; i < total_pixels; i++) {
        r1[i] = bandeira[i * 3];       // canal vermelho
        g1[i] = bandeira[i * 3 + 1];   // canal verde
        b1[i] = bandeira[i * 3 + 2];   // canal azul
    }

    // Comprime cada canal separadamente com RLE
    unsigned char *comp_r1 = (unsigned char *) malloc(total_pixels * 2);
    unsigned char *comp_g1 = (unsigned char *) malloc(total_pixels * 2);
    unsigned char *comp_b1 = (unsigned char *) malloc(total_pixels * 2);

    int tam_r1 = rle_encode(r1, total_pixels, comp_r1);
    int tam_g1 = rle_encode(g1, total_pixels, comp_g1);
    int tam_b1 = rle_encode(b1, total_pixels, comp_b1);
    int tam_comp1 = tam_r1 + tam_g1 + tam_b1;  // soma dos 3 canais comprimidos

    // Salva a imagem original pra visualização
    FILE *f = fopen("bandeira.rgb", "wb");
    fwrite(bandeira, 1, total_bytes, f);
    fclose(f);

    // ===== IMAGEM 2: Degradê (variação contínua) =====

    unsigned char *degrade = (unsigned char *) malloc(total_bytes);

    for (int j = 0; j < height; j++) {
        for (int i = 0; i < width; i++) {
            int idx = (j * width + i) * 3;
            degrade[idx]   = (unsigned char)((i * 255) / (width - 1));   // R varia
            degrade[idx+1] = (unsigned char)((j * 255) / (height - 1));  // G varia
            degrade[idx+2] = 128;                                         // B fixo
        }
    }

    // Separa os canais R, G, B do degradê
    unsigned char *r2 = (unsigned char *) malloc(total_pixels);
    unsigned char *g2 = (unsigned char *) malloc(total_pixels);
    unsigned char *b2 = (unsigned char *) malloc(total_pixels);

    for (int i = 0; i < total_pixels; i++) {
        r2[i] = degrade[i * 3];
        g2[i] = degrade[i * 3 + 1];
        b2[i] = degrade[i * 3 + 2];
    }

    // Comprime cada canal separadamente com RLE
    unsigned char *comp_r2 = (unsigned char *) malloc(total_pixels * 2);
    unsigned char *comp_g2 = (unsigned char *) malloc(total_pixels * 2);
    unsigned char *comp_b2 = (unsigned char *) malloc(total_pixels * 2);

    int tam_r2 = rle_encode(r2, total_pixels, comp_r2);
    int tam_g2 = rle_encode(g2, total_pixels, comp_g2);
    int tam_b2 = rle_encode(b2, total_pixels, comp_b2);
    int tam_comp2 = tam_r2 + tam_g2 + tam_b2;

    // Salva a imagem original pra visualização
    f = fopen("degrade.rgb", "wb");
    fwrite(degrade, 1, total_bytes, f);
    fclose(f);

    // ===== RESULTADOS =====

    printf("=== RLE em Imagens RGB24 (canais separados) ===\n\n");

    printf("--- Imagem 1: Bandeira (faixas sólidas) ---\n");
    printf("Original:   %d bytes (%.1f KB)\n", total_bytes, total_bytes / 1024.0);
    printf("Comprimido: %d bytes (%.2f KB)\n", tam_comp1, tam_comp1 / 1024.0);
    printf("  Canal R: %d bytes\n", tam_r1);
    printf("  Canal G: %d bytes\n", tam_g1);
    printf("  Canal B: %d bytes\n", tam_b1);
    printf("Compressão: %.1f%%\n\n",
           (1.0 - (double)tam_comp1 / total_bytes) * 100.0);

    printf("--- Imagem 2: Degradê (variação contínua) ---\n");
    printf("Original:   %d bytes (%.1f KB)\n", total_bytes, total_bytes / 1024.0);
    printf("Comprimido: %d bytes (%.1f KB)\n", tam_comp2, tam_comp2 / 1024.0);
    printf("  Canal R: %d bytes\n", tam_r2);
    printf("  Canal G: %d bytes\n", tam_g2);
    printf("  Canal B: %d bytes\n", tam_b2);
    printf("Compressão: %.1f%%\n\n",
           (1.0 - (double)tam_comp2 / total_bytes) * 100.0);

    printf("Para visualizar as imagens:\n");
    printf("  ffplay -f rawvideo -pixel_format rgb24 -video_size 100x100 bandeira.rgb\n");
    printf("  ffplay -f rawvideo -pixel_format rgb24 -video_size 100x100 degrade.rgb\n");

    // Verificação lossless: descomprime e compara com o original
    unsigned char *dec_r1 = (unsigned char *) malloc(total_pixels);
    unsigned char *dec_g1 = (unsigned char *) malloc(total_pixels);
    unsigned char *dec_b1 = (unsigned char *) malloc(total_pixels);

    rle_decode(comp_r1, tam_r1, dec_r1);
    rle_decode(comp_g1, tam_g1, dec_g1);
    rle_decode(comp_b1, tam_b1, dec_b1);

    int ok = (memcmp(r1, dec_r1, total_pixels) == 0)
          && (memcmp(g1, dec_g1, total_pixels) == 0)
          && (memcmp(b1, dec_b1, total_pixels) == 0);

    printf("\nVerificação lossless (bandeira): %s\n",
           ok ? "SIM (idêntico ao original)" : "NÃO (ERRO!)");

    // Limpeza
    free(bandeira); free(degrade);
    free(r1); free(g1); free(b1);
    free(r2); free(g2); free(b2);
    free(comp_r1); free(comp_g1); free(comp_b1);
    free(comp_r2); free(comp_g2); free(comp_b2);
    free(dec_r1); free(dec_g1); free(dec_b1);

    return 0;
}

Pra compilar e rodar:

mkdir -p ~/rle-imagens-c
cd ~/rle-imagens-c
# Crie o arquivo main.c com o código acima
gcc main.c -o rle_imagens
./rle_imagens

A saída esperada será algo assim:

=== RLE em Imagens RGB24 (canais separados) ===

--- Imagem 1: Bandeira (faixas sólidas) ---
Original:   30000 bytes (29.3 KB)
Comprimido: 240 bytes (0.23 KB)
  Canal R: 80 bytes
  Canal G: 80 bytes
  Canal B: 80 bytes
Compressão: 99.2%

--- Imagem 2: Degradê (variação contínua) ---
Original:   30000 bytes (29.3 KB)
Comprimido: 20280 bytes (19.8 KB)
  Canal R: 10100 bytes
  Canal G: 10100 bytes
  Canal B: 80 bytes
Compressão: 32.4%

Verificação lossless (bandeira): SIM (idêntico ao original)

Percebe a diferença brutal?

A bandeira (3 faixas de cor sólida) comprimiu de ~30.000 bytes pra apenas ~240 bytes!

Ou seja, incríveis 99.2% de compressão!

Isso porque cada faixa inteira é um único run gigante de bytes repetidos. Repare também que os 3 canais (R, G, B) comprimiram de forma igual (80 bytes cada), porque a imagem é simétrica em termos de repetição.

O degradê (cada pixel é ligeiramente diferente do vizinho) conseguiu apenas 32.4% de compressão. Os canais R e G quase não comprimiram (10.100 bytes cada pra 10.000 originais), porque os valores variam a cada pixel. Só o canal B se salvou (80 bytes), já que ele é fixo em 128 pra toda a imagem.

E tem um detalhe interessante no degradê: repare que o canal B comprimiu exatamente igual ao da bandeira (80 bytes).

Faz sentido! Em ambas as imagens, o canal B tem runs longas de valores iguais, em uma por ter faixas sólidas, e na outra porque fixamos B = 128 pra todos os pixels.

Observação importante: No caso de imagens reais (fotos), a diferença é ainda mais difícil de perceber, porque fotos naturais têm transições de cor suaves. Onde você vai notar diferença é em bordas nítidas entre cores saturadas diferentes, como texto vermelho sobre fundo branco, por exemplo.

Por que o RLE sozinho não serve pro nosso CODEC?

Agora que você viu o RLE em ação, talvez esteja pensando: "Se o RLE é tão ruim pra fotos, por que estamos aprendendo ele?"

A resposta tem duas partes distintas!

Parte 1 — Voce precisa entender esse princípio

O RLE é o algoritmo de compressão mais simples que existe. Ele te ensina os conceitos fundamentais que todo algoritmo de compressão usa:

  • Identificar padrões (runs de valores iguais)
  • Substituir por uma representação menor (par contagem+valor)
  • Garantir que o processo é reversível (lossless)

Esses conceitos aparecem em todos os outros algoritmos, só que de formas mais sofisticadas.

Parte 2 — Ele é usado dentro do JPEG

Como mencionei antes, o JPEG usa uma variação do RLE pra codificar sequências de zeros nos coeficientes DCT quantizados. Depois da quantização, a maioria dos coeficientes de alta frequência vira zero.

Quando o JPEG faz o Zig-Zag Scan (artigo 11), ele encontra longas sequências de zeros consecutivos que são perfeitamente comprimíveis com RLE.

Isso significa que o RLE não será o compressor principal do nosso CODEC, mas sim, uma peça super importante dentro de um pipeline bem maior.

É como aprender a cortar ingredientes antes de aprender a cozinhar. Cortar não é o prato final, mas sem saber cortar, você não faz nada 😅

Variações do RLE que existem por aí

Antes de encerrar, quero te apresentar algumas variações do RLE que foram criadas pra resolver o problema do "pior caso" (quando não tem repetição).

Variação 1 — RLE com flag de escape

Em vez de sempre usar pares (contagem, valor), você usa um byte especial (flag) pra indicar que uma run vem a seguir. Bytes que não são precedidos pelo flag são armazenados "crus" (sem overhead).

Por exemplo, podemos usar o 0xFF como flag da seguinte forma:

Dados:     10 20 30 40 40 40 40 40 50 60
RLE flag:  10 20 30 0xFF 5 40 50 60

Os valores 10, 20, 30, 50 e 60 (que não se repetem) são armazenados direto, sem overhead. Só quando aparece uma repetição (40×5), o flag 0xFF indica que um par (contagem, valor) vem a seguir.

Isso resolve o problema do pior caso: dados sem repetição ocupam praticamente o mesmo espaço que o original (talvez 1-2 bytes a mais pro flag ocasional, em vez de dobrar o tamanho).

A desvantagem é que se o valor 0xFF aparecer naturalmente nos dados, precisa de um tratamento especial (escapar o escape, tipo 0xFF 0xFF pra representar um 0xFF literal).

Variação 2 — PackBits (usado no TIFF e no formato Macintosh)

O PackBits usa um byte de controle que pode ser positivo ou negativo:

  • Controle positivo (1 a 127): os próximos N+1 bytes são dados literais (sem repetição)
  • Controle negativo (-1 a -127): o próximo byte se repete |N|+1 vezes

Vejamos um pequeno exemplo:

Dados:      10 20 30 40 40 40 40 40
PackBits:   [2] 10 20 30 [-4] 40

O [2] diz "os próximos 3 bytes são literais". O [-4] diz "o próximo byte se repete 5 vezes".

Isso combina o melhor dos dois mundos: dados variados são armazenados sem overhead, e runs são comprimidos normalmente.

Variação 3 — RLE nos coeficientes DCT (usado no JPEG)

No JPEG, o RLE é aplicado especificamente na sequência de coeficientes AC depois do Zig-Zag Scan. O formato é: (número_de_zeros_antes, tamanho_do_próximo_coeficiente).

Isso codifica eficientemente as longas cadeias de zeros que aparecem depois da quantização.

Vamos ver isso em detalhes no artigo 11 (Zig-Zag Scan), mas fica o spoiler de que o RLE que aprendemos aqui é a base 😛

Conectando com a entropia

Pra fechar com chave de ouro, vamos conectar o RLE com o conceito de entropia que aprendemos no artigo anterior.

Lembra que a entropia mede o "conteúdo de informação" dos dados? E que dados com entropia baixa comprimem bem?

Pois então, o RLE é um algoritmo que explora especificamente um tipo de padrão: repetições consecutivas. Ele é eficiente quando esse padrão domina os dados (entropia baixa pra esse tipo de padrão), e ineficiente quando esse padrão não existe.

O Huffman (próximo artigo) explora outro tipo de padrão: frequências desiguais de símbolos. Ele é eficiente quando certos valores aparecem muito mais que outros.

O LZ77 (artigo 7) explora ainda outro tipo: sequências que se repetem em posições diferentes (não necessariamente consecutivas).

Cada algoritmo de compressão é como uma ferramenta especializada em identificar e explorar um tipo específico de redundância.

E os CODECs reais combinam várias dessas ferramentas em sequência pra espremer o máximo de cada tipo de redundância que existe nos dados.

No nosso futuro CODEC, o pipeline de implementação de compressão vai ser algo como isso:

  • Chroma Subsampling → remove redundância visual (lossy)
  • DCT → reorganiza dados pra concentrar energia
  • Quantização → zera valores irrelevantes, cria muitos zeros (lossy)
  • RLE → comprime as sequências de zeros consecutivos (lossless)
  • Huffman → comprime a distribuição desigual resultante (lossless)

Cada ferramenta faz o que sabe fazer de melhor, e o resultado final é muito mais poderoso do que qualquer uma delas sozinha.

Resumo: sua primeira ferramenta de compressão

Vamos recapitular tudo o que aprendemos neste artigo:

  • RLE (Run-Length Encoding) substitui sequências de valores iguais consecutivos por pares (contagem, valor).
  • É uma compressão lossless: os dados originais podem ser reconstruídos perfeitamente.
  • Funciona muito bem quando os dados têm muitas repetições consecutivas (imagens com áreas uniformes, documentos de fax, sequências de zeros).
  • Funciona muito mal (e pode até expandir os dados) quando não há repetições consecutivas (fotos reais, dados variados).
  • O RLE é usado em formatos reais como BMP, PCX, fax, e como parte interna do JPEG (pra codificar zeros nos coeficientes DCT).
  • Existem variações (flag de escape, PackBits, RLE em coeficientes DCT) que minimizam o problema do pior caso.
  • No nosso CODEC, o RLE será uma peça do pipeline maior, trabalhando depois da quantização pra comprimir sequências de zeros.

No próximo artigo, vamos aprender a segunda técnica de compressão: Codificação de Huffman, uma técnica muito mais poderosa que o RLE, que explora a redundância estatística (frequências desiguais dos valores) pra criar códigos de tamanho variável.

Se o RLE foi o "cortar ingredientes", o Huffman é o "cozinhar de verdade". Prepare-se, porque vai ser denso, mas muito recompensador 🚀

Criadores de Conteúdo

Foto do William Lima
William Lima
Fundador da Micilini

Inventor nato, escreve conteudos de programação para o portal da micilini.

Torne-se um MIC 🤖

Mais de 100 mic's já estão conectados na plataforma.