Récemment, j’ai eu besoin de générer des écrans à afficher sur une gameboy advance. On pourrait se dire « oui, c’est facile, tu n’as qu’à faire chauffer imagemagick pour les conversions », mais ce génial outil ne supporte pas le mode d’affichage de la console. En effet, Nintendo a utilisé l’espace de couleur RGB15 Big endian (tant qu’à faire, c’est plus amusant).
Du coup, j’ai le petit programme suivant en C89 pour l’occasion. Il permet de convertir une image bitmap de dimensions 240×160 (résolution de la GBA) avec des couleurs dans l’espace RGB24 vers un fichier RAW (les pixels écrits en dur dans le fichier sans en-tête) encodé avec le bon espace (le format binaire est décrit dans les sources).
#include <stdio.h> #include <stdlib.h> #include <string.h> /* Screen size for a GBA */ #define GBA_WIDTH 240 #define GBA_HEIGHT 160 typedef unsigned char uint8; typedef unsigned short uint16; const int BmpHeaderSize = 54; /* BMP file info */ typedef struct { char ID[2]; long FileSize; long RESERVED; long BitmapDataOffset; long BitmapHeaderSize; long Width; long Height; short NrOfPlanes; short BitsPerPixel; long Compression; long BitmapDataSize; long HorizontalResolution; long VerticalResolution; long Colors; long ImportantColors; } BmpHeader; typedef struct { uint8 lsb; uint8 msb; } TwoBytes; /* Convert 24bit RGB to 15BPP Big endian format */ TwoBytes rgb_to_gba_be ( uint8 r, uint8 g, uint8 b) { TwoBytes resp; /* GBA Pixel are Big endian and described as follow : * * -------LSB------- -------MSB------- * Color G G G B B B B B 0 R R R R R G G * Order 2 1 0 4 3 2 1 0 X 4 3 2 1 0 4 3 * */ /* Initialize pixel */ resp.msb = 0; resp.lsb = 0; /* Red component */ resp.msb = resp.msb | ( (r>>1) & 0x7C ) ; /* Green component */ resp.msb = resp.msb | ( (g>>6) & 0x03 ) ; resp.lsb = resp.lsb | ( (g<<5) & 0xE0 ) ; /* Blue component */ resp.lsb = resp.lsb | ( (b>>3) & 0x1F ) ; return resp; } int main( int argc, char** argv) { FILE *pInFile, *pOutRaw; BmpHeader* imgProps = NULL; uint8 r=0,g=0,b=0; TwoBytes gbapix; int line, col; long i; const long image_sz = GBA_HEIGHT * GBA_WIDTH * 2; /* Check args */ if( argc != 3 ) { printf( "Usage: %s infile outfile\n", argv[0]); exit(1); } /* Open source file */ pInFile = (FILE *) fopen( argv[1], "rb"); if( pInFile == NULL ) { printf( "Unable to open input file '%s' for reading.\n", argv[1]); exit(1); } /* Open target file */ pOutRaw = (FILE *) fopen( argv[2], "wb"); if( pOutRaw == NULL ) { printf( "Unable to open output file '%s' for writing.\n", argv[2]); fclose(pInFile); exit(2); } /* Check source file */ imgProps = (BmpHeader*)malloc(sizeof(BmpHeader)); fread( &(imgProps->ID[0]), 1, 1, pInFile); fread( &(imgProps->ID[1]), 1, 1, pInFile); fread( &(imgProps->FileSize), 4, 1, pInFile); fread( &(imgProps->RESERVED), 4, 1, pInFile); fread( &(imgProps->BitmapDataOffset), 4, 1, pInFile); fread( &(imgProps->BitmapHeaderSize), 4, 1, pInFile); fread( &(imgProps->Width), 4, 1, pInFile); fread( &(imgProps->Height), 4, 1, pInFile); fread( &(imgProps->NrOfPlanes), 2, 1, pInFile); fread( &(imgProps->BitsPerPixel), 2, 1, pInFile); fread( &(imgProps->Compression), 4, 1, pInFile); fread( &(imgProps->BitmapDataSize), 4, 1, pInFile); fread( &(imgProps->HorizontalResolution), 4, 1, pInFile); fread( &(imgProps->VerticalResolution), 4, 1, pInFile); fread( &(imgProps->Colors), 4, 1, pInFile); fread( &(imgProps->ImportantColors), 4, 1, pInFile); if( imgProps->ID[0] != 'B' || imgProps->ID[1] != 'M' || imgProps->Width != 240 || imgProps->Height != 160 || imgProps->Compression != 0 || imgProps->BitsPerPixel != 24 ) { printf("*** Invalid input image! ***\n"); printf("Signature: '%c%c' Expected: 'BM'.\n", imgProps->ID[0], imgProps->ID[1] ); printf("Width: %ld Expected: %d\n", imgProps->Width, GBA_WIDTH); printf("Height: %ld Expected: %d\n", imgProps->Height, GBA_HEIGHT); printf("Compression: %s Expected: no\n", (imgProps->Compression != 0)?"yes":"no"); printf("Color depth: %hd Expected: %d\n", imgProps->BitsPerPixel , 24); fclose(pInFile); fclose(pOutRaw); exit(3); } /* Fill the output with the appropriate size */ fseek( pOutRaw, 0, SEEK_SET); for ( i=0; i<image_sz; ++i) fputc( 0x00, pOutRaw); fseek( pOutRaw, 0, SEEK_SET); /* Convert image and write to target */ fseek( pInFile, BmpHeaderSize, SEEK_SET); for( line=0; line<GBA_HEIGHT ; ++line) { fseek( pOutRaw, (GBA_HEIGHT-1-line)*GBA_WIDTH*2, SEEK_SET); for( col=0; col<GBA_WIDTH ; ++col) { b=fgetc( pInFile); g=fgetc( pInFile); r=fgetc( pInFile); gbapix = rgb_to_gba_be ( r, g, b); fputc( gbapix.lsb, pOutRaw); fputc( gbapix.msb, pOutRaw); } } fclose(pInFile); fclose(pOutRaw); free(imgProps); return 0; }
Le code travaille ligne à ligne (attention au format BMP qui stocke les lignes dans l’ordre inversé!), et convertit chaque pixel à la volée. Il est à noter que ce code a été conçu pour mon usage spécifique (vérification du format du fichier et de la taille). La taille prise en charge peut être rendue dynamique moyennant un peu de code supplémentaire. Si ça peut vous être utile, servez-vous et happy hacking ^.^