Llevo literalmente tres horas intentando aplicar un simple filtro de escala de grises a una textura para un motor 2D en el que estoy cacharreando. ¿El problema? Los malditos archivos BMP y la forma en que C maneja la memoria.
Lo que empezó como una tarde de picar código tranquilo, leyendo documentación, se ha convertido en una guerra abierta contra la alineación de bytes.
El barro: Entendiendo al monstruo de dos cabezas
Para manipular un archivo BMP a pelo, sin tirar de librerías externas pesadas (porque compilar SDL_image o libjpeg en Windows con Dev-C++ y MinGW a veces es para pegarse un tiro), tienes que entender qué hay en esos primeros bytes del archivo.
El formato BMP sin compresión tiene dos cabeceras principales antes de llegar a la carnaza (los píxeles): 1. BITMAPFILEHEADER: Ocupa exactamente 14 bytes y nos dice que es un archivo BMP válido (debe empezar por 'B' y 'M') y dónde empiezan los datos de la imagen. 2. BITMAPINFOHEADER: Son 40 bytes que contienen el ancho, alto y la profundidad de color (los típicos 24 bits, un byte para el rojo, verde y azul).
Parece fácil, abres un FILE *, haces un fread de la estructura y a correr. Pues no.
El infame padding del compilador
Aquí es donde C te apuñala por la espalda. Los compiladores de C (sí, te miro a ti, GCC 3.x) alinean las variables en memoria para que el procesador las lea más rápido, normalmente en bloques de 4 bytes. Como el BITMAPFILEHEADER ocupa 14 bytes (que no es múltiplo de 4), el compilador te cuela silenciosamente dos bytes basura al final de la estructura para cuadrarlo a 16. Cuando haces el fread, lees la cabecera movida, los datos se desplazan, el ancho de la imagen te da un número absurdo como 16777216 y todo explota.
Para evitar este desastre y trastear a gusto, hay que obligar al compilador a empaquetar la memoria byte a byte. Os dejo el código que me ha salvado la vida esta noche:
#include <stdio.h>
#include <stdlib.h>
/* Definimos nuestros propios tipos para asegurar el tamaño,
nada de fiarse de si int son 16 o 32 bits en esta máquina */
typedef unsigned short WORD; // 2 bytes
typedef unsigned int DWORD; // 4 bytes
/* Obligamos al compilador a alinear a 1 byte */
#pragma pack(push, 1)
typedef struct {
WORD bfType; // Las mágicas letras 'BM'
DWORD bfSize; // Tamaño total del archivo
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits; // Offset donde empiezan nuestros ansiados píxeles
} BITMAPFILEHEADER;
typedef struct {
DWORD biSize; // Tamaño de esta cabecera (40 bytes)
DWORD biWidth; // Ancho en píxeles
DWORD biHeight; // Alto en píxeles
WORD biPlanes; // Siempre 1
WORD biBitCount; // Bits por píxel (24 para RGB)
DWORD biCompression; // 0 = Sin comprimir
DWORD biSizeImage;
DWORD biXPelsPerMeter;
DWORD biYPelsPerMeter;
DWORD biClrUsed;
DWORD biClrImportant;
} BITMAPINFOHEADER;
#pragma pack(pop) /* Restauramos la alineación normal */
int main() {
FILE *f = fopen("textura.bmp", "rb");
if (!f) return 1;
BITMAPFILEHEADER fileHeader;
fread(&fileHeader, sizeof(BITMAPFILEHEADER), 1, f);
if (fileHeader.bfType != 0x4D42) {
printf("¡Esto no es un BMP, melón!\n");
return 1;
}
printf("Offset a los píxeles: %d bytes\n", fileHeader.bfOffBits);
fclose(f);
return 0;
}
Ese mágico #pragma pack(push, 1) (o __attribute__((packed)) si estás jugando en Linux purista) ha sido la diferencia entre irme a dormir frustrado o poder ver por consola dónde puñetas empiezan mis píxeles.
Conclusión
Tener que bajar al barro para destripar arrays de bytes con punteros es duro y a veces desesperante, pero te da un control absoluto sobre la máquina. Sientes que de verdad estás programando y no solo uniendo piezas prefabricadas.
¿El futuro? Quiero pensar que de aquí a unos años, el hardware será tan bestia que no tendremos que preocuparnos por estos ciclos de reloj o alineaciones de memoria absurdas, y habrá librerías estándar en todos los lenguajes que hagan un triste cargarImagen("foto.jpg") y te devuelvan un array de píxeles sin que tengas que estudiar la especificación del formato y pelearnos con los bits a capón.
Pero por ahora, si quieres hacer cosas rápidas y gráficas, toca ensuciarse las manos. Me voy a la cama que en unas horas suena el despertador.