Nasce o N.148i: Criando o nosso primeiro CODEC de verdade

Nasce o N.148i: Criando o nosso primeiro CODEC de verdade

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

Este é o décimo quarto artigo da nossa série, e ele marca o começo da Fase 4 🎉

Até o momento, como fizemos em artigos anteriores, a gente trabalhou com pixels, matrizes e coeficientes tudo dentro da memória do computador de forma solta por aí...

A partir de agora, a gente vai colocar a mão na massa de verdade, desenvolvendo o nosso primeiro CODEC de forma estruturada, e começar a criar, salvar e ler arquivos no disco. 💾

Mas antes de mais nada, eu preciso te apresentar formalmente uma coisa que eu venho segurando esse tempo todo. 😄

O nosso codec tem nome, e ele se chama N.148i

Durante toda a série, eu venho chamando ele de "o nosso codec" ou "o nosso CODEC estilo JPEG", envolvendo ele dentro de toda uma aura de mistérios rs

Mas agora, chegou a hora de revelar o nome verdadeiro do sujeito, e ele se chama N.148i. 🎊

O "i" no final vem de imagem, até porque no futuro faremos um outro CODEC voltado para vídeos, e este será chamado de N.148v, onde o "v" no final refere-se a palavra vídeo.

Sendo assim, podemos dizer que o N.148i é o nosso CODEC de imagens estáticas (fotos, ilustrações, prints), e é nele que a série toda está focada.

Já o nome N.148, bem... foi apenas um nome bonitinho que saiu da minha cabeça rs

No nosso caso, todo codec que gera arquivos precisa de uma extensão de arquivo, que nada mais é do que aquele sufixo depois do ponto que você vê no nome, como é o caso dos arquivos do tipo foto.jpg ou documento.pdf.

O nosso codec N.148i, ele vai levar a seguinte extenção:

minha-foto.n148i

Isso mesmo que você viu acima, quando a gente terminar a Fase 4, você vai conseguir pegar uma imagem, comprimi-la com o nosso codec, e salvar em um arquivo cuja extensão será .n148i.

E sim, você verá um arquivo chamado meu-arquivo.n148i salva no seu computador, e ainda conseguirá abrir esse arquivo de volta 🤯

Então, sempre que eu falar "o N.148i" daqui pra frente, é do nosso bebê que eu estou me referindo 👶

Dito isso, vamos dar a vida a ele!

Onde a gente parou (e pra onde vamos)

Se você está comigo desde o começa dessa jornada, vai se lembrar que na fase 3 a gente aprendeu a pegar um bloco de 8×8 pixels e comprimir ele em pouquíssimos bytes, passando por DCT, quantização, zig-zag, RLE e Huffman.

Onde no último artigo, transformamos um bloco em apenas 12 bytes 📉

Só que uma imagem não é um bloco só. Uma foto de 640×480, por exemplo, tem milhares de blocos, espalhados em três canais de cor (Y, Cb e Cr).

E todos esses bytes comprimidos precisam ir pra algum lugar organizado, de um jeito que depois a gente (ou outro programa) consiga ler de volta e remontar a imagem.

Esse "lugar organizado" é o formato de arquivo.

E é exatamente isso que a gente vai construir na fase atual. Neste primeiro artigo, vamos com muita calma pelo comecinho de tudo: como criar e ler um arquivo binário em C, e como montar o cabeçalho (header) do nosso n.148i.  

Mas pode ficar super tranquilo, que a ideia aqui é explicar tudo bem devagarinho, passo a passo, sem pressa.

Se você nunca mexeu com arquivos em C, não tem problema nenhum, pois a ideia é que ninguém fique pra trás.

Sendo assim, pega o café ☕ e vem comigo.

Afinal, o que é um arquivo?

Vamos começar do zero absoluto, porque isso aqui é importante.

Imagina uma fita bem comprida, dividida em quadradinhos numerados, um do lado do outro: quadradinho 0, quadradinho 1, quadradinho 2, e assim por diante.

Onde em cada quadradinho, cabe apenas um único número, que pode ser desde o número 0 até o número 255.

O que acabamos de imaginar agora, pode ser considerado um arquivo!

No fundo, todo arquivo do mundo, seja ele uma foto, uma música, um vídeo, um documento do Word etc. É só uma fita comprida de quadradinhos, cada um guardando um número que vai de 0 á 255.

