Ejercicio obligatorio 5

Fecha de entrega: Domingo 7 de febrero

Introducción

El formato de Windows Bitmap (BMP) es un formato binario con muchas limitaciones pero sencillo de implementar y entender y un formato ampliamente difundido.

Si bien el mismo tiene muchos modos de operación, vamos a centrarnos en la versión de 24 bits de color, no comprimida, sin paleta, etc. para simplificar sus particularidades.

Un archivo BMP consta de tres secciones:

Sección

Tamaño

Encabezado de archivo

14

Encabezado de imagen

40

Pixeles

--

Cada una de estas secciones tiene un formato específico.

Si bien la mayor parte de los campos del archivo son de un solo byte, hay números almacenados en más de un byte y en este caso esos números son números signados almacenados según la convención little-endian. Es decir, si un campo numérico contuviera la secuencia {0xAA, 0xBB, 0xCC, 0xDD} el número representado sería el 0xDDCCBBAA.

Encabezado de archivo

El encabezado de archivo consiste en la siguiente secuencia:

Campo

Tipo

Valor

Tipo

char[2]

"BM"

Tamaño

int32_t

El tamaño en bytes del archivo

Reservado

int16_t

0

Reservado

int16_t

0

Offset

int32_t

54

Como se ve, el único valor no definido del formato es el tamaño en bytes del archivo. El parámetro del offset indica en qué posición comienza la tabla de píxeles en el archivo, pero siendo que estamos usando un formato simplificado donde el encabezado de imagen siempre mide 40 bytes entonces este valor queda fijo.

El tamaño en bytes del archivo puede calcularse previo a la escritura en base al ancho y alto de la imagen, lo abordaremos más adelante.

Encabezado de imagen

El encabezado de imagen consiste en la siguiente secuencia:

Campo

Tipo

Valor

Tamaño

int32_t

40

Ancho

int32_t

El ancho de la imagen

Alto

int32_t

El alto de la imagen

Planos

int16_t

1

Bits de color

int16_t

24

Compresión

int32_t

0

Tamaño de imagen

int32_t

0

Resolución X

int32_t

0

Resolución Y

int32_t

0

Tablas de color

int32_t

0

Colores importantes

int32_t

0

Como se ve, con todas las simiplificaciones adoptadas, los únicos dos parámetros variables son el ancho y el alto de la imagen. Así como generaremos imágenes no comprimidas, en 24 bits de color y sin paleta de colores al leer un archivo es importante validar que tanto los bits de color, como la compresión como las tablas de color estén en los valores indicados; si no, no sabemos cómo procesar ese archivo.

Cabe remarcar que la altura podría llegar a ser un valor negativo.

Pixeles

En la sección de los píxeles todos los valores ocupan un byte, es decir, pueden considerarse de tipo uint8_t. Como estamos usando RGB24 cada pixel se representará con 3 bytes consecutivos.

Ahora bien, como el formato es little-endian, si se leyeran los bytes del RGB de a un byte por vez los mismos estarán en orden BGR, es decir, en el orden inverso. Tanto al leer como al escribir lo haremos, por simplicidad, de a un byte por vez y el orden será azul, verde, rojo.

Los pixeles se almacenan según scan lines donde cada scan line es una fila de la imagen recorrida de izquierda a derecha. Ahora bien, cada una de las scan lines tiene que medir un múltiplo de 4 bytes por lo que se completarán con cero los bytes restantes hasta completar los 4.

Por ejemplo, si una imagen tuviera 13 pixeles de ancho, estos 13 pixeles se pueden representar en 39 bytes, pero como eso no es múltiplo de 4 al final de los pixeles se agregará un byte adicional en cero para completar 40 bytes.

Notar que el largo de la scan line puede computarse mirando cuánto falta para que sea múltiplo de 4 la operación de multiplicar por 3 el ancho de la imagen.

La sección de píxeles consistirá en una secuencia de tantas scan lines como altura tenga la imagen. Si el parámetro de altura se hubiera indicado como un número positivo las scan lines están ordenadas de abajo hacia arriba, en cambio si fuera negativo estarán ordenadas de arriba hacia abajo (esta última es la convención que veníamos usando en PGM y en la que almacenamos en nuestro imagen_t).

Tamaño del archivo

