Eu queria muito saber se vocês já tiveram a oportunidade de trabalhar com imagens ou outros arquivos binários… Se já trabalhou tenho quase certeza que fez isso utilizando uma abstração (mais conhecido como biblioteca), e apesar de sempre ouvir todo mundo dizendo para não reinventar a roda, eu acho que em casos como esses você vai ganhar muito mais reinventando ela, já que vai ter um real conhecimento de como o formato funciona e de como criar o seu próprio formato.

File signature

A primeira coisa que nós precisamos saber sobre arquivos binários é file signature, ou assinatura do arquivo, basicamente todo formato tem uma forma de identificar o arquivo, uma espécie de assinatura digital.. e geralmente são uma sequencia de bytes que fica no inicio do arquivo:

.jpg / .jpeg :

hex: ff       d8
dec: 255 216
bin: 11111111 11011000

.exe :

hex:   4d      5a
dec: 77 90
bin: 1001101 1011010
ascii: M Z

E como agente vai trabalhar com uma PNG ( já que eu tenho que escolher algum formato para explicar todos os conceitos aqui ), essa é a assinatura de uma imagem PNG:

hex:   89       50      4e      47      0d   0a   1a    0a
dec: 137 80 78 71 13 10 26 10
bin: 10001001 1010000 1001110 1000111 1101 1010 11010 1010
ascii: P N G

Mão na massa

Já que já entendemos essa parte nós vamos importar a seguinte imagem:

img

Mas redimensionada já que vai ficar mais simples de trabalharmos com uma imagem pequena, e quando acabarmos o programa estaremos prontos para usar a imagem acima, mas até lá iremos usar esta:

img

E tenha em mente que como não vamos implementar todos os suportes necessários, outras imagens podem não ser suportadas pelo código abordado neste post, dito isto vamos começar.

Primeiramente iremos importar o stdio.h no nosso arquivo C, criar a função main e abrir a imagem:

#include <stdio.h>
int main(){
// Criando o arquivo e abrindo a imagem
FILE * image = fopen("acdc.min.png", "rb");
return 0;
}

Tá, mas por enquanto nosso código não faz nada, então iremos ler os primeiros 8 bytes e checar se o arquivo inserido é mesmo uma imagem PNG:

Reescreva o código abaixo logo após a linha onde importamos o arquivo

// Criando variável para guardar a assinatura
Byte signature[9];
signature[8] = 0; /* Caractere vazio para in-
dicar fim da string. */
// Lendo os 8 bytes da imagem e colocando na variavel signature
fread(signature, 1, 8, image);

E logo após isso temos que comparar as duas strings para averiguar se o arquivo é mesmo uma PNG, mas para isso teremos que importar o string.h no inicio do código para fazer a comparação:

#include <string.h>

Agora nós vamos fazer uma checagem e para isso primeiro temos que criar uma variável com os bytes certos:

Coloque logo abaixo da importação do string.h

typedef char Byte;
// Assinatura : 89 50 4e 47 0d 0a 1a 0a
Byte PNG_sign [] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0};

Agora é só fazer a comparação logo abaixo da nova variável e escrever na tela se o arquivo é ou não uma PNG:

if (strcmp(signature, PNG_sign) == 0)
puts("A imagem é uma PNG!");
else puts("O arquivo inserido não é uma PNG!");

Código final

E o nosso código ficou ( ou deveria ter ficado ) assim:

#include <stdio.h>
#include <string.h>
// Assinatura : 89 50 4e 47 0d 0a 1a 0a
const Byte PNG_sign [] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0};
int main(){
// Criando o arquivo e abrindo a imagem
FILE * image = fopen("image.png", "rb");
// Criando variável para guardar a assinatura
Byte signature[9];
signature[8] = 0; /* Caractere vazio para in-
dicar fim da string. */
// Lendo os 8 bytes da imagem e colocando na variavel signature
fread(signature, 1, 8, image);
// Checando se o arquivo é ou não uma PNG
if (strcmp(signature, PNG_sign) == 0)
puts("A imagem é uma PNG!");
else puts("O arquivo inserido não é uma PNG!");
return 0;
}

Esse código vai funcionar plenamente, mas vamos transformar ele em uma função de validação para deixar as coisas mais limpas nos próximos tópicos:

crie um outro arquivo chamado png.c e inclua a seguinte função junto com os includes nele.

#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include "png.h"
bool is_PNG(FILE * image){
// Criando variável para guardar a assinatura
Byte signature[9];
signature[8] = 0; /* Caractere vazio para in-
dicar fim da string. */
// Lendo os 8 bytes da imagem e colocando na variavel signature
fread(signature, 1, 8, image);
// Checando se o arquivo é ou não uma PNG
if (strcmp(signature, PNG_sign) == 0)
return true;
return false;
}

E adicione as linhas a seguir em um arquivo png.h:

typedef char Byte;
// Assinatura : 89 50 4e 47 0d 0a 1a 0a
const Byte PNG_sign [] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0};
bool is_PNG(FILE * image);

Como começamos a separar os arquivos teremos que compilar assim:

$ <compilador> *.c -o <executavel>

Chunks

Beleza, já sabemos como identificar o arquivo e vocês já tem uma idéia de como a leitura da imagem acontece, ta na hora de entendermos um pouco mais sobre os “blocos de byte” ou chunks, e eles variam de binário para binário, mas geralmente eles tem uma estrutura clara de como os bytes se organizam.