Cada um desses quadradinhos (cada número de 0 a 255) tem um nome técnico e no mundo da tecnologia a gente chama ele byte.

  • Um arquivo de 16 bytes = uma fita com 16 quadradinhos.
  • Um arquivo de 1 milhão de bytes (1 MB) = uma fita com 1 milhão de quadradinhos.

A diferença entre um .jpg, um .mp3 e um .n148i não está nos quadradinhos, pois todos usam os mesmos números de 0 a 255.   

A diferença está na ordem e no significado que a gente dá pra esses números. É como se cada formato tivesse uma "receita" de como ler a fita. 📖

E o nosso trabalho nesta fase é justamente inventar a receita do N.148i.

Texto x Binário: os dois jeitos de ler a mesma fita

Você já deve ter aberto um arquivo .txt no bloco de notas e visto letras. E você também já deve ter tentado abrir uma foto no mesmo bloco de notas e visto um monte de símbolos malucos, tipo ؉PNG♥.

Por que isso acontece? 🤨

Porque existem dois "jeitos de encarar" a fita de bytes:

  • Arquivo de texto: cada número da fita representa uma letra (usando uma tabela chamada ASCII, onde 65 = 'A', 66 = 'B', e por aí vai). São feitos pra humanos lerem. 👀
  • Arquivo binário: os números da fita representam qualquer coisa, tipo pixels, cores, coeficientes, o que a gente quiser. Não são feitos pra ler como texto, e sim para um programa interpretar. 🤖

Mas dai você pode estar se perguntando: Porque um binário e não um texto?

Porque os nossos dados já são números, não letras. Pixel é 0–255, coeficiente é número, o bitstream comprimido é uma sequência de bytes.

E guardar um número como texto é um desperdício puro, que eu não quero que você faça...

Imagina guardar o valor de pixel 200 nesses dois casos abaixo:

  • Binário: o número 200 cabe num único byte. → 1 byte.
  • Texto: você escreveria os caracteres '2', '0', '0' (cada um é 1 byte em ASCII), mais um separador pra saber onde termina. → 4 bytes.

Ou seja, texto gastaria até 4x mais só pra guardar a mesma coisa. Numa imagem com milhões de valores, isso é a diferença entre um arquivo de 500 KB e um de 2 MB.

E o objetivo de codec existe justamente pra diminuir bytes, seria contraditório usar texto e inflar tudo. 🎯

Com isso, surge mais uma outra pergunta: JPEG é texto ou binário? E WebP da Google também é texto ou binário?

Respondendo de forma direta e simples: ambos são binários!

Na real, todo formato de imagem, áudio e vídeo do mundo é binário. E você só vê texto quando tenta abrir um arquivo desses num bloco de notas da vida porque ele SEMPRE mostra texto, mesmo quando o arquivo não é texto.

Mas fique tranquilo, porque no fundo no fundo, é tudo binário 😄

Dito isso, o nosso .n148i obviamente que será um arquivo binário! Os bytes dele vão guardar números que representam a imagem comprimida, não letras.

Por isso, quando a gente for abrir e salvar ele em C, precisamos avisar pro computador: "olha, trata isso como binário, não como texto".

Abrindo e fechando arquivos em C

Em C, mexer com arquivo é sempre a mesma dança de três passos:

  • Abrir o arquivo (com fopen). 🔓
  • Escrever ou ler os bytes. ✍️👁️
  • Fechar o arquivo (com fclose). 🔒

No caso do fopen, ele funciona da seguinte maneira:

FILE *f = fopen("imagem.n148i", "wb");

Aquele "wb" no final é o modo de abertura, e ele diz duas coisas:

A primeira letra é o que você quer fazer, onde W significa Write, que por sua vez diz para escrever um arquivo novo, ou apagar o que já existia. E o R que significa Read, onde dá a possibilidade de lermos um arquivo que já existe. (mas ela não foi declarada no comando acima)

Já a segunda letra, o B, é de binary (binário). Ele avisa: "trate os bytes exatamente como eles são, não mexa em nada".

⚠️ Atenção pra um detalhe chato: se você esquecer o "b" e usar só "w", em alguns sistemas (principalmente Windows) o computador vai "ajudar" trocando alguns bytes sozinho (como o byte de quebra de linha). Pra dados binários, isso estraga o arquivo! Sendo assim, para o nosso N.148i, sempre use "wb" e "rb". Nunca esqueça o b.  

