/* 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; } static rgbdata_t *ImagePack( void ) { rgbdata_t *pack; // clear any force flags image.force_flags = 0; if( image.cubemap && image.num_sides != 6 ) { // this never can happen, just in case return NULL; } pack = Mem_Calloc( host.imagepool, sizeof( *pack )); 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 ================ */ static qboolean FS_AddSideToPack( 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 // NOTE: This is super incorrect for compressed images. // No idea why it was needed // 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; } static const loadpixformat_t *Image_GetLoadFormatForExtension( const char *ext ) { const loadpixformat_t *format; if( !COM_CheckStringEmpty( ext )) return NULL; for( format = image.loadformats; format->formatstring; format++ ) { if( !Q_stricmp( ext, format->ext )) return format; } return NULL; } static qboolean Image_ProbeLoadBuffer_( const loadpixformat_t *fmt, const char *name, const byte *buf, size_t size, int override_hint ) { if( override_hint > 0 ) image.hint = override_hint; else image.hint = fmt->hint; return fmt->loadfunc( name, buf, size ); } static qboolean Image_ProbeLoadBuffer( const loadpixformat_t *fmt, const char *name, const byte *buf, size_t size, int override_hint ) { if( size <= 0 ) return false; // bruteforce all loaders if( !fmt ) { for( fmt = image.loadformats; fmt->formatstring; fmt++ ) { if( Image_ProbeLoadBuffer_( fmt, name, buf, size, override_hint )) return true; } return false; } return Image_ProbeLoadBuffer_( fmt, name, buf, size, override_hint ); } static qboolean Image_ProbeLoad_( const loadpixformat_t *fmt, const char *name, const char *suffix, int override_hint ) { qboolean success = false; fs_offset_t filesize; string path; byte *f; Q_snprintf( path, sizeof( path ), fmt->formatstring, name, suffix, fmt->ext ); f = FS_LoadFile( path, &filesize, false ); if( f ) { success = Image_ProbeLoadBuffer( fmt, path, f, filesize, override_hint ); Mem_Free( f ); } return success; } static qboolean Image_ProbeLoad( const loadpixformat_t *fmt, const char *name, const char *suffix, int override_hint ) { if( !fmt ) { // bruteforce all formats to allow implicit extension for( fmt = image.loadformats; fmt->formatstring; fmt++ ) { if( Image_ProbeLoad_( fmt, name, suffix, override_hint )) return true; } return false; } return Image_ProbeLoad_( fmt, name, suffix, override_hint ); } /* ================ 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 loadname; int i; const loadpixformat_t *extfmt; const cubepack_t *cmap; Q_strncpy( loadname, filename, sizeof( loadname )); Image_Reset(); // clear old image // we needs to compare file extension with list of supported formats // and be sure what is real extension, not a filename with dot if(( extfmt = Image_GetLoadFormatForExtension( ext ))) COM_StripExtension( loadname ); // special mode: skip any checks, load file from buffer if( filename[0] == '#' && buffer && size ) goto load_internal; if( Image_ProbeLoad( extfmt, loadname, "", -1 )) return ImagePack(); // check all cubemap sides with package suffix for( cmap = load_cubemap; cmap && cmap->type; cmap++ ) { for( i = 0; i < 6; i++ ) { if( Image_ProbeLoad( extfmt, loadname, cmap->type[i].suf, cmap->type[i].hint )) { FS_AddSideToPack( cmap->type[i].flags ); } 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 that 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: if( buffer && size ) { if( Image_ProbeLoadBuffer( extfmt, loadname, buffer, size, -1 )) return ImagePack(); } if( loadname[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 = !COM_CheckStringEmpty( ext ); 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_snprintf( path, sizeof( 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_snprintf( path, sizeof( 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++ ) { qboolean ret; char name[MAX_VA_STRING]; Q_snprintf( name, sizeof( name ), "test_gen.%s", extensions[i] ); // test saving 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 */