Skip to article frontmatterSkip to article content

Creación de Imágenes de Mapa de Bits

Para crear salidas gráficas.

Introducción

El formato de archivo de mapa de bits (BMP, de Bitmap), también conocido como mapa de bits independiente del dispositivo (DIB), es un formato de imagen rasterizada desarrollado por Microsoft. Su principal característica, en su variante más común, es la simplicidad: no utiliza compresión, lo que significa que cada píxel se almacena directamente con su valor de color. Esto hace que los archivos sean grandes en comparación con formatos como PNG o JPEG, pero a su vez, muy fáciles de leer y escribir mediante programación, lo que los convierte en una excelente herramienta de aprendizaje.

En este apunte, exploraremos cómo crear un archivo BMP desde cero en C, manejando directamente la escritura de las cabeceras y los datos de los píxeles en el archivo. Seguiremos las pautas de estilo de Regla 0x0000h: La claridad y prolijidad son de máxima importancia para asegurar que nuestro código sea claro y mantenible.

¿Cómo son los archivos?

Un archivo BMP se compone principalmente de tres o cuatro partes:

  1. Cabecera del Archivo (File Header): Identifica el archivo como un BMP y contiene información sobre su tamaño total.

  2. Cabecera de Información (DIB Header): Describe los atributos de la imagen, como sus dimensiones y profundidad de color.

  3. Tabla de Colores (Paleta): Opcional. Se utiliza para imágenes con 256 colores o menos (8 bits, 4 bits, 1 bit).

  4. Datos de Píxeles (Pixel Array): Una secuencia de bytes que definen el color de cada píxel de la imagen.

Alineación de Estructuras y #pragma pack

Antes de definir las estructuras de las cabeceras, debemos abordar un concepto crucial: la alineación de memoria.

¿Qué es la Alineación y por qué existe?

Por razones de rendimiento, los compiladores de C suelen añadir bytes de relleno (padding) invisibles dentro de las estructuras. El objetivo es que cada miembro de la estructura comience en una dirección de memoria que sea un múltiplo de su propio tamaño. Por ejemplo, un uint32_t (4 bytes) se alineará preferentemente en una dirección de memoria divisible por 4. Acceder a datos alineados es mucho más rápido para la mayoría de las arquitecturas de CPU.

El Problema con los Formatos de Archivo

Los formatos de archivo binarios como BMP definen una secuencia de bytes exacta y contigua. No hay bytes de relleno. Si declaramos una struct en C y la escribimos directamente en un archivo, el compilador podría haber insertado bytes de padding, corrompiendo el archivo y haciéndolo ilegible para otros programas.

Por ejemplo, nuestra cabecera_archivo_bmp_t tiene un uint16_t seguido de un uint32_t. Un compilador podría añadir 2 bytes de relleno después del uint16_t para que el uint32_t comience en una dirección alineada a 4 bytes. Esto haría que la estructura tuviera 16 bytes en memoria en lugar de los 14 que especifica el formato BMP.

La Solución: #pragma pack

Para solucionar esto, usamos la directiva de preprocesador #pragma pack. Esta directiva le indica al compilador cómo debe alinear los miembros de una estructura.

Paso 1: Las Estructuras de Cabecera

Ahora sí, definimos las estructuras para las cabeceras del BMP, envueltas en las directivas #pragma pack.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdint.h>

// Desactiva el padding para que la estructura coincida con el formato del archivo.
#pragma pack(push, 1)

/**
 * @brief Cabecera de archivo BMP (14 bytes).
 * Contiene información general sobre el archivo de mapa de bits.
 */
typedef struct {
    uint16_t tipo;              // 'BM' (0x4D42) para identificar el formato.
    uint32_t tamano_archivo;    // Tamaño total del archivo en bytes.
    uint16_t reservado1;        // No se usa, debe ser 0.
    uint16_t reservado2;        // No se usa, debe ser 0.
    uint32_t offset_datos;      // Distancia en bytes desde el inicio del archivo hasta los datos de píxeles.
} cabecera_archivo_bmp_t;