No caso de imagens PNG a estrutura é a seguinte:

tamanho  // 4 bytes com a quantidade de bytes do chunk  
tipo // 4 bytes com o tipo do chunk
dados // <tamanho> bytes com os dados
crc /* 4 bytes com o numero para validar
se os dados não foram corrompidos */

E cada chunk tem uma especificação de como os dados dele devem ser interpretados.

Cabeçalho da imagem — IHDR

O chunk de cabeçalho ou IHDR que é o responsável por alguns metadados muito importantes para a imagem, como altura e largura:

tamanho: 13
tipo: IHDR
dados:
comprimento: 4 bytes
altura: 4 bytes
profundidade de bits: 1 byte
tipo de cor: 1 byte
tipo de compressão: 1 byte
tipo de intrelaçamento: 1 byte
crc: <crc>

E acho que você já percebeu que o chunk IHDR é responsável pelos metadados da imagem e também sempre vem primeiro, e nós vamos nos focar nele por enquanto para entendermos melhor como isso se encaixa em forma de código.

Eu acho que despejei muita informação de uma vez, mas vamos começar estruturando o tipo Chunk que vai representar os dados brutos em nosso código:

Adicione os códigos a seguir no arquivo png.h

#include <stdint.h> // Para usarmos o int32_ttypedef Byte char   // Pra ficar mais didatico
typedef struct {
int32_t lenght; // O int 32 sempre tem 4 bytes
Byte type[4]; // O tipo é sempre uma string
void * data; // Os dados tem tamanho variável
int32_t crc;
} Chunk;

Agora iremos estruturar o tipo IHDR :

Sei que agora pode estar um pouco confuso para alguns de vocês, tenha calma que eu já to quase lá.

typedef struct {
uint32_t width; // Comprimento
uint32_t height; // Altura
Byte depth; // Profundidade de bits
Byte color; // Tipo de cor
Byte interlace; // Tipo de intrelaçamento
Byte filter; // Tipo de cor
Byte compression; // Tipo de compressão
} IHDR;

E já que criamos nossas estruturas agora resta ler os dados da imagem e colocar nelas. Para isso primeiro temos que ler o chunk.

Em um arquivo main.c escreva os seguintes códigos:

#include <string.h> // strcmp
#include <stdio.h> // FILE, fopen
#include <stdlib.h> // exit
#include "png.h" // Chunk, Byte
// Função que interrompe o programa e exibe mensagem em caso de erros
void die(Byte * msg){
fprintf(stderr, "Error: %s\n", msg);
exit(1);
}
int main(){
// Abrindo a imagem
FILE * image = fopen("image.png", "rb");
if (!image)
die("Não foi possível ler a imagem");
if (!is_PNG(image))
die("A imagem não é uma PNG");
return 0;
}

Agora que temos que realmente ler a imagem e colocar no chunk:

Chunk bloco;
fread(&bloco.lenght, 4, 1, image); // Lendo o tamanho
// Criando buffer de dados com o tamanho lido
bloco.data = (Byte *)malloc(bloco.lenght);
// Lendo o tipo e validando se o cabeçalho existe
fread(bloco.type, 4, 1, image);
bloco.type[4] = 0; /* O \0 é obrigatório em
strings */
if (strcmp(bloco.type, "IHDR"))
die("Não foi possível ler o cabeçalho do arquivo");
// Lendo os dados do cabeçalho
fread(bloco.data, bloco.lenght, 1, image);
// Lendo o crc
fread(&bloco.crc, 4, 1, image); /* O ideal seria rodar
um algoritmo de CRC
e comparar com esse
número, mas não vai
fazer diferença pra
gente, então vamos
ignorar essa etapa */
// Salvando dados no cabeçalho
IHDR * cabecalho = (IHDR *)bloco.data;

E é aqui que você acha que está tudo certo, já que o código compila sem erros e funciona aparentemente sem erros…

E você só terá certeza disso exibindo as únicas variáveis que importam para agente por enquanto, bloco.lenght, cabecalho->height e cabecalho->width, se você exibi-las na tela como estratégia de depuração terá esse resultado:

printf("Tamanho do cabecalho: %i, Largura: %i, Altura: %i\n",
bloco.lenght, cabecalho->height, cabecalho->width);

Saída:

Tamanho do cabecalho: 218103808, Largura: 486539264, Altura: 469762048

Rodando um file no arquivo acdc.min.png recebi esse resultado:

Deveria ser:

Tamanho do cabecalho: 13, Largura: 29, Altura: 28

Olha só que maravilhoso, um bug… Relaxa depois resolvemos.

Conclusão

Caso queira o código completo acessa ele nesse gist, ou no replit acima.

E você pode tentar resolver isso sozinho até o próximo post, onde iremos explicar o motivo do bug, como corrigi-lo, estruturar melhor o IHDR e ver mais alguns conceitos relacionados a PNGs que vocês vão precisar.

Boa sorte e até o próximo capítulo da série!!

--

--

Yaks Souza

Hello, I’m Yak’s Souza and a developer without cetificate, I’m a curious, a dreamer who dreams too high and ends up falling over and over, but I love what I do.