Ahora conociendo la especificación completa podemos ver que el tamaño del archivo estará dado por el tamaño de los encabezados, que son 14 para el encabezado de archivo y 40 para el encabezado de imagen, y que la sección de píxeles será el tamaño de la scan line multiplicado por el alto de la imagen.

Entonces, el tamaño será: 14 + 40 + alto * scanline. Donde la scanline será ancho * 3 más lo que haga falta para que ese número sea múltiplo de 4.

Trabajo

pixel_t e imagen_t

Nuestro pixel_t del EJ4 y nuestro imagen_t del EJ3 a partir de ahora deben ser considerados como TDAs.

De momento tenemos las siguientes primitivas ya implementadas:

TDA pixel_t:

  • pixel_t pixel_desde_rgb(componente_t rojo, componente_t verde, componente_t azul);

  • pixel_t pixel_desde_hsv(short h, float s, float v);

  • componente_t pixel_get_rojo(pixel_t p);

  • componente_t pixel_get_verde(pixel_t p);

  • componente_t pixel_get_azul(pixel_t p);

  • void pixel_a_hsv(pixel_t p, short *h, float *s, float *v);

  • pixel_t pixel_desde_nombre(const char *nombre);

TDA imagen_t:

  • imagen_t *imagen_leer_stdin();

  • void imagen_destruir(imagen_t *imagen);

  • void imagen_escribir(const imagen_t *imagen);

  • imagen_t *imagen_recortar(const imagen_t *imagen, size_t x0, size_t y0, size_t ancho, size_t alto);

  • imagen_t *imagen_clonar(const imagen_t *imagen);

Manejo de PPM

Notar que las primitivas de imagen_leer_stdin(); y imagen_escribir(); fueron implementadas en el EJ3, donde se trabajó sobre imágenes PGM en grises. En el EJ4 se implementaron funciones de lectura y escritura de archivos PPM pero en este caso se hicieron sobre matrices estáticas en vez de sobre el TDA imagen_t.

Se pide modificar las funciones ya implementadas para implementar la primitiva imagen_t *imagen_leer_PPM(FILE *f); la misma recibirá un descriptor de archivo f abierto en modo texto (que podrá eventualmente ser stdin) y cargará en memoria una imagen PPM desde ese archivo.

Se pide modificar las funciones ya implementadas para implementar la primitiva void imagen_escribir_PPM(const imagen_t *imagen, FILE *f); la misma recibirá un descriptor de archivo f abierto en modo texto (que podrá eventualmente ser stdot) y escribirá la imagen en dicho archivo en formato PPM.

Las primitivas imagen_leer_stdin(); e imagen_escribir(); pasan a ser obsoletas y se retiran del TDA.

Endianness

Escribir una función void escribir_int16_little_endian(FILE *f, int16_t v); que reciba un archivo f y un entero v de 16 bits y lo escriba en el archivo en formato little-endian.

Escribir una función void escribir_int32_little_endian(FILE *f, int32_t v); similar a la anterior pero que escriba un entero de 32 bits.

Nota

No puede asumirse que la plataforma es big/little-endian. Las funciones deben operar con los bytes a bajo nivel y ser portables a cualquier arquitectura.

Manejo de BMP

Agregarle al TDA imagen_t la primitiva void imagen_escribir_BMP(const imagen_t *imagen, FILE *f); que reciba un archivo f abierto en modo binario y escriba en el mismo la imagen en formato BMP.

Aplicación

Se pide implementar una aplicación que se ejecute como:

$ ./ppm2bmp [entrada.ppm] [salida.bmp]

que realice conversiones de PPM a BMP.

Ejemplo:

$ ./ppm2bmp gatito.ppm gatito.bmp

Convierte el archivo gatito.ppm de formato PPM en un archivo gatito.bmp de formato BMP.

Entrega

Deberá entregarse los códigos fuentes de los TDAs desarrollados, la aplicación y el archivo Makefile para compilar el proyecto.

El programa y cada módulo debe compilar correctamente con los flags:

-Wall -Werror -std=c99 -pedantic

Cada TDA debe contener su propio código fuente con su respectivo archivo de encabezado.

La entrega se realiza a través del sistema de entregas.

El ejercicio es de entrega individual.

Referencias

Formato BMP: http://www.dragonwins.com/domains/getteched/bmp/bmpfileformat.htm

Ejemplo 1: https://en.wikipedia.org/wiki/BMP_file_format#Example_1