/**
 * @brief Cabecera de información del bitmap (DIB header, 40 bytes).
 * Contiene detalles sobre la imagen.
 */
typedef struct {
    uint32_t tamano_cabecera;   // Tamaño de esta cabecera (40 bytes).
    int32_t  ancho;             // Ancho de la imagen en píxeles.
    int32_t  alto;              // Alto de la imagen en píxeles.
    uint16_t planos;            // Número de planos de color (siempre 1).
    uint16_t bits_por_pixel;    // Profundidad de color (ej. 1, 8, 24).
    uint32_t compresion;        // Tipo de compresión (0 para sin compresión).
    uint32_t tamano_imagen;     // Tamaño de los datos de píxeles en bytes (incluyendo padding).
    int32_t  resolucion_x;      // Píxeles por metro en X (generalmente 0).
    int32_t  resolucion_y;      // Píxeles por metro en Y (generalmente 0).
    uint32_t colores_usados;    // Número de colores en la paleta (0 para 24-bit).
    uint32_t colores_importantes; // Número de colores importantes (0 = todos).
} cabecera_info_bmp_t;

// Restaura la configuración de padding original.
#pragma pack(pop)

Ejemplo 1: Crear una imagen de 24 bits (True Color)

Una imagen de 24 bits es la más sencilla de crear, ya que no requiere una paleta de colores. Cada píxel se representa por tres bytes: uno para el azul (Blue), uno para el verde (Green) y uno para el rojo (Red), en ese orden (BGR).

Datos de Píxeles en 24 bits

Los datos de los píxeles se escriben en el archivo de una manera muy específica:

  1. Orden de Filas: Se escriben desde la fila inferior de la imagen hasta la superior.

  2. Orden de Píxeles: Dentro de cada fila, los píxeles se escriben de izquierda a derecha.

  3. Orden de Colores: Cada píxel se almacena en formato BGR (Azul, Verde, Rojo), no en el más común RGB.

  4. Relleno (Padding): Cada fila de píxeles debe ocupar un número de bytes que sea múltiplo de 4. Si la cantidad de bytes de los píxeles de una fila (ancho * 3) no es múltiplo de 4, se deben añadir bytes de relleno (con valor 0) al final de la fila hasta alcanzar el siguiente múltiplo de 4.

La fórmula para calcular el padding es: (4 - (ancho_en_pixeles * 3) % 4) % 4.

Código de Ejemplo

La siguiente función crea una imagen BMP de 24 bits con un degradado de azul a negro.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>

// Incluir las definiciones de las estructuras de cabecera aquí...

/**
 * @brief Crea una imagen BMP de 24 bits con un degradado.
 *
 * @param nombre_archivo El nombre del archivo a crear.
 * @param ancho El ancho de la imagen en píxeles.
 * @param alto El alto de la imagen en píxeles.
 * @returns `true` si la imagen se creó con éxito, `false` en caso contrario.
 *
 * @pre nombre_archivo no debe ser NULL.
 * @post Se crea un archivo BMP en disco o se retorna `false`.
 */
