/* img_main.c - load & save various image formats Copyright (C) 2007 Uncle Mike This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. */ #include #include "imagelib.h" // global image variables imglib_t image; typedef struct suffix_s { const char *suf; uint flags; side_hint_t hint; } suffix_t; static const suffix_t skybox_qv1[6] = { { "ft", IMAGE_FLIP_X, CB_HINT_POSX }, { "bk", IMAGE_FLIP_Y, CB_HINT_NEGX }, { "up", IMAGE_ROT_90, CB_HINT_POSZ }, { "dn", IMAGE_ROT_90, CB_HINT_NEGZ }, { "rt", IMAGE_ROT_90, CB_HINT_POSY }, { "lf", IMAGE_ROT270, CB_HINT_NEGY }, }; static const suffix_t skybox_qv2[6] = { { "_ft", IMAGE_FLIP_X, CB_HINT_POSX }, { "_bk", IMAGE_FLIP_Y, CB_HINT_NEGX }, { "_up", IMAGE_ROT_90, CB_HINT_POSZ }, { "_dn", IMAGE_ROT_90, CB_HINT_NEGZ }, { "_rt", IMAGE_ROT_90, CB_HINT_POSY }, { "_lf", IMAGE_ROT270, CB_HINT_NEGY }, }; static const suffix_t cubemap_v1[6] = { { "px", 0, CB_HINT_POSX }, { "nx", 0, CB_HINT_NEGX }, { "py", 0, CB_HINT_POSY }, { "ny", 0, CB_HINT_NEGY }, { "pz", 0, CB_HINT_POSZ }, { "nz", 0, CB_HINT_NEGZ }, }; typedef struct cubepack_s { const char *name; // just for debug const suffix_t *type; } cubepack_t; static const cubepack_t load_cubemap[] = { { "3Ds Sky1", skybox_qv1 }, { "3Ds Sky2", skybox_qv2 }, { "3Ds Cube", cubemap_v1 }, { NULL, NULL }, }; // soul of ImageLib - table of image format constants const bpc_desc_t PFDesc[] = { { PF_UNKNOWN, "raw", 0x1908, 0 }, { PF_INDEXED_24, "pal 24", 0x1908, 1 }, { PF_INDEXED_32, "pal 32", 0x1908, 1 }, { PF_RGBA_32, "RGBA 32",0x1908, 4 }, { PF_BGRA_32, "BGRA 32",0x80E1, 4 }, { PF_RGB_24, "RGB 24", 0x1908, 3 }, { PF_BGR_24, "BGR 24", 0x80E0, 3 }, { PF_LUMINANCE, "LUM 8", 0x1909, 1 }, { PF_DXT1, "DXT 1", 0x83F1, 4 }, { PF_DXT3, "DXT 3", 0x83F2, 4 }, { PF_DXT5, "DXT 5", 0x83F3, 4 }, { PF_ATI2, "ATI 2", 0x8837, 4 }, }; void Image_Reset( void ) { // reset global variables image.width = image.height = image.depth = 0; image.source_width = image.source_height = 0; image.source_type = image.num_mips = 0; image.num_sides = image.flags = 0; image.encode = DXT_ENCODE_DEFAULT; image.type = PF_UNKNOWN; image.fogParams[0] = 0; image.fogParams[1] = 0; image.fogParams[2] = 0; image.fogParams[3] = 0; // pointers will be saved with prevoius picture struct // don't care about it image.palette = NULL; image.cubemap = NULL; image.rgba = NULL; image.ptr = 0; image.size = 0; } rgbdata_t *ImagePack( void ) { rgbdata_t *pack = Mem_Calloc( host.imagepool, sizeof( rgbdata_t )); // clear any force flags image.force_flags = 0; if( image.cubemap && image.num_sides != 6 ) { // this never be happens, just in case FS_FreeImage( pack ); return NULL; } if( image.cubemap ) { image.flags |= IMAGE_CUBEMAP; pack->buffer = image.cubemap; pack->width = image.source_width; pack->height = image.source_height; pack->type = image.source_type; pack->size = image.size * image.num_sides; } else { pack->buffer = image.rgba; pack->width = image.width; pack->height = image.height; pack->depth = image.depth; pack->type = image.type; pack->size = image.size; } // copy fog params pack->fogParams[0] = image.fogParams[0]; pack->fogParams[1] = image.fogParams[1]; pack->fogParams[2] = image.fogParams[2]; pack->fogParams[3] = image.fogParams[3]; pack->flags = image.flags; pack->numMips = image.num_mips; pack->palette = image.palette; pack->encode = image.encode; return pack; } /* ================ FS_AddSideToPack ================ */ qboolean FS_AddSideToPack( const char *name, int adjust_flags ) { byte *out, *flipped; qboolean resampled = false; // first side set average size for all cubemap sides! if( !image.cubemap ) { image.source_width = image.width; image.source_height = image.height; image.source_type = image.type; } // keep constant size, render.dll expecting it image.size = image.source_width * image.source_height * 4; // mixing dds format with any existing ? if( image.type != image.source_type ) return false; // flip image if needed flipped = Image_FlipInternal( image.rgba, &image.width, &image.height, image.source_type, adjust_flags ); if( !flipped ) return false; // try to reasmple dxt? if( flipped != image.rgba ) image.rgba = Image_Copy( image.size ); // resampling image if needed out = Image_ResampleInternal((uint *)image.rgba, image.width, image.height, image.source_width, image.source_height, image.source_type, &resampled ); if( !out ) return false; // try to reasmple dxt? if( resampled ) image.rgba = Image_Copy( image.size ); image.cubemap = Mem_Realloc( host.imagepool, image.cubemap, image.ptr + image.size ); memcpy( image.cubemap + image.ptr, image.rgba, image.size ); // add new side Mem_Free( image.rgba ); // release source buffer image.ptr += image.size; // move to next image.num_sides++; // bump sides count return true; } /* ================ FS_LoadImage loading and unpack to rgba any known image ================ */ rgbdata_t *FS_LoadImage( const char *filename, const byte *buffer, size_t size ) { const char *ext = COM_FileExtension( filename ); string path, loadname, sidename; qboolean anyformat = true; int i; fs_offset_t filesize = 0; const loadpixformat_t *format; const cubepack_t *cmap; byte *f; Q_strncpy( loadname, filename, sizeof( loadname )); Image_Reset(); // clear old image if( Q_stricmp( ext, "" )) { // we needs to compare file extension with list of supported formats // and be sure what is real extension, not a filename with dot for( format = image.loadformats; format && format->formatstring; format++ ) { if( !Q_stricmp( format->ext, ext )) { COM_StripExtension( loadname ); anyformat = false; break; } } } // special mode: skip any checks, load file from buffer if( filename[0] == '#' && buffer && size ) goto load_internal; // now try all the formats in the selected list for( format = image.loadformats; format && format->formatstring; format++) { if( anyformat || !Q_stricmp( ext, format->ext )) { Q_sprintf( path, format->formatstring, loadname, "", format->ext ); image.hint = format->hint; f = FS_LoadFile( path, &filesize, false ); if( f && filesize > 0 ) { if( format->loadfunc( path, f, filesize )) { Mem_Free( f ); // release buffer return ImagePack(); // loaded } else Mem_Free( f ); // release buffer } } } // check all cubemap sides with package suffix for( cmap = load_cubemap; cmap && cmap->type; cmap++ ) { for( i = 0; i < 6; i++ ) { // for support mixed cubemaps e.g. sky_ft.bmp, sky_rt.tga // NOTE: all loaders must keep sides in one format for all for( format = image.loadformats; format && format->formatstring; format++ ) { if( anyformat || !Q_stricmp( ext, format->ext )) { Q_sprintf( path, format->formatstring, loadname, cmap->type[i].suf, format->ext ); image.hint = (image_hint_t)cmap->type[i].hint; // side hint f = FS_LoadFile( path, &filesize, false ); if( f && filesize > 0 ) { // this name will be used only for tell user about problems if( format->loadfunc( path, f, filesize )) { Q_snprintf( sidename, sizeof( sidename ), "%s%s.%s", loadname, cmap->type[i].suf, format->ext ); if( FS_AddSideToPack( sidename, cmap->type[i].flags )) // process flags to flip some sides { Mem_Free( f ); break; // loaded } } Mem_Free( f ); } } } if( image.num_sides != i + 1 ) // check side { // first side not found, probably it's not cubemap // it contain info about image_type and dimensions, don't generate black cubemaps if( !image.cubemap ) break; // Mem_Alloc already filled memblock with 0x00, no need to do it again image.cubemap = Mem_Realloc( host.imagepool, image.cubemap, image.ptr + image.size ); image.ptr += image.size; // move to next image.num_sides++; // merge counter } } // make sure what all sides is loaded if( image.num_sides != 6 ) { // unexpected errors ? if( image.cubemap ) Mem_Free( image.cubemap ); Image_Reset(); } else break; } if( image.cubemap ) return ImagePack(); // all done load_internal: for( format = image.loadformats; format && format->formatstring; format++ ) { if( anyformat || !Q_stricmp( ext, format->ext )) { image.hint = format->hint; if( buffer && size > 0 ) { if( format->loadfunc( loadname, buffer, size )) return ImagePack(); // loaded } } } if( filename[0] != '#' ) Con_Reportf( S_WARN "FS_LoadImage: couldn't load \"%s\"\n", loadname ); // clear any force flags image.force_flags = 0; return NULL; } /* ================ Image_Save writes image as any known format ================ */ qboolean FS_SaveImage( const char *filename, rgbdata_t *pix ) { const char *ext = COM_FileExtension( filename ); qboolean anyformat = !Q_stricmp( ext, "" ) ? true : false; string path, savename; const savepixformat_t *format; if( !pix || !pix->buffer || anyformat ) { // clear any force flags image.force_flags = 0; return false; } Q_strncpy( savename, filename, sizeof( savename )); COM_StripExtension( savename ); // remove extension if needed if( pix->flags & (IMAGE_CUBEMAP|IMAGE_SKYBOX)) { size_t realSize = pix->size; // keep real pic size byte *picBuffer; // to avoid corrupt memory on free data const suffix_t *box; int i; if( pix->flags & IMAGE_SKYBOX ) box = skybox_qv1; else if( pix->flags & IMAGE_CUBEMAP ) box = cubemap_v1; else { // clear any force flags image.force_flags = 0; return false; // do not happens } pix->size /= 6; // now set as side size picBuffer = pix->buffer; // save all sides seperately for( format = image.saveformats; format && format->formatstring; format++ ) { if( !Q_stricmp( ext, format->ext )) { for( i = 0; i < 6; i++ ) { Q_sprintf( path, format->formatstring, savename, box[i].suf, format->ext ); if( !format->savefunc( path, pix )) break; // there were errors pix->buffer += pix->size; // move pointer } // restore pointers pix->size = realSize; pix->buffer = picBuffer; // clear any force flags image.force_flags = 0; return ( i == 6 ); } } } else { for( format = image.saveformats; format && format->formatstring; format++ ) { if( !Q_stricmp( ext, format->ext )) { Q_sprintf( path, format->formatstring, savename, "", format->ext ); if( format->savefunc( path, pix )) { // clear any force flags image.force_flags = 0; return true; // saved } } } } // clear any force flags image.force_flags = 0; return false; } /* ================ Image_FreeImage free RGBA buffer ================ */ void FS_FreeImage( rgbdata_t *pack ) { if( !pack ) return; if( pack->buffer ) Mem_Free( pack->buffer ); if( pack->palette ) Mem_Free( pack->palette ); Mem_Free( pack ); } /* ================ FS_CopyImage make an image copy ================ */ rgbdata_t *FS_CopyImage( rgbdata_t *in ) { rgbdata_t *out; int palSize = 0; if( !in ) return NULL; out = Mem_Malloc( host.imagepool, sizeof( rgbdata_t )); *out = *in; switch( in->type ) { case PF_INDEXED_24: palSize = 768; break; case PF_INDEXED_32: palSize = 1024; break; } if( palSize ) { out->palette = Mem_Malloc( host.imagepool, palSize ); memcpy( out->palette, in->palette, palSize ); } if( in->size ) { out->buffer = Mem_Malloc( host.imagepool, in->size ); memcpy( out->buffer, in->buffer, in->size ); } return out; } #if XASH_ENGINE_TESTS #include "tests.h" static void GeneratePixel( byte *pix, uint i, uint j, uint w, uint h, qboolean genAlpha ) { double x = ( j / (double)w ) - 0.5; double y = ( i / (double)h ) - 0.5; double d = sqrt( x * x + y * y ); pix[0] = (byte)(( sin( d * 30.0 ) + 1.0 ) * 126 ); pix[1] = (byte)(( sin( d * 27.723 ) + 1.0 ) * 126 ); pix[2] = (byte)(( sin( d * 42.41 ) + 1.0 ) * 126 ); pix[3] = genAlpha ? (byte)(( cos( d * 2.0 ) + 1.0 ) * 126 ) : 255; } static void Test_CheckImage( const char *name, rgbdata_t *rgb ) { rgbdata_t *load; // test reading load = FS_LoadImage( name, NULL, 0 ); TASSERT( load->width == rgb->width ) TASSERT( load->height == rgb->height ) TASSERT( load->type == rgb->type ) TASSERT( ( load->flags & rgb->flags ) != 0 ) TASSERT( load->size == rgb->size ) TASSERT( memcmp(load->buffer, rgb->buffer, rgb->size ) == 0 ) Mem_Free( load ); } void Test_RunImagelib( void ) { rgbdata_t rgb = { 0 }; byte *buf; const char *extensions[] = { "tga", "png", "bmp" }; uint i, j; Image_Setup(); // generate image rgb.width = 256; rgb.height = 512; rgb.type = PF_RGBA_32; rgb.flags = IMAGE_HAS_ALPHA; rgb.size = rgb.width * rgb.height * 4; buf = rgb.buffer = Z_Malloc( rgb.size ); for( i = 0; i < rgb.height; i++ ) { for( j = 0; j < rgb.width; j++ ) { GeneratePixel( buf, i, j, rgb.width, rgb.height, true ); buf += 4; } } for( i = 0; i < sizeof(extensions) / sizeof(extensions[0]); i++ ) { const char *name = va( "test_gen.%s", extensions[i] ); // test saving qboolean ret = FS_SaveImage( name, &rgb ); Con_Printf( "Checking if we can save images in '%s' format...\n", extensions[i] ); ASSERT(ret == true); // test reading Con_Printf( "Checking if we can read images in '%s' format...\n", extensions[i] ); Test_CheckImage( name, &rgb ); } Z_Free( rgb.buffer ); } #define IMPLEMENT_IMAGELIB_FUZZ_TARGET( export, target ) \ int EXPORT export( const uint8_t *Data, size_t Size ) \ { \ rgbdata_t *rgb; \ host.type = HOST_NORMAL; \ Memory_Init(); \ Image_Init(); \ if( target( "#internal", Data, Size )) \ { \ rgb = ImagePack(); \ FS_FreeImage( rgb ); \ } \ Image_Shutdown(); \ return 0; \ } \ IMPLEMENT_IMAGELIB_FUZZ_TARGET( Fuzz_Image_LoadBMP, Image_LoadBMP ) IMPLEMENT_IMAGELIB_FUZZ_TARGET( Fuzz_Image_LoadPNG, Image_LoadPNG ) IMPLEMENT_IMAGELIB_FUZZ_TARGET( Fuzz_Image_LoadDDS, Image_LoadDDS ) IMPLEMENT_IMAGELIB_FUZZ_TARGET( Fuzz_Image_LoadTGA, Image_LoadTGA ) #endif /* XASH_ENGINE_TESTS */