Já o comando fclose, é só uma boa educação de fechar a porta quando terminou:

fclose(f);

Sempre feche seus arquivos! Se você não fechar, corre o risco de o arquivo ficar incompleto ou corrompido. É tipo lavar a louça depois de comer.

Escrevendo e lendo bytes

Pra colocar um byte na fita, a função mais simples é o fputc (file put character):

fputc(65, f);   // escreve o número 65 no arquivo

E pra ler um byte de volta, o fgetc (file get character):

int b = fgetc(f);   // le o proximo byte do arquivo

Também existe o fwrite, que escreve vários bytes de uma vez (útil pra escrever, por exemplo, os 5 bytes da nossa assinatura de uma tacada só):

// fwrite escreve VÁRIOS bytes de uma vez.
// Assinatura: os parametros sao (o que escrever, tamanho de cada item,
// quantos itens, e o arquivo).

char assinatura[5] = {'N', '1', '4', '8', 'I'};
fwrite(assinatura, 1, 5, f);   // escreve os 5 bytes "N148I" de uma tacada so

// Compare com fazer o mesmo byte a byte com fputc:
fputc('N', f);
fputc('1', f);
fputc('4', f);
fputc('8', f);
fputc('I', f);
// As duas formas produzem exatamente os mesmos 5 bytes no arquivo.

Simples assim! Abrir, escrever/ler byte por byte, fechar, e só com essas pecinhas, a gente já consegue montar qualquer arquivo do mundo. 🌍

Organizando o nosso projeto em pastas

Antes de escrever código, deixa eu te mostrar como eu gosto de organizar a pasta do projeto. Conforme o N.148i cresce, ter tudo arrumadinho faz toda a diferença.

Sendo assim, recomendo que você crie uma pasta dentro do seu computador, e crie a seguinte estrutura, os arquivos ali existente dentro das pastas podem ficar vazios por enquanto, ok?

n148i/
├── src/
│   ├── main.c          ← o programa principal
│   ├── header.c        ← escrita/leitura do cabeçalho
│   └── header.h        ← as definições do cabeçalho
├── images/
│   └── entry.ppm       ← imagens de teste pra comprimir
└── output/
    └── image.n148i    ← os arquivos que a gente gera

Vamos começar a valer essa estrutura já neste artigo.

Crie a pasta n148i e, dentro dela, as três subpastas: src (onde ficam os códigos), images (as fotos de entrada) e output (os arquivos .n148i que a gente gera).

O programa que criaremos ainda neste artigo vai morar em n148i/src/main.c.

Neste primeiro artigo, pra não complicar, vou colocar todo o código dentro do main.c. Conforme a gente for avançando na Fase 4, vamos separar as coisas em header.c e header.h, cada arquivo cuidando de uma parte.  

Mas a árvore de pastas e arquivos, eu já queria que você deixasse criados desde agora.

Projetando o cabeçalho do N.148i

Agora a parte divertida: bora projetar a "receita" do nosso arquivo 👨‍🍳

Todo formato de arquivo começa com um cabeçalho (header): um pedacinho no comecinho da fita que guarda as informações essenciais sobre o arquivo.

Gostaria que você pensasse nele como a etiqueta de uma caixa de mudança, antes de abrir a caixa, você lê a etiqueta e já sabe o que tem dentro. 📦🏷️

Dito isso, o nosso decoder ele precisa saber como conseguir remontar a imagem após a compressão, e para que ele consiga fazer isso, devemos levar em consideração alguns pontos:

Precisamos de uma assinatura: pra confirmar que "sim, isto é um arquivo N.148i de verdade".

Precisamos de uma versão de arquivo: pra no futuro a gente poder mudar coisas sem quebrar arquivos antigos, e ele conseguir fazer a leitura mantendo a compatibilidade.

Precisamos descrever a altura e largura da imagem: senão o decoder não sabe o tamanho da foto.

Precisamos descrever a qualidade que foi usada: pra saber qual tabela de quantização aplicar na hora de descomprimir.

E por fim, precisamos armazenar o modo de chroma subsampling!

Nesse caso, o ideal é que o nosso cabeçalho tenha estes campos seguindo esta ordem:

Campo        │ Tamanho  │ Exemplo
─────────────┼──────────┼──────────────
Assinatura   │ 5 bytes  │ "N148I"
Versão       │ 1 byte   │ 1
Largura      │ 4 bytes  │ 640
Altura       │ 4 bytes  │ 480
Qualidade    │ 1 byte   │ 50
Chroma       │ 1 byte   │ 2 (= 4:2:0)
─────────────┴──────────┴──────────────
Total: 16 bytes de cabeçalho

Repara que a largura e a altura ocupam 4 bytes cada. Mas por quê?

Porque um único byte só vai de 0 a 255, e a nossa imagem pode ser bem maior que isso (640, 1920, 4000 pixels...). Com 4 bytes juntos, a gente consegue representar números gigantes (até uns 4 bilhões).

A assinatura: como um arquivo diz "sou eu"?

Aquele campo de assinatura (também chamado de magic number, número mágico) é um detalhe genial que quase todo formato usa.

A ideia é simples: os primeiros bytes do arquivo são sempre uma sequência fixa e conhecida, que identifica o formato, atuando como a "impressão digital" do arquivo. 🔍

E isso quer dizer que:

  • Todo PNG começa com os bytes que formam ‰PNG.
  • Todo PDF começa com %PDF.
  • E todo N.148i vai começar com as letras N148I.

Por que isso é útil? Porque quando o nosso decoder for abrir um arquivo, a primeira coisa que ele faz é conferir: "os primeiros 5 bytes são N148I?".

Se forem, ótimo, é um arquivo nosso. Se não forem, ele avisa "ei, isso aí não é um N148i!" em vez de tentar ler lixo e dar tela azul...

Um detalhe importante: a ordem dos bytes (endianness)

Aqui vem uma sutileza que confunde muita gente, então presta atenção que eu vou explicar bem devagar🐢

A largura 640 precisa de 4 bytes pra ser guardada. Mas... em que ordem a gente coloca esses 4 bytes na fita? 🤔

O número 640, em hexadecimal, é 00 00 02 80 e, existem dois jeitos de escrever ele na fita:

Do menor pro maior (little-endian): 80 02 00 00

Do maior pro menor (big-endian): 00 00 02 80

Os dois representam 640! Só muda a ordem, e isso tem um nome e se chama endianness (a "ordem dos bytes"). 🔄

O problema é: se eu escrevo de um jeito e você lê de outro, o número sai todo errado. Então a gente precisa escolher uma ordem e sempre usar a mesma, tanto na escrita (codec) quanto na leitura (decoder).

Pro N.148i, eu escolhi o little-endian (do menor pro maior byte).

E (detalhe importante) em vez de simplesmente jogar o número na fita e torcer, a gente vai escrever byte por byte, na mão, controlando a ordem exata. Isso garante que o arquivo funcione igualzinho em qualquer computador, seja Windows, Linux ou Mac. 💪

💡 Curiosidade: existe uma tentação de pegar a struct inteira do C e jogar direto no arquivo com um fwrite só. Não faça isso! O compilador às vezes coloca "buracos" (padding) entre os campos da struct pra alinhar a memória, e a ordem dos bytes varia por computador. O resultado seria um arquivo diferente em cada máquina. Escrever campo por campo, na mão, é mais trabalhoso, mas é o jeito certo e seguro. ✅

Instalando todas as dependências C em seu sistema operacional (Windows, Linux e Mac)

Se você é iniciante, talvez esteja se perguntando: "beleza, tenho o código, mas como é que eu transformo isso num programa que roda?".

Ótima pergunta! Vou explicar bem de leve, porque isso é algo que a gente nunca chegou a detalhar até agora nesta série 😊 (e peço perdão por isso).

Pra rodar código em C, a gente precisa de duas coisinhas:

  • Um compilador, que nada mais é do que um programa que traduz o nosso arquivo cuja estensão é .c pra linguagem de máquina (aquela que o processador entende). O compilador que a gente vai usar se chama gcc.
  • Um terminal, que é aquela telinha preta onde você digita comandos em vez de clicar em botões. É por ali que a gente manda o compilador trabalhar e depois roda o programa. ⌨️

Você só precisa preparar isso uma vez. Depois, é só usar. Como cada sistema operacional (Windows, Linux e Mac) é um pouco diferente, vou deixar que você escolha uma trilha abaixo de acordo com o sistema operacional que você vai usar...