bool crear_degradado_bmp_24bit(const char *nombre_archivo, int ancho, int alto) {
    FILE *archivo = NULL;
    uint8_t *fila_pixeles = NULL;
    bool exito = true;

    archivo = fopen(nombre_archivo, "wb");
    if (archivo == NULL) {
        perror("No se pudo crear el archivo BMP");
        exito = false;
    }

    if (exito) {
        // --- Cálculo de Tamaños y Padding ---
        int padding = (4 - (ancho * 3) % 4) % 4;
        int tamano_fila = (ancho * 3) + padding;
        uint32_t tamano_datos_pixeles = tamano_fila * alto;

        // --- Cabecera de Archivo ---
        cabecera_archivo_bmp_t cabecera_archivo = {
            .tipo = 0x4D42, // 'BM' en little-endian
            .tamano_archivo = sizeof(cabecera_archivo_bmp_t) + sizeof(cabecera_info_bmp_t) + tamano_datos_pixeles,
            .reservado1 = 0,
            .reservado2 = 0,
            .offset_datos = sizeof(cabecera_archivo_bmp_t) + sizeof(cabecera_info_bmp_t)
        };

        // --- Cabecera de Información (DIB) ---
        cabecera_info_bmp_t cabecera_info = {
            .tamano_cabecera = sizeof(cabecera_info_bmp_t),
            .ancho = ancho,
            .alto = alto,
            .planos = 1,
            .bits_por_pixel = 24,
            .compresion = 0, // BI_RGB (sin compresión)
            .tamano_imagen = tamano_datos_pixeles,
            .resolucion_x = 2835, // 72 DPI
            .resolucion_y = 2835, // 72 DPI
            .colores_usados = 0, // No usa paleta
            .colores_importantes = 0
        };

        // --- Escritura de Cabeceras ---
        if (fwrite(&cabecera_archivo, sizeof(cabecera_archivo_bmp_t), 1, archivo) != 1 ||
            fwrite(&cabecera_info, sizeof(cabecera_info_bmp_t), 1, archivo) != 1) {
            exito = false;
        }
    }

    if (exito) {
        // --- Escritura de Datos de Píxeles ---
        fila_pixeles = (uint8_t *)calloc(tamano_fila, 1);
        if (fila_pixeles == NULL) {
            exito = false;
        }
    }

    if (exito) {
        // Escribimos las filas desde abajo hacia arriba (y = 0 es la fila inferior).
        for (int y = 0; y < alto && exito; y++) {
            for (int x = 0; x < ancho; x++) {
                // Creamos un degradado azul que varía con la altura
                uint8_t azul = (uint8_t)((double)y / alto * 255.0);
                uint8_t verde = 0;
                uint8_t rojo = 0;

                // El formato es BGR
                fila_pixeles[x * 3 + 0] = azul;
                fila_pixeles[x * 3 + 1] = verde;
                fila_pixeles[x * 3 + 2] = rojo;
            }

            if (fwrite(fila_pixeles, 1, tamano_fila, archivo) != tamano_fila) {
                exito = false;
            }
        }
    }

    // --- Bloque de Limpieza Centralizado ---
    if (fila_pixeles != NULL) {
        free(fila_pixeles);
    }
    if (archivo != NULL) {
        fclose(archivo);
    }

    return exito;
}

Ejemplo 2: Crear una imagen de 8 bits (Escala de Grises)

Las imágenes de 8 bits utilizan una paleta de colores para definir los hasta 256 colores que pueden usar. Los datos de los píxeles, en lugar de contener el color BGR, contienen un índice (de 0 a 255) que apunta a una entrada en esta paleta.

La Paleta de Colores

La paleta se escribe en el archivo justo después de las cabeceras. Para una imagen en escala de grises, crearemos una paleta donde cada entrada tiene el mismo valor para R, G y B (ej. R=G=B=128 para un gris medio), creando así 256 tonos de gris.

1
2
3
4
5
6
7
8
9
/**
 * @brief Representa una entrada en la tabla de colores BMP (4 bytes).
 */
typedef struct {
    uint8_t azul;
    uint8_t verde;
    uint8_t rojo;
    uint8_t reservado; // Debe ser 0
} rgb_quad_t;

Código de Ejemplo

Esta función crea una imagen en escala de grises de 8 bits con un degradado horizontal.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// Incluir cabeceras y estructuras...

