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 |
|
|
Tamaño |
|
El tamaño en bytes del archivo |
Reservado |
|
|
Reservado |
|
|
Offset |
|
|
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 |
|
|
Ancho |
|
El ancho de la imagen |
Alto |
|
El alto de la imagen |
Planos |
|
|
Bits de color |
|
|
Compresión |
|
|
Tamaño de imagen |
|
|
Resolución X |
|
|
Resolución Y |
|
|
Tablas de color |
|
|
Colores importantes |
|
|
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