Instalando o GCC no Windows

O Windows não vem com o gcc de fábrica, então a gente precisa instalar. O caminho mais tranquilo é instalar o MSYS2:

  • Baixe e instale o MSYS2 pelo site oficial: https://www.msys2.org
  • Abra o terminal dele (procure por "MSYS2 UCRT64" no menu iniciar) e instale o gcc digitando:
pacman -S mingw-w64-ucrt-x86_64-gcc

Pronto, a partir daí você usa esse terminal do MSYS2 pra compilar.

Instalando o GCC no Linux

Aqui é o mais fácil, porque o gcc quase sempre já vem, ou está a um comando de distância. No terminal (é só procurar por "Terminal" no seu sistema), rode o seguinte comando abaixo:

sudo apt install build-essential

Isso vale pra Ubuntu, Debian e derivados. Em outras distros o comando muda um pouquinho (no Fedora, por exemplo, é sudo dnf groupinstall "Development Tools").

Pronto, a partir daí você usa esse terminal pra compilar.

Instalando o GCC no Mac

No Mac, a Apple oferece as Command Line Tools, que já trazem o compilador. Abra o Terminal (fica em Aplicativos → Utilitários → Terminal) e rode:

xcode-select --install

Vai aparecer uma janelinha pedindo confirmação; é só aceitar e esperar instalar.

Um detalhe: no Mac, o comando gcc na verdade chama o clang (um outro compilador), mas pro nosso código dá exatamente no mesmo. 👍

Conferindo se deu certo 

Não importa o sistema, você pode testar se o compilador está instalado digitando isto no terminal:

gcc --version

Se aparecer um monte de texto com um número de versão, deu tudo certo! 🎉 Se der "comando não encontrado", volta um passo e confere a instalação.

O código completo em C

Beleza, agora eu acredito que já temos teoria o suficiente, sendo assim, bora programar!

O programa abaixo cria um cabeçalho N.148i, salva num arquivo de verdade dentro da pasta output/, e depois lê ele de volta pra provar que deu tudo certo.

Abra o arquivo n148i/src/main.c (o que fica dentro da pasta src) e coloque isto, e salve no final:

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

// The signature (magic number) of our format
#define N148I_MAGIC     "N148I"
#define N148I_MAGIC_LEN 5

// The "box label": everything the decoder needs to know
typedef struct {
    char     magic[6];   // "N148I" + '\0' terminator
    uint8_t  version;
    uint32_t width;
    uint32_t height;
    uint8_t  quality;
    uint8_t  chroma;     // 0 = 4:4:4   1 = 4:2:2   2 = 4:2:0
} N148iHeader;

// ============================================================
// WRITING NUMBERS (byte by byte, little-endian)
// ============================================================
//
// We write each byte by hand to guarantee the same order on
// any computer. Never dump the struct straight into the file!

void write_u8(FILE *f, uint8_t v) {
    fputc(v, f);
}

void write_u32(FILE *f, uint32_t v) {
    fputc((v      ) & 0xFF, f);   // least significant byte first
    fputc((v >>  8) & 0xFF, f);
    fputc((v >> 16) & 0xFF, f);
    fputc((v >> 24) & 0xFF, f);   // most significant byte last
}

// ============================================================
// SAVE THE HEADER TO A FILE
// ============================================================

int save_header(const char *path, N148iHeader *h) {
    FILE *f = fopen(path, "wb");     // "wb" = write binary
    if (!f) {
        printf("Could not create the file!\n");
        return 0;
    }

    fwrite(N148I_MAGIC, 1, N148I_MAGIC_LEN, f);  // 5 bytes of the signature
    write_u8(f, h->version);
    write_u32(f, h->width);
    write_u32(f, h->height);
    write_u8(f, h->quality);
    write_u8(f, h->chroma);

    fclose(f);
    return 1;
}

// ============================================================
// READING NUMBERS (in the same order we wrote them)
// ============================================================

uint8_t read_u8(FILE *f) {
    return (uint8_t) fgetc(f);
}

uint32_t read_u32(FILE *f) {
    uint32_t b0 = fgetc(f);
    uint32_t b1 = fgetc(f);
    uint32_t b2 = fgetc(f);
    uint32_t b3 = fgetc(f);
    return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
}

