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.