bool crear_degradado_bmp_8bit(const char *nombre_archivo, int ancho, int alto) {
    FILE *archivo = NULL;
    uint8_t *fila_indices = NULL;
    bool exito = true;

    archivo = fopen(nombre_archivo, "wb");
    if (archivo == NULL) {
        perror("No se pudo crear el archivo BMP");
        exito = false;
    }

    if (exito) {
        int padding = (4 - ancho % 4) % 4;
        int tamano_fila = ancho + padding;
        uint32_t tamano_datos_pixeles = tamano_fila * alto;
        uint32_t tamano_paleta = 256 * sizeof(rgb_quad_t);
        uint32_t offset_datos = sizeof(cabecera_archivo_bmp_t) + sizeof(cabecera_info_bmp_t) + tamano_paleta;

        cabecera_archivo_bmp_t cabecera_archivo = {
            .tipo = 0x4D42,
            .tamano_archivo = offset_datos + tamano_datos_pixeles,
            .reservado1 = 0,
            .reservado2 = 0,
            .offset_datos = offset_datos
        };

        cabecera_info_bmp_t cabecera_info = {
            .tamano_cabecera = sizeof(cabecera_info_bmp_t),
            .ancho = ancho,
            .alto = alto,
            .planos = 1,
            .bits_por_pixel = 8,
            .compresion = 0,
            .tamano_imagen = tamano_datos_pixeles,
            .resolucion_x = 2835,
            .resolucion_y = 2835,
            .colores_usados = 256, // Indicamos que usamos 256 colores
            .colores_importantes = 256 // O 0 para indicar que todos son importantes
        };

        // --- Escritura de Cabeceras y Paleta ---
        if (fwrite(&cabecera_archivo, 1, sizeof(cabecera_archivo_bmp_t), archivo) != sizeof(cabecera_archivo_bmp_t) ||
            fwrite(&cabecera_info, 1, sizeof(cabecera_info_bmp_t), archivo) != sizeof(cabecera_info_bmp_t)) {
            exito = false;
        } else {
            rgb_quad_t paleta[256];
            for (int i = 0; i < 256; i++) {
                paleta[i].rojo = (uint8_t)i;
                paleta[i].verde = (uint8_t)i;
                paleta[i].azul = (uint8_t)i;
                paleta[i].reservado = 0;
            }
            if (fwrite(paleta, sizeof(rgb_quad_t), 256, archivo) != 256) {
                exito = false;
            }
        }
    }

    if (exito) {
        fila_indices = (uint8_t *)calloc(tamano_fila, 1);
        if (fila_indices == NULL) {
            exito = false;
        }
    }

    if (exito) {
        for (int y = 0; y < alto && exito; y++) {
            for (int x = 0; x < ancho; x++) {
                // El valor del píxel es el índice en la paleta (degradado horizontal)
                uint8_t indice_gris = (uint8_t)((double)x / ancho * 255.0);
                fila_indices[x] = indice_gris;
            }
            if (fwrite(fila_indices, 1, tamano_fila, archivo) != tamano_fila) {
                exito = false;
            }
        }
    }

    // --- Limpieza ---
    if (fila_indices != NULL) {
        free(fila_indices);
    }
    if (archivo != NULL) {
        fclose(archivo);
    }

    return exito;
}

Poniéndolo todo junto

Para compilar y ejecutar los ejemplos, podemos usar una función main como la siguiente:

1
2
3
4
5
6
7
8
9
10
11
int main() {
    if (crear_degradado_bmp_24bit("degradado_24bit.bmp", 256, 256)) {
        printf("Imagen de 24 bits creada con éxito.\n");
    }

    if (crear_degradado_bmp_8bit("degradado_8bit.bmp", 256, 256)) {
        printf("Imagen de 8 bits creada con éxito.\n");
    }

    return 0;
}

Conclusión

Crear archivos BMP en C es un excelente ejercicio para entender cómo se estructuran los datos a bajo nivel y cómo interactuar con archivos binarios. Aunque hemos cubierto los casos más comunes (24 y 8 bits sin compresión), el formato BMP es más versátil y soporta otras profundidades de color y tipos de compresión.

Al escribir código que maneja formatos de archivo, es vital prestar atención a los detalles como el padding, el orden de los bytes (endianness) y seguir las especificaciones del formato al pie de la letra. Afortunadamente, el formato BMP utiliza el orden little-endian, el mismo que usan las arquitecturas x86 y ARM modernas, por lo que generalmente no necesitamos preocuparnos por la conversión del orden de bytes.