// ============================================================
// LOAD THE HEADER FROM A FILE
// ============================================================

int load_header(const char *path, N148iHeader *h) {
    FILE *f = fopen(path, "rb");     // "rb" = read binary
    if (!f) {
        printf("Could not open the file!\n");
        return 0;
    }

    // First, we check the signature
    char m[6] = {0};
    fread(m, 1, N148I_MAGIC_LEN, f);
    if (memcmp(m, N148I_MAGIC, N148I_MAGIC_LEN) != 0) {
        printf("This is NOT a valid N148i file!\n");
        fclose(f);
        return 0;
    }

    // Signature matches: read the rest in the same order we saved
    strcpy(h->magic, N148I_MAGIC);
    h->version = read_u8(f);
    h->width   = read_u32(f);
    h->height  = read_u32(f);
    h->quality = read_u8(f);
    h->chroma  = read_u8(f);

    fclose(f);
    return 1;
}

// Just to print the chroma mode nicely
const char* chroma_name(uint8_t c) {
    switch (c) {
        case 0: return "4:4:4";
        case 1: return "4:2:2";
        case 2: return "4:2:0";
        default: return "unknown";
    }
}

// ============================================================
// MAIN
// ============================================================

int main() {
    // Build an example header: 640x480 image, quality 50, 4:2:0
    N148iHeader h;
    strcpy(h.magic, N148I_MAGIC);
    h.version = 1;
    h.width   = 640;
    h.height  = 480;
    h.quality = 50;
    h.chroma  = 2;

    // 1. Save to disk, into the output/ folder (one level up from src/)
    if (!save_header("../output/image.n148i", &h)) return 1;
    printf("File '../output/image.n148i' saved successfully!\n\n");

    // 2. Read it back and show it
    N148iHeader loaded;
    if (!load_header("../output/image.n148i", &loaded)) return 1;

    printf("--- Header read from file ---\n");
    printf("Signature: %s\n", loaded.magic);
    printf("Version:   %d\n", loaded.version);
    printf("Size:      %u x %u pixels\n", loaded.width, loaded.height);
    printf("Quality:   %d\n", loaded.quality);
    printf("Chroma:    %s\n", chroma_name(loaded.chroma));

    return 0;
}

Observe que a partir de agora, estamos usando a língua inglesa para criar variáveis, funções, consoles e comentários, uma vez que ela é a mais falado no mundo inteiro.

Feito isso, e com o arquivo salvo, chegou o momento de abrirmos o nosso terminal, dentro da pasta n148i, e seguir os comandos abaixo: (Lembre-se de digitar linha por linha, um de cada vez, são três comandos separados, ok?)

cd n148i/src
gcc main.c -o n148i
./n148i

Traduzindo cada linha acima:

  • cd n148i/src → entra na pasta src, onde mora o nosso main.c. 📂
  • gcc main.c -o n148i → compila o main.c e gera um programa chamado n148i. O -o é de output (o nome do programa que vai sair). 🔨
  • ./n148i → roda o programa que acabamos de criar. ▶️

💡 No Windows, o programa gerado vem com .exe no nome (n148i.exe). No terminal do MSYS2, o ./n148i funciona igual. Se estiver usando o PowerShell, rode com .\n148i.exe.  

O que acontece quando você roda o programa acima?

Ao rodar o programa, o resultado que aparece na tela é exatamente este:

File '../output/image.n148i' saved successfully!

--- Header read from file ---
Signature: N148I
Version:   1
Size:      640 x 480 pixels
Quality:   50
Chroma:    4:2:0

E se você olhar dentro da pasta output/, o arquivo image.n148i vai estar lá, com os seus 16 bytes. 🎉

Mas o mais legal é espiar o arquivo por dentro e ver a fita de bytes que a gente criou.  

No Linux ou Mac, você pode usar o comando od pra ver os bytes em hexadecimal (rodando ainda de dentro de src/):

od -A d -t x1 ../output/image.n148i

E o que aparece são exatamente 16 bytes:

4e 31 34 38 49 01 80 02 00 00 e0 01 00 00 32 02

Agora vem a parte mais satisfatória: vamos decifrar cada byte dessa fita, um por um, e ver a nossa receita funcionando na prática. 🔬

Deixa eu organizar isso num quadro pra ficar cristalino:

Bytes            │ Campo       │ Significado
─────────────────┼─────────────┼──────────────
4e 31 34 38 49   │ Assinatura  │ "N148I"
01               │ Versão      │ 1
80 02 00 00      │ Largura     │ 640
e0 01 00 00      │ Altura      │ 480
32               │ Qualidade   │ 50  (0x32 = 50)
02               │ Chroma      │ 2 (= 4:2:0)

Olha que lindo! 😍

Cada byte tem o seu lugar e o seu significado. Repara nos primeiros cinco: 4e é o código da letra 'N', 31 é o '1', 34 é o '4', 38 é o '8' e 49 é o 'I', juntos formam a nossa assinatura N148I.

E no 80 02 00 00 da largura: lido do jeito little-endian, o 80 (128) mais o 02 (que vale 2×256 = 512) dá exatamente 640. É a ordem dos bytes que a gente combinou funcionando na prática. ✅

Esse image.n148i de 16 bytes é, oficialmente, o primeiro arquivo N.148i da história. 🏆 Ele ainda não tem a imagem dentro, só a etiqueta. Mas o alicerce está construído.

O que vem por aí no N.148i

O cabeçalho é só a etiqueta da caixa. O que vem depois dele, na fita, é o conteúdo de verdade: os bytes comprimidos de todos os blocos da imagem, canal por canal.

Nos próximos artigos da Fase 4, a gente vai:

  • Ler uma imagem de entrada de verdade (num formato simples de ler, o PPM) e separar nos canais Y, Cb, Cr.
  • Passar a imagem inteira pelo pipeline da Fase 3 (blocos → DCT → quantização → zig-zag → RLE → Huffman), não só um bloco.
  • Gravar todo esse conteúdo comprimido logo depois do cabeçalho, formando o arquivo .n148i completo.
  • E claro, o caminho de volta: abrir um .n148i, ler o cabeçalho, descomprimir tudo e remontar a imagem original.

Ou seja: no fim da Fase 4, a gente vai ter um encoder e um decoder de verdade, que abrem e salvam arquivos .n148i reais. 🎯

Repositório no GitHub

E sim, você pode acompanhar a evolução desse CODEC de imagens em um repositório no GitHub, commit por commit 👋

Segue o link abaixo do commit desse artigo, onde criamos a estrutura de pastas e a lógica de criação de cabeçalho:

Resumo

Recapitulando este primeiro passo da Fase 4:

  • 🏷️ O nosso codec se chama N.148i (o "i" é de imagem), e ele gera arquivos com a extensão .n148i.
  • 🧱 Todo arquivo, no fundo, é uma fita de bytes: quadradinhos numerados de 0 a 255. A diferença entre formatos está na receita de como ler essa fita.
  • 🚪 Em C, a gente mexe com arquivos em três passos: abrir (fopen com modo "wb" ou "rb"), escrever/ler (fputc, fgetc, fwrite, fread) e fechar (fclose). Nunca esqueça o "b" de binário!
  • 🛠️ Pra rodar C, você precisa de um compilador (o gcc) e um terminal: e agora você já sabe instalar isso no Windows, Linux ou Mac.
  • 🪪 O cabeçalho (header) é a etiqueta da caixa: guarda a assinatura, versão, largura, altura, qualidade e modo de chroma.
  • 🔍 A assinatura (magic number) N148I deixa qualquer programa reconhecer o nosso formato na hora.
  • 🔀 A ordem dos bytes (endianness) importa: escolhemos little-endian e escrevemos byte a byte na mão, pra o arquivo funcionar igual em qualquer computador.
  • 🏆 Criamos, salvamos e lemos de volta o primeiro cabeçalho N.148i, um arquivo real de 16 bytes.

Demos o primeiro passo pra sair do mundo da teoria e entrar no mundo dos arquivos de verdade. 💪

A etiqueta da caixa está pronta; agora falta encher a caixa!

No próximo artigo, a gente vai aprender a ler uma imagem de verdade pra dentro do nosso programa, no formato PPM (um formato super simples e perfeito pra aprender), e separar ela nos canais de cor.

É o primeiro passo pra alimentar o nosso encoder com uma foto real. 🖼️

Prepara o café ☕, porque a partir daqui a gente começa a trabalhar com imagens de verdade. Até o próximo! 👋

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.