diff --git a/source/lib/res/graphics/tex.cpp b/source/lib/res/graphics/tex.cpp index 114628a546..5c9c93c02c 100755 --- a/source/lib/res/graphics/tex.cpp +++ b/source/lib/res/graphics/tex.cpp @@ -21,164 +21,52 @@ #include #include - #include #include "lib.h" #include "../res.h" #include "tex.h" -#include "byte_order.h" +#include "tex_codec.h" -// supported formats: -//#define NO_DDS -//#define NO_TGA -//#define NO_BMP -//#define NO_PNG -#define NO_JP2 -//#define NO_RAW -//#define NO_JPG - - -#ifndef NO_JP2 -#include -#endif - -#ifndef NO_JPG -extern "C" { -# include "jpeglib.h" -// extensions -EXTERN(void) jpeg_mem_src(j_decompress_ptr cinfo, void* p, size_t size); -EXTERN(void) jpeg_vfs_dst(j_compress_ptr cinfo, Handle hf); -} -# if MSC_VERSION -# ifdef NDEBUG -# pragma comment(lib, "jpeg-6b.lib") -# else -# pragma comment(lib, "jpeg-6bd.lib") -# endif // #ifdef NDEBUG -# endif // #ifdef _MSV_VER -#endif // #ifndef NO_JPG - - -#ifndef NO_PNG -# if OS_WIN - // to avoid conflicts, windows.h must not be included. - // libpng pulls it in for WINAPI; we prevent the include - // and define that here. -# define _WINDOWS_ -# define WINAPI __stdcall -# define WINAPIV __cdecl - // different header name, too. -# include -# if MSC_VERSION -# ifdef NDEBUG -# pragma comment(lib, "libpng13.lib") -# else -# pragma comment(lib, "libpng13d.lib") -# endif // NDEBUG -# endif // MSC_VERSION -# else // i.e. !OS_WIN -# include -# endif // OS_WIN -#endif // NO_PNG - -// squelch "dtor / setjmp interaction" warnings. -// all attempts to resolve the underlying problem failed; apparently -// the warning is generated if setjmp is used at all in C++ mode. -// (png_decode has no code that would trigger ctors/dtors, nor are any -// called in its prolog/epilog code). -#if MSC_VERSION -# pragma warning(disable: 4611) -#endif - - -#define CODEC(name) { name##_fmt, name##_ext, name##_decode, name##_encode, #name} - -struct Codec -{ - // pointers aren't const, because the textures - // may have to be flipped in-place - see "texture orientation". - - // size is guaranteed to be >= 4. - // (usually enough to compare the header's "magic" field; - // anyway, no legitimate file will be smaller) - bool(*is_fmt)(const u8* p, size_t size); - bool(*is_ext)(const char* ext); - int(*decode)(TexInfo* t, const char* fn, u8* file, size_t file_size); - int(*encode)(TexInfo* t, const char* fn, u8* img, size_t img_size); - - // no get_output_size() function and central mem alloc / file write, - // because codecs that write via external lib or output compressed files - // don't know the output size beforehand. - - const char* name; -}; - - -////////////////////////////////////////////////////////////////////////////// -// -// image transformations -// -////////////////////////////////////////////////////////////////////////////// - -// TexInfo.flags indicate deviations from the standard image format -// (left-to-right, RGBA layout, up/down=global_orientation). -// -// we don't want to dump the burden of dealing with that on -// the app - that would affect all drawing code. -// instead, we convert as much as is convenient at load-time. -// -// supported transforms: BGR<->RGB, row flip. -// we don't bother providing a true S3TC aka DXTC codec - -// they are always passed unmodified to OpenGL, and decoding is complicated. -// -// converting is slow; in release builds, we should be using formats -// optimized for their intended use that don't require preprocessing. - - -// switch between top-down and bottom-up orientation. -// -// the default top-down is to match the Photoshop DDS plugin's output. -// DDS is the optimized format, so we don't want to have to flip that. -// notes: -// - there's no way to tell which orientation a DDS file has; -// we have to go with what the DDS encoder uses. -// - flipping DDS is possible without re-encoding; we'd have to shuffle -// around the pixel values inside the 4x4 blocks. -// -// the app can change orientation, e.g. to speed up loading -// "upside-down" formats, or to match OpenGL's bottom-up convention. - +// rationale for default: see tex_set_global_orientation static int global_orientation = TEX_TOP_DOWN; -void tex_set_global_orientation(int o) -{ - if(o == TEX_TOP_DOWN || o == TEX_BOTTOM_UP) - global_orientation = o; - else - debug_warn("tex_set_global_orientation: invalid param"); -} - +// handles BGR and row flipping in "plain" format (see below). +// +// called by codecs after they get their format-specific transforms out of +// the way. note that this approach requires several passes over the image, +// but is much easier to maintain than providing all<->all conversion paths. +// // somewhat optimized (loops are hoisted, cache associativity accounted for) -int transform(TexInfo* t, u8* img, int transforms) +static int plain_transform(Tex* t, uint new_format) { - // nothing to do - bail. - if(!transforms) - return 0; + // extract texture info + const int pending_transforms = t->flags ^ new_format; + const uint w = t->w, h = t->h, bpp = t->bpp, flags = t->flags; + u8* const img = tex_get_data(t); - const uint w = t->w, h = t->h, bpp_8 = t->bpp / 8; - const size_t pitch = w * bpp_8; + // sanity checks (not errors, we just can't handle these cases) + // .. unknown transform + if(pending_transforms & ~(TEX_BGR|TEX_ORIENTATION)) + return TEX_CODEC_CANNOT_HANDLE; + // .. img is not in "plain" format + if(tex_codec_validate_format(bpp, flags) != 0) + return TEX_CODEC_CANNOT_HANDLE; + // .. nothing to do + if(!pending_transforms) + return 0; // setup row source/destination pointers (simplifies outer loop) u8* dst = img; const u8* src = img; + const size_t pitch = w * bpp/8; ssize_t row_ofs = (ssize_t)pitch; - // avoid y*pitch multiply in row loop; instead, add row_ofs. + // avoid y*pitch multiply in row loop; instead, add row_ofs. void* clone_img = 0; // flipping rows (0,1,2 -> 2,1,0) - if(transforms & TEX_ORIENTATION) + if(pending_transforms & TEX_ORIENTATION) { // L1 cache is typically A2 => swapping in-place with a line buffer // leads to thrashing. we'll assume the whole texture*2 fits in cache, @@ -195,17 +83,19 @@ int transform(TexInfo* t, u8* img, int transforms) row_ofs = -(ssize_t)pitch; } - // no BGR convert necessary - if(!(transforms & TEX_BGR)) + if(!(pending_transforms & TEX_BGR)) + { for(uint y = 0; y < h; y++) { memcpy(dst, src, pitch); dst += pitch; src += row_ofs; } + } // RGB <-> BGR - else if(bpp_8 == 3) + else if(bpp == 24) + { for(uint y = 0; y < h; y++) { for(uint x = 0; x < w; x++) @@ -218,8 +108,10 @@ int transform(TexInfo* t, u8* img, int transforms) } src += row_ofs - pitch; // flip? previous row : stay } + } // RGBA <-> BGRA - else + else if(bpp == 32) + { for(uint y = 0; y < h; y++) { for(uint x = 0; x < w; x++) @@ -232,6 +124,7 @@ int transform(TexInfo* t, u8* img, int transforms) } src += row_ofs - pitch; // flip? previous row : stay } + } if(clone_img) mem_free(clone_img); @@ -240,1463 +133,129 @@ int transform(TexInfo* t, u8* img, int transforms) } -typedef const u8* RowPtr; -typedef RowPtr* RowArray; +//----------------------------------------------------------------------------- +// support routines for codecs +//----------------------------------------------------------------------------- -// allocate and set up rows to point into the given texture data. -// invert if transforms & TEX_ORIENTATION; caller is responsible for comparing -// a file format's orientation with the global setting (when decoding) or -// with the image format (when encoding). -// caller must free() rows when done. -static int alloc_rows(const u8* tex, size_t h, size_t pitch, - int transforms, RowArray& rows) +// should be a tight bound because we iterate this many times (for convenience) +static const uint MAX_CODECS = 8; +static const TexCodecVTbl* codecs[MAX_CODECS]; + +// add this vtbl to the codec list. called at NLSO init time by the +// TEX_CODEC_REGISTER in each codec file. note that call order and therefore +// order in the list is undefined, but since each codec only steps up if it +// can handle the given format, this is not a problem. +int tex_codec_register(const TexCodecVTbl* c) { + for(int i = 0; i < MAX_CODECS; i++) + { + // slot available + if(codecs[i] == 0) + { + codecs[i] = c; + return 0; // success + } + } + + // didn't find a free slot. + debug_warn("tex_codec_register: increase MAX_CODECS"); + return 0; // failure, but caller ignores return value +} + + +// allocate an array of row pointers that point into the given texture data. +// indicates whether the file format is top-down or +// bottom-up; the row array is inverted if necessary to match global +// orienatation. (this is more efficient than "transforming" later) +// used by PNG and JPG codecs; caller must free() rows when done. +int tex_codec_alloc_rows(const u8* data, size_t h, size_t pitch, + int file_orientation, RowArray& rows) +{ + debug_assert((file_orientation & ~TEX_ORIENTATION) == 0); + rows = (RowArray)malloc(h * sizeof(RowPtr)); if(!rows) return ERR_NO_MEM; - // rows are inverted; current position pointer counts backwards. - if(transforms & TEX_ORIENTATION) + // determine start position and direction + const bool flip = (file_orientation ^ global_orientation) != 0; + RowPtr pos = flip? data+pitch*(h-1) : data; + ssize_t add = flip? -(ssize_t)pitch : (ssize_t)pitch; + + for(size_t i = 0; i < h; i++) { - RowPtr pos = tex + pitch*h; - for(size_t i = 0; i < h; i++) - { - pos -= pitch; - rows[i] = pos; - } - } - // normal; count ahead. - else - { - RowPtr pos = tex; - for(size_t i = 0; i < h; i++) - { - rows[i] = pos; - pos += pitch; - } + rows[i] = pos; + pos += add; } return 0; } -////////////////////////////////////////////////////////////////////////////// - - -// shoehorn header and image into to file -// (it's faster to write in one shot). -static int write_img(const char* fn, const void* hdr, size_t hdr_size, - const void* img, size_t img_size) +int tex_codec_set_orientation(Tex* t, int file_orientation) { - const size_t file_size = hdr_size + img_size; - u8* file = (u8*)mem_alloc(file_size); - if(!file) - return ERR_NO_MEM; - - memcpy(file, hdr, hdr_size); - memcpy((char*)file+hdr_size, img, img_size); - - CHECK_ERR(vfs_store(fn, file, file_size, FILE_NO_AIO)); - mem_free(file); - return 0; + int pending_transforms = file_orientation ^ global_orientation; + int new_flags = t->flags ^ pending_transforms; + return plain_transform(t, new_flags); } -static int fmt_8_or_24_or_32(int bpp, int flags) +// check if the given texture format is acceptable: 8bpp grey, +// 24bpp color or 32bpp color+alpha (BGR / upside down are permitted). +// basically, this is the "plain" format understood by all codecs and +// tex_codec_plain_transform. +// return 0 if ok or a negative error code. +int tex_codec_validate_format(uint bpp, uint flags) { - const bool alpha = (flags & TEX_ALPHA) != 0; - const bool grey = (flags & TEX_GREY ) != 0; - const bool dxt = (flags & TEX_DXT ) != 0; + const bool alpha = (flags & TEX_ALPHA ) != 0; + const bool grey = (flags & TEX_GREY ) != 0; + const bool dxt = (flags & TEX_DXT ) != 0; + const bool mipmaps = (flags & TEX_MIPMAPS) != 0; - if(dxt) + if(dxt || mipmaps) return ERR_TEX_FMT_INVALID; - // if grey.. + // grey must be 8bpp without alpha, or it's invalid. if(grey) { - // and 8bpp / no alpha, it's ok. if(bpp == 8 && !alpha) return 0; - - // otherwise, it's invalid. return ERR_TEX_FMT_INVALID; } - // it's not grey. if(bpp == 24 && !alpha) return 0; - if(bpp == 32 && !alpha) + if(bpp == 32 && alpha) return 0; return ERR_TEX_FMT_INVALID; } -////////////////////////////////////////////////////////////////////////////// +//----------------------------------------------------------------------------- +// API +//----------------------------------------------------------------------------- + +// switch between top-down and bottom-up orientation. // -// DDS +// the default top-down is to match the Photoshop DDS plugin's output. +// DDS is the optimized format, so we don't want to have to flip that. +// notes: +// - there's no way to tell which orientation a DDS file has; +// we have to go with what the DDS encoder uses. +// - flipping DDS is possible without re-encoding; we'd have to shuffle +// around the pixel values inside the 4x4 blocks. // -////////////////////////////////////////////////////////////////////////////// - -#ifndef NO_DDS - - -// NOTE: the convention is bottom-up for DDS, but there's no way to tell. - -// defs modified from ddraw header - - -#pragma pack(push, 1) - -// DDPIXELFORMAT.dwFlags -#define DDPF_ALPHAPIXELS 0x00000001 - -typedef struct +// the app can change orientation, e.g. to speed up loading +// "upside-down" formats, or to match OpenGL's bottom-up convention. +void tex_set_global_orientation(int o) { - u32 dwSize; // size of structure (32) - u32 dwFlags; // indicates which fields are valid - u32 dwFourCC; // (DDPF_FOURCC) FOURCC code, "DXTn" - u32 dwReserved1[5]; // reserved -} -DDPIXELFORMAT; - -typedef struct -{ - u32 dwCaps[4]; -} -DDSCAPS2; - -// DDSURFACEDESC2.dwFlags -#define DDSD_HEIGHT 0x00000002 -#define DDSD_WIDTH 0x00000004 -#define DDSD_PIXELFORMAT 0x00001000 -#define DDSD_MIPMAPCOUNT 0x00020000 - -typedef struct -{ - u32 dwSize; // size of structure (124) - u32 dwFlags; // indicates which fields are valid - u32 dwHeight; // height of main image (pixels) - u32 dwWidth; // width of main image (pixels) - u32 dwLinearSize; // (DDSD_LINEARSIZE): total image size - u32 dwDepth; // (DDSD_DEPTH) vol. textures: vol. depth - u32 dwMipMapCount; // (DDSD_MIPMAPCOUNT) total # levels - u32 dwReserved1[11]; // reserved - DDPIXELFORMAT ddpfPixelFormat; // pixel format description of the surface - DDSCAPS2 ddsCaps; // direct draw surface capabilities - u32 dwReserved2; // reserved -} -DDSURFACEDESC2; - -#pragma pack(pop) - - -static inline bool dds_fmt(const u8* ptr, size_t size) -{ - UNUSED2(size); // size >= 4, we only need 4 bytes - - return *(u32*)ptr == FOURCC('D','D','S',' '); + debug_assert(o == TEX_TOP_DOWN || o == TEX_BOTTOM_UP); + global_orientation = o; } -static inline bool dds_ext(const char* ext) -{ - return !stricmp(ext, ".dds"); -} - - -static int dds_decode(TexInfo* t, const char* fn, u8* file, size_t file_size) -{ - const char* err = 0; - - const DDSURFACEDESC2* surf = (const DDSURFACEDESC2*)(file+4); - const size_t hdr_size = 4+sizeof(DDSURFACEDESC2); - - // make sure we can access all header fields - if(file_size < hdr_size) - { - err = "header not completely read"; -fail: - debug_printf("dds_decode: %s: %s\n", fn, err); - return ERR_CORRUPTED; - } - - const u32 sd_size = read_le32(&surf->dwSize); - const u32 sd_flags = read_le32(&surf->dwFlags); - const u32 h = read_le32(&surf->dwHeight); - const u32 w = read_le32(&surf->dwWidth); - const u32 img_size = read_le32(&surf->dwLinearSize); - u32 mipmaps = read_le32(&surf->dwMipMapCount); - const u32 pf_size = read_le32(&surf->ddpfPixelFormat.dwSize); - const u32 pf_flags = read_le32(&surf->ddpfPixelFormat.dwFlags); - const u32 fourcc = surf->ddpfPixelFormat.dwFourCC; - // compared against FOURCC, which takes care of endian conversion. - - // we'll use these fields; make sure they're present below. - // note: we can't guess image dimensions if not specified - - // the image isn't necessarily square. - const u32 sd_req_flags = DDSD_WIDTH | DDSD_HEIGHT | DDSD_PIXELFORMAT; - - // make sure fields that aren't indicated as valid are zeroed. - if(!(sd_flags & DDSD_MIPMAPCOUNT)) - mipmaps = 0; - - // MS DXTex tool doesn't set the required dwPitchOrLinearSize field - - // they can't even write out their own file format correctly. *sigh* - // we need to pass it to OpenGL; it's calculated from w, h, and bpp, - // which we determine from the pixel format. - int bpp = 0; - int flags = 0; - - if(pf_flags & DDPF_ALPHAPIXELS) - flags |= TEX_ALPHA; - - switch(fourcc) - { - case FOURCC('D','X','T','1'): - bpp = 4; - flags |= 1; // bits in TEX_DXT mask indicate format - break; - case FOURCC('D','X','T','3'): - bpp = 8; - flags |= 3; // " - break; - case FOURCC('D','X','T','5'): - bpp = 8; - flags |= 5; // " - break; - } - - if(mipmaps) - flags |= TEX_MIPMAPS; - - if(file_size < hdr_size + img_size) - err = "file size too small"; - if(w % 4 || h % 4) - err = "image dimensions not padded to S3TC block size"; - if(!w || !h) - err = "width or height = 0"; - if(bpp == 0) - err = "invalid pixel format (not DXT{1,3,5})"; - if((sd_flags & sd_req_flags) != sd_req_flags) - err = "missing one or more required fields (w, h, pixel format)"; - if(sizeof(DDPIXELFORMAT) != pf_size) - err = "DDPIXELFORMAT size mismatch"; - if(sizeof(DDSURFACEDESC2) != sd_size) - err = "DDSURFACEDESC2 size mismatch"; - - if(err) - goto fail; - - t->ofs = hdr_size; - t->w = w; - t->h = h; - t->bpp = bpp; - t->flags = flags; - return 0; -} - - -static int dds_encode(TexInfo* UNUSED(t), const char* UNUSED(fn), u8* UNUSED(img), size_t UNUSED(img_size)) -{ - return ERR_NOT_IMPLEMENTED; -} - -#endif // #ifndef NO_DDS - - -////////////////////////////////////////////////////////////////////////////// -// -// TGA -// -////////////////////////////////////////////////////////////////////////////// - -#ifndef NO_TGA - - -#pragma pack(push, 1) - -enum TgaImgType -{ - TGA_TRUE_COLOUR = 2, // uncompressed 24 or 32 bit direct RGB - TGA_GREY = 3 // uncompressed 8 bit direct greyscale -}; - -enum TgaImgDesc -{ - TGA_RIGHT_TO_LEFT = BIT(4), - TGA_TOP_DOWN = BIT(5), - TGA_BOTTOM_UP = 0 // opposite of TGA_TOP_DOWN -}; - -typedef struct -{ - u8 img_id_len; // 0 - no image identifier present - u8 colour_map_type; // 0 - no colour map present - u8 img_type; // see TgaImgType - u8 colour_map[5]; // unused - - u16 x_origin; // unused - u16 y_origin; // unused - - u16 w; - u16 h; - u8 bpp; // bits per pixel - - u8 img_desc; -} -TgaHeader; - -// TGA file: header [img id] [colour map] image data - - -#pragma pack(pop) - - -// the first TGA header doesn't have a magic field; -// we can only check if the first 4 bytes are valid -static inline bool tga_fmt(const u8* ptr, size_t size) -{ - UNUSED2(size); // size >= 4, we only need 4 bytes - - TgaHeader* hdr = (TgaHeader*)ptr; - - // not direct colour - if(hdr->colour_map_type != 0) - return false; - - // wrong colour type (not uncompressed greyscale or RGB) - if(hdr->img_type != TGA_TRUE_COLOUR && hdr->img_type != TGA_GREY) - return false; - - return true; -} - - -static inline bool tga_ext(const char* ext) -{ - return !stricmp(ext, ".tga"); -} - - -// requirements: uncompressed, direct colour, bottom up -static int tga_decode(TexInfo* t, const char* fn, u8* file, size_t file_size) -{ - const char* err = 0; - - TgaHeader* hdr = (TgaHeader*)file; - const size_t hdr_size = 18 + hdr->img_id_len; - - // make sure we can access all header fields - if(file_size < hdr_size) - { - err = "header not completely read"; -fail: - debug_printf("tga_decode: %s: %s\n", fn, err); - return ERR_CORRUPTED; - } - - const u8 type = hdr->img_type; - const uint w = read_le16(&hdr->w); - const uint h = read_le16(&hdr->h); - const uint bpp = hdr->bpp; - const u8 desc = hdr->img_desc; - - const u8 alpha_bits = desc & 0x0f; - const int orientation = (desc & TGA_TOP_DOWN)? TEX_TOP_DOWN : TEX_BOTTOM_UP; - u8* const img = file + hdr_size; - const size_t img_size = w * h * bpp/8; - - - int flags = 0; - if(alpha_bits != 0) - flags |= TEX_ALPHA; - if(bpp == 8) - flags |= TEX_GREY; - if(type == TGA_TRUE_COLOUR) - flags |= TEX_BGR; - - // storing right-to-left is just stupid; - // we're not going to bother converting it. - if(desc & TGA_RIGHT_TO_LEFT) - err = "image is stored right-to-left"; - if(bpp != 8 && bpp != 16 && bpp != 24 && bpp != 32) - err = "invalid bpp"; - if(file_size < hdr_size + img_size) - err = "size < image size"; - - if(err) - goto fail; - - t->ofs = hdr_size; - t->w = w; - t->h = h; - t->bpp = bpp; - t->flags = flags; - - const int transforms = orientation ^ global_orientation; - transform(t, img, transforms); - - return 0; -} - - -static int tga_encode(TexInfo* t, const char* fn, u8* img, size_t img_size) -{ - CHECK_ERR(fmt_8_or_24_or_32(t->bpp, t->flags)); - - u8 img_desc = (t->flags & TEX_TOP_DOWN)? TGA_TOP_DOWN : TGA_BOTTOM_UP; - if(t->bpp == 32) - img_desc |= 8; // size of alpha channel - TgaImgType img_type = (t->flags & TEX_GREY)? TGA_GREY : TGA_TRUE_COLOUR; - - // transform - int transforms = t->flags; - transforms &= ~TEX_ORIENTATION; // no flip needed - we can set top-down bit. - transforms ^= TEX_BGR; // TGA is native BGR. - transform(t, img, transforms); - - TgaHeader hdr = - { - 0, // no image identifier present - 0, // no colour map present - (u8)img_type, - {0,0,0,0,0}, // unused (colour map) - 0, 0, // unused (origin) - t->w, - t->h, - t->bpp, - img_desc - }; - return write_img(fn, &hdr, sizeof(hdr), img, img_size); -} - -#endif // #ifndef NO_TGA - - -////////////////////////////////////////////////////////////////////////////// -// -// BMP -// -////////////////////////////////////////////////////////////////////////////// - -#ifndef NO_BMP - -#pragma pack(push, 1) - -struct BmpHeader -{ - // - // BITMAPFILEHEADER - // - - u16 bfType; // "BM" - u32 bfSize; // of file - u16 bfReserved1; - u16 bfReserved2; - u32 bfOffBits; // offset to image data - - - // - // BITMAPINFOHEADER - // - - u32 biSize; - long biWidth; - long biHeight; - u16 biPlanes; - u16 biBitCount; - u32 biCompression; - u32 biSizeImage; - - // the following are unused and zeroed when writing: - long biXPelsPerMeter; - long biYPelsPerMeter; - u32 biClrUsed; - u32 biClrImportant; -}; - -#pragma pack(pop) - -#define BI_RGB 0 // biCompression - - -static inline bool bmp_fmt(const u8* p, size_t size) -{ - UNUSED2(size); // size >= 4, we only need 2 bytes - - // check header signature (bfType == "BM"?). - // we compare single bytes to be endian-safe. - return p[0] == 'B' && p[1] == 'M'; -} - - -static inline bool bmp_ext(const char* ext) -{ - return !stricmp(ext, ".bmp"); -} - - -// requirements: uncompressed, direct colour, bottom up -static int bmp_decode(TexInfo* t, const char* fn, u8* file, size_t file_size) -{ - const char* err = 0; - - const size_t hdr_size = sizeof(BmpHeader); - - // make sure we can access all header fields - if(file_size < hdr_size) - { - err = "header not completely read"; -fail: - debug_printf("bmp_decode: %s: %s\n", fn, err); - return ERR_CORRUPTED; - } - - const BmpHeader* hdr = (const BmpHeader*)file; - const long w = (long)read_le32(&hdr->biWidth); - const long h_ = (long)read_le32(&hdr->biHeight); - const u16 bpp = read_le16(&hdr->biBitCount); - const u32 compress = read_le32(&hdr->biCompression); - const u32 ofs = read_le32(&hdr->bfOffBits); - - const int orientation = (h_ < 0)? TEX_TOP_DOWN : TEX_BOTTOM_UP; - const long h = abs(h_); - - const size_t pitch = w * bpp/8; - const size_t img_size = h * pitch; - u8* const img = file + ofs; - - int flags = TEX_BGR; - if(bpp == 32) - flags |= TEX_ALPHA; - - if(compress != BI_RGB) - err = "compressed"; - if(bpp != 24 && bpp != 32) - err = "invalid bpp (not direct colour)"; - if(file_size < ofs+img_size) - err = "image not completely read"; - if(err) - goto fail; - - t->ofs = ofs; - t->w = w; - t->h = h; - t->bpp = bpp; - t->flags = flags; - - const int transforms = orientation ^ global_orientation; - transform(t, img, transforms); - - return 0; -} - - -static int bmp_encode(TexInfo* t, const char* fn, u8* img, size_t img_size) -{ - CHECK_ERR(fmt_8_or_24_or_32(t->bpp, t->flags)); - - const size_t hdr_size = sizeof(BmpHeader); // needed for BITMAPFILEHEADER - const size_t file_size = hdr_size + img_size; - const long h = (t->flags & TEX_TOP_DOWN)? -(int)t->h : t->h; - - int transforms = t->flags; - transforms &= ~TEX_ORIENTATION; // no flip needed - we can set top-down bit. - transforms ^= TEX_BGR; // BMP is native BGR. - transform(t, img, transforms); - - const BmpHeader hdr = - { - // BITMAPFILEHEADER - 0x4d42, // bfType = 'B','M' - (u32)file_size, // bfSize - 0, 0, // bfReserved1,2 - hdr_size, // bfOffBits - - // BITMAPINFOHEADER - 40, // biSize = sizeof(BITMAPINFOHEADER) - t->w, - h, - 1, // biPlanes - t->bpp, - BI_RGB, // biCompression - (u32)img_size, // biSizeImage - 0, 0, 0, 0 // unused (bi?PelsPerMeter, biClr*) - }; - - return write_img(fn, &hdr, sizeof(hdr), img, img_size); -} - -#endif // #ifndef NO_BMP - - -////////////////////////////////////////////////////////////////////////////// -// -// RAW -// -////////////////////////////////////////////////////////////////////////////// - -#ifndef NO_RAW - -// assume bottom-up - -// always returns true because this is the last registered codec. -static inline bool raw_fmt(const u8* UNUSED(p), size_t UNUSED(size)) -{ - return true; -} - - -static inline bool raw_ext(const char* ext) -{ - return !stricmp(ext, ".raw"); -} - - -static int raw_decode(TexInfo* t, const char* fn, u8* file, size_t file_size) -{ - // TODO: allow 8 bit format. problem: how to differentiate from 32? filename? - - // find a colour depth that matches file_size - uint i, dim; - for(i = 2; i <= 4; i++) - { - dim = (uint)sqrtf((float)file_size/i); - if(dim*dim*i == file_size) - goto have_bpp; - } - - debug_printf("raw_decode: %s: %s\n", fn, "no matching format found"); - return ERR_TEX_FMT_INVALID; - -have_bpp: - const int orientation = TEX_BOTTOM_UP; - u8* const img = file; - - // formats are: GL_LUMINANCE_ALPHA, GL_RGB, GL_RGBA - int flags = (i == 3)? 0 : TEX_ALPHA; - - t->ofs = 0; - t->w = dim; - t->h = dim; - t->bpp = i*8; - t->flags = flags; - - const int transforms = orientation ^ global_orientation; - return transform(t, img, transforms); -} - - -static int raw_encode(TexInfo* t, const char* fn, u8* img, size_t img_size) -{ - CHECK_ERR(fmt_8_or_24_or_32(t->bpp, t->flags)); - - // transforms - int transforms = t->flags; - transforms ^= TEX_BOTTOM_UP; // RAW is native bottom-up. - transforms ^= TEX_BGR; // RAW is native BGR. - transform(t, img, transforms); - - return write_img(fn, 0, 0, img, img_size); -} - -#endif // #ifndef NO_RAW - - -////////////////////////////////////////////////////////////////////////////// -// -// PNG -// -////////////////////////////////////////////////////////////////////////////// - -#ifndef NO_PNG - -static inline bool png_fmt(const u8* ptr, size_t size) -{ - UNUSED2(size); // size >= 4, we only need 4 bytes - - // don't use png_sig_cmp, so we don't pull in libpng for - // this check alone (it might not be used later). - return *(u32*)ptr == FOURCC('\x89','P','N','G'); -} - - -static inline bool png_ext(const char* ext) -{ - return !stricmp(ext, ".png"); -} - - -// note: it's not worth combining png_encode and png_decode, due to -// libpng read/write interface differences (grr). - - -struct PngMemFile -{ - const u8* p; - size_t size; - - size_t pos; // 0-initialized if no initializer -}; - -// pass data from PNG file in memory to libpng -static void png_read(png_struct* png_ptr, u8* data, png_size_t length) -{ - PngMemFile* f = (PngMemFile*)png_ptr->io_ptr; - - void* src = (u8*)(f->p + f->pos); - - // make sure there's enough new data remaining in the buffer - f->pos += length; - if(f->pos > f->size) - png_error(png_ptr, "png_read: not enough data to satisfy request!"); - - memcpy(data, src, length); -} - - -// split out of png_decode to simplify resource cleanup and avoid -// "dtor / setjmp interaction" warning. -static int png_decode_impl(TexInfo* t, u8* file, size_t file_size, - png_structp png_ptr, png_infop info_ptr, - u8*& img, RowArray& rows, const char*& msg) -{ - PngMemFile f = { file, file_size }; - png_set_read_fn(png_ptr, &f, png_read); - - // read header and determine format - png_read_info(png_ptr, info_ptr); - png_uint_32 w, h; - int bit_depth, colour_type; - png_get_IHDR(png_ptr, info_ptr, &w, &h, &bit_depth, &colour_type, 0, 0, 0); - const size_t pitch = png_get_rowbytes(png_ptr, info_ptr); - const u32 bpp = (u32)(pitch / w * 8); - - int flags = 0; - if(bpp == 32) - flags |= TEX_ALPHA; - if(colour_type == PNG_COLOR_TYPE_GRAY) - flags |= TEX_GREY; - - // make sure format is acceptable - if(bit_depth != 8) - msg = "channel precision != 8 bits"; - if(colour_type & PNG_COLOR_MASK_PALETTE) - msg = "colour type is invalid (must be direct colour)"; - if(msg) - return -1; - - const size_t img_size = pitch * h; - Handle img_hm; - // cannot free old t->hm until after png_read_end, - // but need to set to this handle afterwards => need tmp var. - img = (u8*)mem_alloc(img_size, 64*KiB, 0, &img_hm); - if(!img) - return ERR_NO_MEM; - - const int transforms = TEX_TOP_DOWN ^ global_orientation; - CHECK_ERR(alloc_rows(img, h, pitch, transforms, rows)); - - png_read_image(png_ptr, (png_bytepp)rows); - png_read_end(png_ptr, info_ptr); - - // success; make sure all data was consumed. - debug_assert(f.p == file && f.size == file_size && f.pos == f.size); - - // store image info - // .. transparently switch handles - free the old (compressed) - // buffer and replace it with the decoded-image memory handle. - mem_free_h(t->hm); - t->hm = img_hm; - t->ofs = 0; // libpng returns decoded image data; no header - t->w = w; - t->h = h; - t->bpp = bpp; - t->flags = flags; - - return 0; -} - - -// limitation: palette images aren't supported -static int png_decode(TexInfo* t, const char* fn, u8* file, size_t file_size) -{ - const char* msg = 0; - int err = -1; - - // freed when ret is reached: - png_structp png_ptr = 0; - png_infop info_ptr = 0; - RowArray rows = 0; - - // freed if fail is reached: - u8* img = 0; // decompressed image memory - - // allocate PNG structures; use default stderr and longjmp error handlers - png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0); - if(!png_ptr) - goto fail; - info_ptr = png_create_info_struct(png_ptr); - if(!info_ptr) - goto fail; - - // setup error handling - if(setjmp(png_jmpbuf(png_ptr))) - { - // reached if PNG triggered a longjmp -fail: - mem_free(img); - - if(!msg) - msg = "unknown error"; - debug_printf("png_decode: %s: %s\n", fn, msg); - goto ret; - } - - err = png_decode_impl(t, file, file_size, png_ptr, info_ptr, img, rows, msg); - if(err < 0) - goto fail; - - // shared cleanup -ret: - free(rows); - - if(png_ptr) - png_destroy_read_struct(&png_ptr, &info_ptr, 0); - - return err; -} - - -// write libpng output to PNG file -static void png_write(png_struct* png_ptr, u8* data, png_size_t length) -{ - void* p = (void*)data; - Handle hf = *(Handle*)png_ptr->io_ptr; - if(vfs_io(hf, length, &p) != (ssize_t)length) - png_error(png_ptr, "png_write: !"); -} - -static void png_flush(png_structp) -{ -} - - -// split out of png_encode to simplify resource cleanup and avoid -// "dtor / setjmp interaction" warning. -static int png_encode_impl(TexInfo* t, const char* fn, u8* img, - png_structp png_ptr, png_infop info_ptr, - RowArray& rows, Handle& hf, const char*& msg) -{ - UNUSED2(msg); // we don't produce any error messages ATM. - - const int png_transforms = (t->flags & TEX_BGR)? PNG_TRANSFORM_BGR : PNG_TRANSFORM_IDENTITY; - // PNG is native RGB. - - const png_uint_32 w = t->w, h = t->h; - const size_t pitch = w * t->bpp / 8; - - int colour_type; - switch(t->flags & (TEX_GREY|TEX_ALPHA)) - { - case TEX_GREY|TEX_ALPHA: - colour_type = PNG_COLOR_TYPE_GRAY_ALPHA; - break; - case TEX_GREY: - colour_type = PNG_COLOR_TYPE_GRAY; - break; - case TEX_ALPHA: - colour_type = PNG_COLOR_TYPE_RGB_ALPHA; - break; - default: - colour_type = PNG_COLOR_TYPE_RGB; - break; - } - - hf = vfs_open(fn, FILE_WRITE|FILE_NO_AIO); - CHECK_ERR(hf); - png_set_write_fn(png_ptr, &hf, png_write, png_flush); - - png_set_IHDR(png_ptr, info_ptr, w, h, 8, colour_type, - PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); - - const int transforms = TEX_TOP_DOWN ^ t->flags; - CHECK_ERR(alloc_rows(img, h, pitch, transforms, rows)); - - png_set_rows(png_ptr, info_ptr, (png_bytepp)rows); - png_write_png(png_ptr, info_ptr, png_transforms, 0); - - return 0; -} - - -// limitation: palette images aren't supported -static int png_encode(TexInfo* t, const char* fn, u8* img, size_t UNUSED(img_size)) -{ - const char* msg = 0; - int err = -1; - - // freed when ret is reached. - png_structp png_ptr = 0; - png_infop info_ptr = 0; - RowArray rows = 0; - Handle hf = 0; - - // allocate PNG structures; use default stderr and longjmp error handlers - png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0); - if(!png_ptr) - goto fail; - info_ptr = png_create_info_struct(png_ptr); - if(!info_ptr) - goto fail; - - // setup error handling - if(setjmp(png_jmpbuf(png_ptr))) - { - // reached if libpng triggered a longjmp -fail: - if(!msg) - msg = "unknown error"; - debug_printf("png_encode: %s: %s\n", fn, msg); - goto ret; - } - - err = png_encode_impl(t, fn, img, png_ptr, info_ptr, rows, hf, msg); - if(err < 0) - goto fail; - - // shared cleanup -ret: - png_destroy_write_struct(&png_ptr, &info_ptr); - - free(rows); - vfs_close(hf); - - return err; -} - -#endif // #ifndef NO_PNG - - -////////////////////////////////////////////////////////////////////////////// -// -// JP2 -// -////////////////////////////////////////////////////////////////////////////// - -#ifndef NO_JP2 - -static inline bool jp2_fmt(u8* p, size_t size) -{ - ONCE(jas_init()); - - jas_stream_t* stream = jas_stream_memopen((char*)p, size); - return jp2_validate(stream) >= 0; -} - - -static inline bool jp2_ext(const char* ext) -{ - return !stricmp(ext, ".jp2"); -} - - -static int jp2_decode(TexInfo* t, const char* fn, u8* file, size_t file_size) -{ - const char* err = 0; - - jas_stream_t* stream = jas_stream_memopen((char*)file, file_size); - jas_image_t* image = jas_image_decode(stream, -1, 0); - if(!image) - return -1; - - const int num_cmpts = jas_image_numcmpts(image); - jas_matrix_t* matr[4] = {0}; - jas_seqent_t* rows[4] = {0}; - const u32 w = jas_image_cmptwidth (image, 0); - const u32 h = jas_image_cmptheight(image, 0); - const int prec = jas_image_cmptprec (image, 0); - const u32 bpp = num_cmpts * 8; - const u32 ofs = 0; // jasper returns decoded image data; no header - int flags = 0; - - if(depth != 8) - { - err = "channel precision != 8"; - -fail: - debug_printf("jp2_decode: %s: %s\n", fn, err); -// TODO: destroy image - return -1; - } - - size_t img_size = w * h * num_cmpts; - Handle img_hm; - u8* img = (u8*)mem_alloc(img_size, 64*KiB, 0, &img_hm); - u8* out = img; - - int cmpt; - for(cmpt = 0; cmpt < num_cmpts; cmpt++) - matr[cmpt] = jas_matrix_create(1, w); - - for(int y = 0; y < h; y++) - { - for(cmpt = 0; cmpt < num_cmpts; cmpt++) - { - jas_image_readcmpt(image, cmpt, 0, y, w, 1, matr[cmpt]); - rows[cmpt] = jas_matrix_getref(matr[cmpt], 0, 0); - } - - for(int x = 0; x < w; x++) - for(cmpt = 0; cmpt < num_cmpts; cmpt++) - *out++ = *rows[cmpt]++; - } - - for(cmpt = 0; cmpt < num_cmpts; cmpt++) - jas_matrix_destroy(matr[cmpt]); - - // .. transparently switch handles - free the old (compressed) - // buffer and replace it with the decoded-image memory handle. - mem_free_h(t->hm); - t->hm = img_hm; - - t->ofs = ofs; - t->w = w; - t->h = h; - t->bpp = bpp; - t->flags = flags; - - return 0; -} - - -static int jp2_encode(TexInfo* t, const char* fn, const u8* img, size_t img_size) -{ - return ERR_NOT_IMPLEMENTED; -} - -#endif // #ifndef NO_JP2 - - -////////////////////////////////////////////////////////////////////////////// -// -// JPG -// -////////////////////////////////////////////////////////////////////////////// - -#ifndef NO_JPG - -static inline bool jpg_fmt(const u8* p, size_t size) -{ - UNUSED2(size); // size >= 4, we only need 2 bytes - - // JFIF requires SOI marker at start of stream. - // we compare single bytes to be endian-safe. - return p[0] == 0xff && p[1] == 0xd8; -} - - -static inline bool jpg_ext(const char* ext) -{ - return !stricmp(ext, ".jpg") || !stricmp(ext, ".jpeg"); -} - - -// -// error handler, shared by jpg_(en|de)code: -// -// the JPEG library's standard error handler (jerror.c) is divided into -// several "methods" which we can override individually. This allows -// adjusting the behavior without duplicating a lot of code, which may -// have to be updated with each future release. -// -// we here override error_exit to return control to the library's caller -// (i.e. jpg_(de|en)code) when a fatal error occurs, rather than calling exit. -// -// the replacement error_exit does a longjmp back to the caller's -// setjmp return point. it needs access to the jmp_buf, -// so we store it in a "subclass" of jpeg_error_mgr. -// - -struct JpgErrMgr -{ - struct jpeg_error_mgr pub; // "public" fields - - jmp_buf call_site; // jump here (back to JPEG lib caller) on error - char msg[JMSG_LENGTH_MAX]; // description of first error encountered - // must store per JPEG context for thread safety. - // initialized as part of JPEG context error setup. -}; - -METHODDEF(void) jpg_error_exit(j_common_ptr cinfo) -{ - // get subclass - JpgErrMgr* err_mgr = (JpgErrMgr*)cinfo->err; - - // "output" error message (i.e. store in JpgErrMgr; - // call_site is responsible for displaying it via debug_printf) - (*cinfo->err->output_message)(cinfo); - - // jump back to call site, i.e. jpg_(de|en)code - longjmp(err_mgr->call_site, 1); -} - - -// stores message in JpgErrMgr for later output by jpg_(de|en)code. -// note: don't display message here, so the caller can -// add some context (whether encoding or decoding, and filename). -METHODDEF(void) jpg_output_message(j_common_ptr cinfo) -{ - // get subclass - JpgErrMgr* err_mgr = (JpgErrMgr*)cinfo->err; - - // this context already had an error message; don't overwrite it. - // (subsequent errors probably aren't related to the real problem). - // note: was set to '\0' by JPEG context error setup. - if(err_mgr->msg[0] != '\0') - return; - - // generate the message and store it - (*cinfo->err->format_message)(cinfo, err_mgr->msg); -} - - -static int jpg_decode(TexInfo* t, const char* fn, u8* file, size_t file_size) -{ - const char* msg = 0; - int err = -1; - - // freed when ret is reached: - struct jpeg_decompress_struct cinfo; - // contains the JPEG decompression parameters and pointers to - // working space (allocated as needed by the JPEG library). - RowArray rows = 0; - // array of pointers to scanlines in img, set by alloc_rows. - // jpeg won't output more than a few scanlines at a time, - // so we need an output loop anyway, but passing at least 2..4 - // rows is more efficient in low-quality modes (due to less copying). - - // freed when fail is reached: - u8* img = 0; // decompressed image memory - - // set up our error handler, which overrides jpeg's default - // write-to-stderr-and-exit behavior. - // notes: - // - must be done before jpeg_create_decompress, in case that fails - // (unlikely, but possible if out of memory). - // - valid over cinfo lifetime (avoids dangling pointer in cinfo) - JpgErrMgr jerr; - cinfo.err = jpeg_std_error(&jerr.pub); - jerr.pub.error_exit = jpg_error_exit; - jerr.pub.output_message = jpg_output_message; - jerr.msg[0] = '\0'; - // required for "already have message" check in output_message - if(setjmp(jerr.call_site)) - { -fail: - // either JPEG has raised an error, or code below failed. - // warn user, and skip to cleanup code. - debug_printf("jpg_decode: %s: %s\n", fn, msg? msg : "unknown"); - goto ret; - } - - // goto scoping - { - jpeg_create_decompress(&cinfo); - - jpeg_mem_src(&cinfo, file, file_size); - - - // - // read header, determine format - // - - (void) jpeg_read_header(&cinfo, TRUE); - // we can ignore the return value since: - // - suspension is not possible with the mem data source - // - we passed TRUE to raise an error if table-only JPEG file - - int bpp = cinfo.num_components * 8; - // preliminary; set below to reflect output params - - // make sure we get a colour format we know - // (exception: if bpp = 8, go greyscale below) - // necessary to support non-standard CMYK files written by Photoshop. - cinfo.out_color_space = JCS_RGB; - - int flags = 0; - if(bpp == 8) - { - flags |= TEX_GREY; - cinfo.out_color_space = JCS_GRAYSCALE; - } - - // lower quality, but faster - cinfo.dct_method = JDCT_IFAST; - cinfo.do_fancy_upsampling = FALSE; - - - (void) jpeg_start_decompress(&cinfo); - // we can ignore the return value since - // suspension is not possible with the mem data source. - - // scaled output image dimensions and final bpp are now available. - int w = cinfo.output_width; - int h = cinfo.output_height; - bpp = cinfo.output_components * 8; - - // note: since we've set out_color_space, JPEG will always - // return an acceptable image format; no need to check. - - - // - // allocate memory for uncompressed image - // - - size_t pitch = w * bpp / 8; - // needed by alloc_rows - const size_t img_size = pitch * h; - Handle img_hm; - // cannot free old t->hm until after jpeg_finish_decompress, - // but need to set to this handle afterwards => need tmp var. - img = (u8*)mem_alloc(img_size, 64*KiB, 0, &img_hm); - if(!img) - { - err = ERR_NO_MEM; - goto fail; - } - const int transforms = TEX_TOP_DOWN ^ global_orientation; - int ret = alloc_rows(img, h, pitch, transforms, rows); - if(ret < 0) - { - err = ret; - goto fail; - } - - - // could use cinfo.output_scanline to keep track of progress, - // but we need to count lines_left anyway (paranoia). - JSAMPARRAY row = (JSAMPARRAY)rows; - JDIMENSION lines_left = h; - while(lines_left != 0) - { - JDIMENSION lines_read = jpeg_read_scanlines(&cinfo, row, lines_left); - row += lines_read; - lines_left -= lines_read; - - // we've decoded in-place; no need to further process - } - - (void)jpeg_finish_decompress(&cinfo); - // we can ignore the return value since suspension - // is not possible with the mem data source. - - if(jerr.pub.num_warnings != 0) - debug_printf("jpg_decode: corrupt-data warning(s) occurred\n"); - - // store image info - // .. transparently switch handles - free the old (compressed) - // buffer and replace it with the decoded-image memory handle. - mem_free_h(t->hm); - t->hm = img_hm; - t->ofs = 0; // jpeg returns decoded image data; no header - t->w = w; - t->h = h; - t->bpp = bpp; - t->flags = flags; - - err = 0; - - } - - // shared cleanup -ret: - jpeg_destroy_decompress(&cinfo); - // releases a "good deal" of memory - - free(rows); - - return err; -} - - - -// limitation: palette images aren't supported -static int jpg_encode(TexInfo* t, const char* fn, u8* img, size_t UNUSED(img_size)) -{ - const char* msg = 0; - int err = -1; - - // freed when ret is reached: - struct jpeg_compress_struct cinfo; - // contains the JPEG compression parameters and pointers to - // working space (allocated as needed by the JPEG library). - RowArray rows = 0; - // array of pointers to scanlines in img, set by alloc_rows. - // jpeg won't output more than a few scanlines at a time, - // so we need an output loop anyway, but passing at least 2..4 - // rows is more efficient in low-quality modes (due to less copying). - - Handle hf = 0; - - // set up our error handler, which overrides jpeg's default - // write-to-stderr-and-exit behavior. - // notes: - // - must be done before jpeg_create_compress, in case that fails - // (unlikely, but possible if out of memory). - // - valid over cinfo lifetime (avoids dangling pointer in cinfo) - JpgErrMgr jerr; - cinfo.err = jpeg_std_error(&jerr.pub); - jerr.pub.error_exit = jpg_error_exit; - jerr.pub.output_message = jpg_output_message; - jerr.msg[0] = '\0'; - // required for "already have message" check in output_message - if(setjmp(jerr.call_site)) - { -fail: - // either JPEG has raised an error, or code below failed. - // warn user, and skip to cleanup code. - debug_printf("jpg_encode: %s: %s\n", fn, msg? msg : "unknown"); - goto ret; - } - - // goto scoping - { - - jpeg_create_compress(&cinfo); - - hf = vfs_open(fn, FILE_WRITE|FILE_NO_AIO); - if(hf <= 0) - { - err = (int)hf; - goto fail; - } - jpeg_vfs_dst(&cinfo, hf); - - - // - // describe input image - // - - // required: - cinfo.image_width = t->w; - cinfo.image_height = t->h; - cinfo.input_components = t->bpp / 8; - cinfo.in_color_space = (t->bpp == 8)? JCS_GRAYSCALE : JCS_RGB; - - // defaults depend on cinfo.in_color_space already having been set! - jpeg_set_defaults(&cinfo); - - // more settings (e.g. quality) - - - jpeg_start_compress(&cinfo, TRUE); - // TRUE ensures that we will write a complete interchange-JPEG file. - // don't change unless you are very sure of what you're doing. - - - // make sure we have RGB - const int bgr_transform = t->flags & TEX_BGR; // JPG is native RGB. - transform(t, img, bgr_transform); - - const size_t pitch = t->w * t->bpp / 8; - const int transform = TEX_TOP_DOWN ^ t->flags; - int ret = alloc_rows(img, t->h, pitch, transform, rows); - if(ret < 0) - { - err = ret; - goto fail; - } - - - // could use cinfo.output_scanline to keep track of progress, - // but we need to count lines_left anyway (paranoia). - JSAMPARRAY row = (JSAMPARRAY)rows; - JDIMENSION lines_left = t->h; - while(lines_left != 0) - { - JDIMENSION lines_read = jpeg_write_scanlines(&cinfo, row, lines_left); - row += lines_read; - lines_left -= lines_read; - - // we've decoded in-place; no need to further process - } - - jpeg_finish_compress(&cinfo); - - if(jerr.pub.num_warnings != 0) - debug_printf("jpg_encode: corrupt-data warning(s) occurred\n"); - - err = 0; - - } - - // shared cleanup -ret: - jpeg_destroy_compress(&cinfo); - // releases a "good deal" of memory - - free(rows); - vfs_close(hf); - - return err; -} - -#endif // #ifndef NO_JPG - - -////////////////////////////////////////////////////////////////////////////// -// -// -// -////////////////////////////////////////////////////////////////////////////// - - - -static const Codec codecs[] = -{ -#ifndef NO_DDS - CODEC(dds), -#endif -#ifndef NO_PNG - CODEC(png), -#endif -#ifndef NO_JPG - CODEC(jpg), -#endif -#ifndef NO_BMP - CODEC(bmp), -#endif -#ifndef NO_TGA - CODEC(tga), -#endif -#ifndef NO_JP2 - CODEC(jp2), -#endif - -// must be last, as raw_fmt always returns true! -#ifndef NO_RAW - CODEC(raw) -#endif - -}; - -static const int num_codecs = ARRAY_SIZE(codecs); - - -int tex_is_known_fmt(void* p, size_t size) -{ - // skips raw, because that always returns true. - const Codec* c = codecs; - for(int i = 0; i < num_codecs-1; i++, c++) - if(c->is_fmt((const u8*)p, size)) - return 1; - return 0; -} - - -int tex_load_mem(Handle hm, const char* fn, TexInfo* t) +int tex_load_mem(Handle hm, const char* fn, Tex* t) { size_t size; void* _p = mem_get_ptr(hm, &size); @@ -1706,26 +265,28 @@ int tex_load_mem(Handle hm, const char* fn, TexInfo* t) return ERR_CORRUPTED; t->hm = hm; + // more convenient to pass loaders u8 - less casting. + // not const, because image may have to be flipped (in-place). u8* p = (u8*)_p; - // more convenient to pass loaders u8 - less casting. - // not const, because image may have to be flipped (in-place). // find codec that understands the data, and decode - const Codec* c = codecs; - for(int i = 0; i < num_codecs; i++, c++) + for(int i = 0; i < MAX_CODECS; i++) { - if(c->is_fmt(p, size)) - { - CHECK_ERR(c->decode(t, fn, p, size)); + const char* err_msg = 0; + int err = codecs[i]->decode(p, size, t, &err_msg); + if(err == TEX_CODEC_CANNOT_HANDLE) + continue; + if(err == 0) return 0; - } + debug_printf("tex_load_mem (%s): %s: %s", codecs[i]->name, fn, err_msg); + CHECK_ERR(err); } return ERR_UNKNOWN_FORMAT; } -int tex_load(const char* fn, TexInfo* t) +int tex_load(const char* fn, Tex* t) { // load file void* p; size_t size; // unused @@ -1735,41 +296,81 @@ int tex_load(const char* fn, TexInfo* t) // do not free hm! it either still holds the image data (i.e. texture // wasn't compressed) or was replaced by a new buffer for the image data. if(ret < 0) - memset(t, 0, sizeof(TexInfo)); + memset(t, 0, sizeof(Tex)); return ret; } -int tex_free(TexInfo* t) +int tex_free(Tex* t) { mem_free_h(t->hm); return 0; } -int tex_write(const char* fn, uint w, uint h, uint bpp, uint flags, void* img) +u8* tex_get_data(const Tex* t) { - const size_t img_size = w * h * bpp / 8; + u8* p = (u8*)mem_get_ptr(t->hm); + if(!p) + return 0; + return p + t->ofs; +} + + +int tex_transform(Tex* t, uint new_flags) +{ + // find codec that understands the data, and transform + for(int i = 0; i < MAX_CODECS; i++) + { + const char* err_msg = 0; + int err = codecs[i]->transform(t, new_flags); + if(err == TEX_CODEC_CANNOT_HANDLE) + continue; + if(err == 0) + return 0; + debug_printf("tex_transform (%s): failed, error %d", codecs[i]->name, err); + CHECK_ERR(err); + } + + // last chance + return plain_transform(t, new_flags); +} + + +int tex_write(const char* fn, uint w, uint h, uint bpp, uint flags, void* in_img) +{ + const size_t in_img_size = w * h * bpp / 8; const char* ext = strrchr(fn, '.'); if(!ext) return -1; + ext++; // skip . - TexInfo t = + Handle hm = mem_assign(in_img, in_img_size, 0, 0, 0, 0, 0); + Tex t = { - 0, // handle - not needed by encoders + hm, 0, // image data offset w, h, bpp, flags }; - - const Codec* c = codecs; - for(int i = 0; i < num_codecs; i++, c++) - if(c->is_ext(ext)) - { - CHECK_ERR(c->encode(&t, fn, (u8*)img, img_size)); - return 0; - } + u8* out; size_t out_size; + for(int i = 0; i < MAX_CODECS; i++) + { + const char* err_msg = 0; + int err = codecs[i]->encode(ext, &t, &out, &out_size, &err_msg); + if(err == TEX_CODEC_CANNOT_HANDLE) + continue; + if(err == 0) + goto have_codec; + debug_printf("tex_write(%s): %s: %s", codecs[i]->name, fn, err_msg); + CHECK_ERR(err); + } // no codec found return ERR_UNKNOWN_FORMAT; + +have_codec: + WARN_ERR(vfs_store(fn, out, out_size)); + free(out); + return 0; } diff --git a/source/lib/res/graphics/tex.h b/source/lib/res/graphics/tex.h index 487cde8f40..eb5d5e0469 100755 --- a/source/lib/res/graphics/tex.h +++ b/source/lib/res/graphics/tex.h @@ -23,14 +23,16 @@ -enum TexInfoFlags +enum TexFlags { - TEX_DXT = 0x07, // mask; value = {1,3,5} - TEX_BGR = 0x08, + TEX_DXT = 0x7, // mask; value = {1,3,5} + TEX_ALPHA = 0x10, TEX_GREY = 0x20, - // orientation - never returned by tex_load, since it automatically + TEX_BGR = 0x08, + + // orientation - never returned by ogl_tex_load, since it automatically // flips to match global orientation. these are passed to tex_write // to indicate the image orientation, or to tex_set_global_orientation. TEX_BOTTOM_UP = 0x40, @@ -43,25 +45,41 @@ enum TexInfoFlags }; // minimize size - stored in ogl tex resource control block -struct TexInfo +struct Tex { Handle hm; // H_Mem handle to loaded file size_t ofs; // offset to image data in file uint w : 16; uint h : 16; - uint bpp : 16; // average bits per pixel + uint bpp : 16; + + // describes the image format; indicates deviations from the + // plain image format (e.g. BGR layout, upside-down). + // + // we don't want to just load as-is and set flags, thereby dumping the + // burden of conversion on apps. instead, we convert as much as is + // convenient and necessary at load-time. however, we do not go + // overboard and support all<->all conversions (too complex) or + // encourage transforms at runtime. + // in release builds, prefer formats optimized for their intended use + // that don't require any preprocessing (minimize load time!) uint flags : 16; }; -extern int tex_load(const char* fn, TexInfo* ti); -extern int tex_load_mem(Handle hm, const char* fn, TexInfo* t); -extern int tex_free(TexInfo* ti); + +extern int tex_load(const char* fn, Tex* t); +extern int tex_load_mem(Handle hm, const char* fn, Tex* t); +extern int tex_free(Tex* t); + +extern u8* tex_get_data(const Tex* t); +extern int tex_transform(Tex* t, uint new_flags); extern int tex_write(const char* fn, uint w, uint h, uint bpp, uint flags, void* img); -extern int tex_is_known_fmt(void* p, size_t size_t); - +// rationale: some libraries can flip images for free when loading, so set a +// global orientation rather than only flipping at upload time. // param: either TEX_BOTTOM_UP or TEX_TOP_DOWN extern void tex_set_global_orientation(int orientation); + #endif // __TEX_H__ diff --git a/source/lib/res/graphics/tex_bmp.cpp b/source/lib/res/graphics/tex_bmp.cpp new file mode 100644 index 0000000000..ca00b742b1 --- /dev/null +++ b/source/lib/res/graphics/tex_bmp.cpp @@ -0,0 +1,152 @@ +#include "precompiled.h" + +#include "lib/byte_order.h" +#include "tex_codec.h" + +#pragma pack(push, 1) + +struct BmpHeader +{ + // + // BITMAPFILEHEADER + // + + u16 bfType; // "BM" + u32 bfSize; // of file + u16 bfReserved1; + u16 bfReserved2; + u32 bfOffBits; // offset to image data + + + // + // BITMAPINFOHEADER + // + + u32 biSize; + long biWidth; + long biHeight; + u16 biPlanes; + u16 biBitCount; + u32 biCompression; + u32 biSizeImage; + + // the following are unused and zeroed when writing: + long biXPelsPerMeter; + long biYPelsPerMeter; + u32 biClrUsed; + u32 biClrImportant; +}; + +#pragma pack(pop) + +#define BI_RGB 0 // biCompression + + +static int bmp_transform(Tex* t, int new_flags) +{ + return TEX_CODEC_CANNOT_HANDLE; +} + + +// requirements: uncompressed, direct colour, bottom up +static int bmp_decode(u8* file, size_t file_size, Tex* t, const char** perr_msg) +{ + // check header signature (bfType == "BM"?). + // we compare single bytes to be endian-safe. + if(file[0] != 'B' || file[1] != 'M') + return TEX_CODEC_CANNOT_HANDLE; + + const size_t hdr_size = sizeof(BmpHeader); + + // make sure we can access all header fields + if(file_size < hdr_size) + { + *perr_msg = "header not completely read"; +fail: + return ERR_CORRUPTED; + } + + const BmpHeader* hdr = (const BmpHeader*)file; + const long w = (long)read_le32(&hdr->biWidth); + const long h_ = (long)read_le32(&hdr->biHeight); + const u16 bpp = read_le16(&hdr->biBitCount); + const u32 compress = read_le32(&hdr->biCompression); + const u32 ofs = read_le32(&hdr->bfOffBits); + + const int orientation = (h_ < 0)? TEX_TOP_DOWN : TEX_BOTTOM_UP; + const long h = abs(h_); + const size_t pitch = w * bpp/8; + const size_t img_size = h * pitch; + u8* const img = file + ofs; + + int flags = TEX_BGR; + if(bpp == 32) + flags |= TEX_ALPHA; + + // sanity checks + const char* err = 0; + if(compress != BI_RGB) + err = "compressed"; + if(bpp != 24 && bpp != 32) + err = "invalid bpp (not direct colour)"; + if(file_size < ofs+img_size) + err = "image not completely read"; + if(err) + { + *perr_msg = err; + goto fail; + } + + t->ofs = ofs; + t->w = w; + t->h = h; + t->bpp = bpp; + t->flags = flags; + + tex_codec_set_orientation(t, orientation); + + return 0; +} + + +static int bmp_encode(const char* ext, Tex* t, u8** out, size_t* out_size, const char** perr_msg) +{ + if(stricmp(ext, "bmp")) + return TEX_CODEC_CANNOT_HANDLE; +/* + CHECK_ERR(fmt_8_or_24_or_32(t->bpp, t->flags)); + + const size_t hdr_size = sizeof(BmpHeader); // needed for BITMAPFILEHEADER + const size_t file_size = hdr_size + img_size; + const long h = (t->flags & TEX_TOP_DOWN)? -(int)t->h : t->h; + + int transforms = t->flags; + transforms &= ~TEX_ORIENTATION; // no flip needed - we can set top-down bit. + transforms ^= TEX_BGR; // BMP is native BGR. + transform(t, img, transforms); + + const BmpHeader hdr = + { + // BITMAPFILEHEADER + 0x4d42, // bfType = 'B','M' + (u32)file_size, // bfSize + 0, 0, // bfReserved1,2 + hdr_size, // bfOffBits + + // BITMAPINFOHEADER + 40, // biSize = sizeof(BITMAPINFOHEADER) + t->w, + h, + 1, // biPlanes + t->bpp, + BI_RGB, // biCompression + (u32)img_size, // biSizeImage + 0, 0, 0, 0 // unused (bi?PelsPerMeter, biClr*) + }; + + return write_img(fn, &hdr, sizeof(hdr), img, img_size); +*/ + return 0; +} + +TEX_CODEC_REGISTER(bmp); diff --git a/source/lib/res/graphics/tex_codec.h b/source/lib/res/graphics/tex_codec.h new file mode 100644 index 0000000000..8b7d8d2fab --- /dev/null +++ b/source/lib/res/graphics/tex_codec.h @@ -0,0 +1,62 @@ +#ifndef TEX_CODEC_H__ +#define TEX_CODEC_H__ + +#include "tex.h" + +// rationale: no C++ to allow us to store const char* name in vtbl. + +struct TexCodecVTbl +{ + // pointers aren't const, because the textures + // may have to be flipped in-place - see "texture orientation". + // size is guaranteed to be >= 4. + // (usually enough to compare the header's "magic" field; + // anyway, no legitimate file will be smaller) + int (*decode)(u8* data, size_t data_size, Tex* t, const char** perr_msg); + + // rationale: some codecs cannot calculate the output size beforehand + // (e.g. PNG output via libpng); we therefore require each one to + // allocate memory itself and return the pointer. + int (*encode)(const char* ext, Tex* t, u8** out, size_t* out_size, const char** perr_msg); + + int (*transform)(Tex* t, int new_flags); + + const char* name; +}; + + +#define TEX_CODEC_REGISTER(name)\ + static const TexCodecVTbl vtbl = { name##_decode, name##_encode, name##_transform, #name};\ + static int dummy = tex_codec_register(&vtbl); + + +// the given texture cannot be handled by this codec; pass the buck on to the next one +const int TEX_CODEC_CANNOT_HANDLE = 1; + + +// add this vtbl to the codec list. called at NLSO init time by the +// TEX_CODEC_REGISTER in each codec file. note that call order and therefore +// order in the list is undefined, but since each codec only steps up if it +// can handle the given format, this is not a problem. +extern int tex_codec_register(const TexCodecVTbl* c); + +// allocate an array of row pointers that point into the given texture data. +// indicates whether the file format is top-down or +// bottom-up; the row array is inverted if necessary to match global +// orienatation. (this is more efficient than "transforming" later) +// used by PNG and JPG codecs; caller must free() rows when done. +typedef const u8* RowPtr; +typedef RowPtr* RowArray; +extern int tex_codec_alloc_rows(const u8* data, size_t h, size_t pitch, + int file_orientation, RowArray& rows); + +extern int tex_codec_set_orientation(Tex* t, int file_orientation); + +// check if the given texture format is acceptable: 8bpp grey, +// 24bpp color or 32bpp color+alpha (BGR / upside down are permitted). +// basically, this is the "plain" format understood by all codecs and +// tex_codec_plain_transform. +// return 0 if ok or a negative error code. +extern int tex_codec_validate_format(uint bpp, uint flags); + +#endif // #ifndef TEX_CODEC_H__ diff --git a/source/lib/res/graphics/tex_dds.cpp b/source/lib/res/graphics/tex_dds.cpp new file mode 100644 index 0000000000..a7d12e8677 --- /dev/null +++ b/source/lib/res/graphics/tex_dds.cpp @@ -0,0 +1,309 @@ +#include "precompiled.h" + +#include "lib/byte_order.h" +#include "tex_codec.h" + +// NOTE: the convention is bottom-up for DDS, but there's no way to tell. + +// defs modified from ddraw header + + +#pragma pack(push, 1) + +// DDPIXELFORMAT.dwFlags +#define DDPF_ALPHAPIXELS 0x00000001 + +typedef struct +{ + u32 dwSize; // size of structure (32) + u32 dwFlags; // indicates which fields are valid + u32 dwFourCC; // (DDPF_FOURCC) FOURCC code, "DXTn" + u32 dwReserved1[5]; // reserved +} +DDPIXELFORMAT; + +typedef struct +{ + u32 dwCaps[4]; +} +DDSCAPS2; + +// DDSURFACEDESC2.dwFlags +#define DDSD_HEIGHT 0x00000002 +#define DDSD_WIDTH 0x00000004 +#define DDSD_PIXELFORMAT 0x00001000 +#define DDSD_MIPMAPCOUNT 0x00020000 + +typedef struct +{ + u32 dwSize; // size of structure (124) + u32 dwFlags; // indicates which fields are valid + u32 dwHeight; // height of main image (pixels) + u32 dwWidth; // width of main image (pixels) + u32 dwLinearSize; // (DDSD_LINEARSIZE): total image size + u32 dwDepth; // (DDSD_DEPTH) vol. textures: vol. depth + u32 dwMipMapCount; // (DDSD_MIPMAPCOUNT) total # levels + u32 dwReserved1[11]; // reserved + DDPIXELFORMAT ddpfPixelFormat; // pixel format description of the surface + DDSCAPS2 ddsCaps; // direct draw surface capabilities + u32 dwReserved2; // reserved +} +DDSURFACEDESC2; + +#pragma pack(pop) + + +static int dds_decompress(Tex* t) +{ + uint w = t->w, h = t->h; + if(w==0 || h==0 || w%4 || h%4) + return ERR_TEX_FMT_INVALID; + + int dxt = t->flags & TEX_DXT; + debug_assert(dxt == 1 || dxt == 3 || dxt == 5); + + u8* data = tex_get_data(t); +/* + GLsizei blocks_w = (GLsizei)(round_up(w, 4) / 4); + GLsizei blocks_h = (GLsizei)(round_up(h, 4) / 4); + GLsizei blocks = blocks_w * blocks_h; + GLsizei size = blocks * 16 * (base_fmt == GL_RGB ? 3 : 4); + GLsizei pitch = size / (blocks_h*4); + UNUSED2(pitch); + void* rgb_data = malloc(size); + + debug_assert(imageSize == blocks * (dxt1? 8 : 16)); + + // This code is inefficient, but I don't care: + + for(GLsizei block_y = 0; block_y < blocks_h; ++block_y) + for(GLsizei block_x = 0; block_x < blocks_w; ++block_x) + { + int c0_a = 255, c1_a = 255, c2_a = 255, c3_a = 255; + u8* alpha = NULL; + u8 dxt5alpha[8]; + + if(!dxt1) + { + alpha = (u8*)data; + data = (char*)data + 8; + + if(dxt5) + { + dxt5alpha[0] = alpha[0]; + dxt5alpha[1] = alpha[1]; + if(alpha[0] > alpha[1]) + { + dxt5alpha[2] = (6*alpha[0] + 1*alpha[1] + 3)/7; + dxt5alpha[3] = (5*alpha[0] + 2*alpha[1] + 3)/7; + dxt5alpha[4] = (4*alpha[0] + 3*alpha[1] + 3)/7; + dxt5alpha[5] = (3*alpha[0] + 4*alpha[1] + 3)/7; + dxt5alpha[6] = (2*alpha[0] + 5*alpha[1] + 3)/7; + dxt5alpha[7] = (1*alpha[0] + 6*alpha[1] + 3)/7; + } + else + { + dxt5alpha[2] = (4*alpha[0] + 1*alpha[1] + 2)/5; + dxt5alpha[3] = (3*alpha[0] + 2*alpha[1] + 2)/5; + dxt5alpha[4] = (2*alpha[0] + 3*alpha[1] + 2)/5; + dxt5alpha[5] = (1*alpha[0] + 4*alpha[1] + 2)/5; + dxt5alpha[6] = 0; + dxt5alpha[7] = 255; + } + } + } + + u16 c0 = *(u16*)( (char*)data + 0 ); + u16 c1 = *(u16*)( (char*)data + 2 ); + u32 bits = *(u32*)( (char*)data + 4 ); + + data = (char*)data + 8; + + // Unpack 565, and copy high bits to low bits + int c0_r = ((c0>>8)&0xF8) | ((c0>>13)&7); + int c1_r = ((c1>>8)&0xF8) | ((c1>>13)&7); + int c0_g = ((c0>>3)&0xFC) | ((c0>>9 )&3); + int c1_g = ((c1>>3)&0xFC) | ((c1>>9 )&3); + int c0_b = ((c0<<3)&0xF8) | ((c0>>2 )&7); + int c1_b = ((c1<<3)&0xF8) | ((c1>>2 )&7); + int c2_r, c2_g, c2_b; + int c3_r, c3_g, c3_b; + if(!dxt1 || c0 > c1) + { + c2_r = (c0_r*2+c1_r+1)/3; c2_g = (c0_g*2+c1_g+1)/3; c2_b = (c0_b*2+c1_b+1)/3; + c3_r = (c0_r+2*c1_r+1)/3; c3_g = (c0_g+2*c1_g+1)/3; c3_b = (c0_b+2*c1_b+1)/3; + } + else + { + c2_r = (c0_r+c1_r)/2; c2_g = (c0_g+c1_g)/2; c2_b = (c0_b+c1_b)/2; + c3_r = c3_g = c3_b = c3_a = 0; + } + + if(base_fmt == GL_RGB) + { + int i = 0; + for(int y = 0; y < 4; ++y) + { + u8* out = (u8*)rgb_data + ((block_y*4+y)*blocks_w*4 + block_x*4) * 3; + for(int x = 0; x < 4; ++x, ++i) + { + switch((bits >> (2*i)) & 3) { + case 0: *out++ = c0_r; *out++ = c0_g; *out++ = c0_b; break; + case 1: *out++ = c1_r; *out++ = c1_g; *out++ = c1_b; break; + case 2: *out++ = c2_r; *out++ = c2_g; *out++ = c2_b; break; + case 3: *out++ = c3_r; *out++ = c3_g; *out++ = c3_b; break; + } + } + } + + } + else + { + int i = 0; + for(int y = 0; y < 4; ++y) + { + u8* out = (u8*)rgb_data + ((block_y*4+y)*blocks_w*4 + block_x*4) * 4; + for(int x = 0; x < 4; ++x, ++i) + { + int a = 0; // squelch bogus uninitialized warning + switch((bits >> (2*i)) & 3) { + case 0: *out++ = c0_r; *out++ = c0_g; *out++ = c0_b; a = c0_a; break; + case 1: *out++ = c1_r; *out++ = c1_g; *out++ = c1_b; a = c1_a; break; + case 2: *out++ = c2_r; *out++ = c2_g; *out++ = c2_b; a = c2_a; break; + case 3: *out++ = c3_r; *out++ = c3_g; *out++ = c3_b; a = c3_a; break; + } + if(dxt3) + { + a = (int)((*(u64*)alpha >> (4*i)) & 0xF); + a |= a<<4; // copy low bits to high bits + } + else if(dxt5) + { + a = dxt5alpha[(*(u64*)(alpha+2) >> (3*i)) & 0x7]; + } + *out++ = a; + } + } + } + } + */ + + return 0; +} + +static int dds_transform(Tex* t, int new_flags) +{ + const int dxt = t->flags & TEX_DXT, new_dxt = new_flags & TEX_DXT; + // requesting decompression + if(dxt && !new_dxt) + return dds_decompress(t); + // both are DXT (unsupported; there are no flags we can change while + // compressed) or requesting compression (not implemented) or + // both not DXT (nothing we can do) - bail. + else + return TEX_CODEC_CANNOT_HANDLE; +} + + +static int dds_decode(u8* file, size_t file_size, Tex* t, const char** perr_msg) +{ + if(*(u32*)file != FOURCC('D','D','S',' ')) + return TEX_CODEC_CANNOT_HANDLE; + + const DDSURFACEDESC2* surf = (const DDSURFACEDESC2*)(file+4); + const size_t hdr_size = 4+sizeof(DDSURFACEDESC2); + + // make sure we can access all header fields + if(file_size < hdr_size) + { + *perr_msg = "header not completely read"; +fail: + return ERR_CORRUPTED; + } + + const u32 sd_size = read_le32(&surf->dwSize); + const u32 sd_flags = read_le32(&surf->dwFlags); + const u32 h = read_le32(&surf->dwHeight); + const u32 w = read_le32(&surf->dwWidth); + const u32 img_size = read_le32(&surf->dwLinearSize); + u32 mipmaps = read_le32(&surf->dwMipMapCount); + const u32 pf_size = read_le32(&surf->ddpfPixelFormat.dwSize); + const u32 pf_flags = read_le32(&surf->ddpfPixelFormat.dwFlags); + const u32 fourcc = surf->ddpfPixelFormat.dwFourCC; + // compared against FOURCC, which takes care of endian conversion. + + // we'll use these fields; make sure they're present below. + // note: we can't guess image dimensions if not specified - + // the image isn't necessarily square. + const u32 sd_req_flags = DDSD_WIDTH | DDSD_HEIGHT | DDSD_PIXELFORMAT; + + // make sure fields that aren't indicated as valid are zeroed. + if(!(sd_flags & DDSD_MIPMAPCOUNT)) + mipmaps = 0; + + // MS DXTex tool doesn't set the required dwPitchOrLinearSize field - + // they can't even write out their own file format correctly. *sigh* + // we need to pass it to OpenGL; it's calculated from w, h, and bpp, + // which we determine from the pixel format. + int bpp = 0; + int flags = 0; + + if(pf_flags & DDPF_ALPHAPIXELS) + flags |= TEX_ALPHA; + + switch(fourcc) + { + case FOURCC('D','X','T','1'): + bpp = 4; + flags |= 1; // bits in TEX_DXT mask indicate format + break; + case FOURCC('D','X','T','3'): + bpp = 8; + flags |= 3; // " + break; + case FOURCC('D','X','T','5'): + bpp = 8; + flags |= 5; // " + break; + } + + if(mipmaps) + flags |= TEX_MIPMAPS; + + // sanity checks + const char* err = 0; + if(file_size < hdr_size + img_size) + err = "file size too small"; + if(w % 4 || h % 4) + err = "image dimensions not padded to S3TC block size"; + if(!w || !h) + err = "width or height = 0"; + if(bpp == 0) + err = "invalid pixel format (not DXT{1,3,5})"; + if((sd_flags & sd_req_flags) != sd_req_flags) + err = "missing one or more required fields (w, h, pixel format)"; + if(sizeof(DDPIXELFORMAT) != pf_size) + err = "DDPIXELFORMAT size mismatch"; + if(sizeof(DDSURFACEDESC2) != sd_size) + err = "DDSURFACEDESC2 size mismatch"; + if(err) + { + *perr_msg = err; + goto fail; + } + + t->ofs = hdr_size; + t->w = w; + t->h = h; + t->bpp = bpp; + t->flags = flags; + return 0; +} + + +static int dds_encode(const char* UNUSED(ext), Tex* UNUSED(t), u8** UNUSED(out), size_t* UNUSED(out_size), const char** perr_msg) +{ + return ERR_NOT_IMPLEMENTED; +} + +TEX_CODEC_REGISTER(dds); diff --git a/source/lib/res/graphics/tex_jpg.cpp b/source/lib/res/graphics/tex_jpg.cpp new file mode 100644 index 0000000000..5302c77672 --- /dev/null +++ b/source/lib/res/graphics/tex_jpg.cpp @@ -0,0 +1,667 @@ +#include "precompiled.h" + +extern "C" { +// this is not a core library module, so it doesn't define JPEG_INTERNALS +#include "jpeglib.h" +#include "jerror.h" +} + +#include "lib.h" +#include "lib/res/res.h" +#include "tex_codec.h" + +#if MSC_VERSION +# ifdef NDEBUG +# pragma comment(lib, "jpeg-6b.lib") +# else +# pragma comment(lib, "jpeg-6bd.lib") +# endif // #ifdef NDEBUG +#endif // #ifdef MSC_VERSION + + + +/* IMPORTANT: we assume that JOCTET is 8 bits. */ +cassert(sizeof(JOCTET) == 1 && CHAR_BIT == 8); + +//----------------------------------------------------------------------------- +// mem source manager +//----------------------------------------------------------------------------- + + +/* Expanded data source object for memory input */ +typedef struct +{ + struct jpeg_source_mgr pub; /* public fields */ + + JOCTET* buf; + size_t size; /* total size (bytes) */ + size_t pos; /* offset (bytes) to new data */ +} +MemSrcMgr; + +typedef MemSrcMgr* SrcPtr; + + +/* +* Initialize source --- called by jpeg_read_header +* before any data is actually read. +*/ + +METHODDEF(void) init_source(j_decompress_ptr UNUSED(cinfo)) +{ +} + + +/* +* Fill the input buffer --- called whenever buffer is emptied. +* +* In typical applications, this should read fresh data into the buffer +* (ignoring the current state of next_input_byte & bytes_in_buffer), +* reset the pointer & count to the start of the buffer, and return TRUE +* indicating that the buffer has been reloaded. It is not necessary to +* fill the buffer entirely, only to obtain at least one more byte. +* +* There is no such thing as an EOF return. If the end of the file has been +* reached, the routine has a choice of ERREXIT() or inserting fake data into +* the buffer. In most cases, generating a warning message and inserting a +* fake EOI marker is the best course of action --- this will allow the +* decompressor to output however much of the image is there. However, +* the resulting error message is misleading if the real problem is an empty +* input file, so we handle that case specially. +*/ + +METHODDEF(boolean) fill_input_buffer(j_decompress_ptr cinfo) +{ + SrcPtr src = (SrcPtr)cinfo->src; + static const JOCTET eoi[2] = { 0xFF, JPEG_EOI }; + + /* + * since jpeg_mem_src fills the buffer with everything we've got, + * jpeg is trying to read beyond end of buffer. return a fake EOI marker. + * note: don't modify input buffer: it might be read-only. + */ + + WARNMS(cinfo, JWRN_JPEG_EOF); + + + src->pub.next_input_byte = eoi; + src->pub.bytes_in_buffer = 2; + return TRUE; +} + + +/* +* Skip data --- used to skip over a potentially large amount of +* uninteresting data (such as an APPn marker). +*/ + +METHODDEF(void) skip_input_data(j_decompress_ptr cinfo, long num_bytes) +{ + SrcPtr src = (SrcPtr)cinfo->src; + size_t skip_count = (size_t)num_bytes; + + /* docs say non-positive num_byte skips should be ignored */ + if(num_bytes <= 0) + return; + + /* + * just subtract bytes available in buffer, + * making sure we don't underflow the size_t. + * note: if we skip to or beyond end of buffer, + * bytes_in_buffer = 0 => fill_input_buffer called => abort. + */ + if(skip_count > src->pub.bytes_in_buffer) + skip_count = src->pub.bytes_in_buffer; + + src->pub.bytes_in_buffer -= skip_count; + src->pub.next_input_byte += skip_count; +} + + +/* +* An additional method that can be provided by data source modules is the +* resync_to_restart method for error recovery in the presence of RST markers. +* For the moment, this source module just uses the default resync method +* provided by the JPEG library. That method assumes that no backtracking +* is possible. +*/ + + +/* +* Terminate source --- called by jpeg_finish_decompress +* after all data has been read. Often a no-op. +* +* NB: *not* called by jpeg_abort or jpeg_destroy; surrounding +* application must deal with any cleanup that should happen even +* for error exit. +*/ + +METHODDEF(void) term_source(j_decompress_ptr UNUSED(cinfo)) +{ + /* + * no-op (we don't own the buffer and shouldn't, + * to make possible multiple images in a source). + */ +} + + +/* +* Prepare for input from a buffer. +* The caller is responsible for freeing it after finishing decompression. +*/ + +GLOBAL(void) jpeg_mem_src(j_decompress_ptr cinfo, void* p, size_t size) +{ + SrcPtr src; + + /* Treat 0-length buffer as fatal error */ + if(size == 0) + ERREXIT(cinfo, JERR_INPUT_EMPTY); + + /* + * The source object is made permanent so that + * a series of JPEG images can be read from the same file + * by calling jpeg_mem_src only before the first one. + * This makes it unsafe to use this manager and a different source + * manager serially with the same JPEG object. Caveat programmer. + */ + + /* first time for this JPEG object? */ + if(!cinfo->src) + cinfo->src = (struct jpeg_source_mgr*) + (*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_PERMANENT, + sizeof(MemSrcMgr)); + /* (takes care of raising error if out of memory) */ + + src = (SrcPtr)cinfo->src; + src->pub.init_source = init_source; + src->pub.fill_input_buffer = fill_input_buffer; + src->pub.skip_input_data = skip_input_data; + src->pub.resync_to_restart = jpeg_resync_to_restart; /* default */ + src->pub.term_source = term_source; + + /* + * fill buffer with everything we have. + * if fill_input_buffer is called, the buffer was overrun. + */ + src->pub.bytes_in_buffer = size; + src->pub.next_input_byte = (JOCTET*)p; +} + + +//----------------------------------------------------------------------------- +// mem destination manager +//----------------------------------------------------------------------------- + +/* Expanded data destination object for VFS output */ +typedef struct { + struct jpeg_destination_mgr pub; /* public fields */ + + Handle hf; + JOCTET* buf; + // jpeg-6b interface needs a memory buffer +} VfsDstMgr; + +typedef VfsDstMgr* DstPtr; + +#define OUTPUT_BUF_SIZE 16*KiB /* choose an efficiently writeable size */ + + +/* +* Initialize destination --- called by jpeg_start_compress +* before any data is actually written. +*/ + +METHODDEF(void) init_destination(j_compress_ptr cinfo) +{ + DstPtr dst = (DstPtr)cinfo->dest; + + /* Allocate the output buffer --- it will be released when done with image */ + dst->buf = (JOCTET*)(*cinfo->mem->alloc_small)((j_common_ptr)cinfo, JPOOL_IMAGE, + OUTPUT_BUF_SIZE * sizeof(JOCTET)); + + dst->pub.next_output_byte = dst->buf; + dst->pub.free_in_buffer = OUTPUT_BUF_SIZE; +} + + +/* +* Empty the output buffer --- called whenever buffer fills up. +* +* In typical applications, this should write the entire output buffer +* (ignoring the current state of next_output_byte & free_in_buffer), +* reset the pointer & count to the start of the buffer, and return TRUE +* indicating that the buffer has been dumped. +* +* In applications that need to be able to suspend compression due to output +* overrun, a FALSE return indicates that the buffer cannot be emptied now. +* In this situation, the compressor will return to its caller (possibly with +* an indication that it has not accepted all the supplied scanlines). The +* application should resume compression after it has made more room in the +* output buffer. Note that there are substantial restrictions on the use of +* suspension --- see the documentation. +* +* When suspending, the compressor will back up to a convenient restart point +* (typically the start of the current MCU). next_output_byte & free_in_buffer +* indicate where the restart point will be if the current call returns FALSE. +* Data beyond this point will be regenerated after resumption, so do not +* write it out when emptying the buffer externally. +*/ + +METHODDEF(boolean) empty_output_buffer(j_compress_ptr cinfo) +{ + DstPtr dst = (DstPtr)cinfo->dest; + + // writing out OUTPUT_BUF_SIZE-dst->pub.free_in_buffer bytes + // sounds reasonable, but make for broken output. + if(vfs_io(dst->hf, OUTPUT_BUF_SIZE, (void**)&dst->buf) != OUTPUT_BUF_SIZE) + ERREXIT(cinfo, JERR_FILE_WRITE); + + dst->pub.next_output_byte = dst->buf; + dst->pub.free_in_buffer = OUTPUT_BUF_SIZE; + + return TRUE; +} + + +/* +* Terminate destination --- called by jpeg_finish_compress +* after all data has been written. Usually needs to flush buffer. +* +* NB: *not* called by jpeg_abort or jpeg_destroy; surrounding +* application must deal with any cleanup that should happen even +* for error exit. +*/ + +METHODDEF(void) term_destination(j_compress_ptr cinfo) +{ + DstPtr dst = (DstPtr)cinfo->dest; + + // make sure any data left in the buffer is written out + const size_t bytes_in_buf = OUTPUT_BUF_SIZE - dst->pub.free_in_buffer; + if(vfs_io(dst->hf, bytes_in_buf, (void**)&dst->buf) != (ssize_t)bytes_in_buf) + ERREXIT(cinfo, JERR_FILE_WRITE); + + // flush file, if necessary. +} + + +/* +* Prepare for output to a stdio stream. +* The caller must have already opened the stream, and is responsible +* for closing it after finishing compression. +*/ + +GLOBAL(void) jpeg_vfs_dst(j_compress_ptr cinfo, Handle hf) +{ + /* The destination object is made permanent so that multiple JPEG images + * can be written to the same file without re-executing jpeg_stdio_dest. + * This makes it dangerous to use this manager and a different destination + * manager serially with the same JPEG object, because their private object + * sizes may be different. Caveat programmer. + */ + if (cinfo->dest == NULL) { /* first time for this JPEG object? */ + cinfo->dest = (struct jpeg_destination_mgr*)(*cinfo->mem->alloc_small) + ((j_common_ptr)cinfo, JPOOL_PERMANENT, sizeof(VfsDstMgr)); + } + + DstPtr dst = (DstPtr)cinfo->dest; + dst->pub.init_destination = init_destination; + dst->pub.empty_output_buffer = empty_output_buffer; + dst->pub.term_destination = term_destination; + dst->hf = hf; +} + + +//----------------------------------------------------------------------------- +// error handler, shared by jpg_(en|de)code +//----------------------------------------------------------------------------- + +// the JPEG library's standard error handler (jerror.c) is divided into +// several "methods" which we can override individually. This allows +// adjusting the behavior without duplicating a lot of code, which may +// have to be updated with each future release. +// +// we here override error_exit to return control to the library's caller +// (i.e. jpg_(de|en)code) when a fatal error occurs, rather than calling exit. +// +// the replacement error_exit does a longjmp back to the caller's +// setjmp return point. it needs access to the jmp_buf, +// so we store it in a "subclass" of jpeg_error_mgr. + +struct JpgErrMgr +{ + struct jpeg_error_mgr pub; // "public" fields + + jmp_buf call_site; // jump here (back to JPEG lib caller) on error + char msg[JMSG_LENGTH_MAX]; // description of first error encountered + // must store per JPEG context for thread safety. + // initialized as part of JPEG context error setup. +}; + +METHODDEF(void) jpg_error_exit(j_common_ptr cinfo) +{ + // get subclass + JpgErrMgr* err_mgr = (JpgErrMgr*)cinfo->err; + + // "output" error message (i.e. store in JpgErrMgr; + // call_site is responsible for displaying it via debug_printf) + (*cinfo->err->output_message)(cinfo); + + // jump back to call site, i.e. jpg_(de|en)code + longjmp(err_mgr->call_site, 1); +} + + +// stores message in JpgErrMgr for later output by jpg_(de|en)code. +// note: don't display message here, so the caller can +// add some context (whether encoding or decoding, and filename). +METHODDEF(void) jpg_output_message(j_common_ptr cinfo) +{ + // get subclass + JpgErrMgr* err_mgr = (JpgErrMgr*)cinfo->err; + + // this context already had an error message; don't overwrite it. + // (subsequent errors probably aren't related to the real problem). + // note: was set to '\0' by JPEG context error setup. + if(err_mgr->msg[0] != '\0') + return; + + // generate the message and store it + (*cinfo->err->format_message)(cinfo, err_mgr->msg); +} + + +//----------------------------------------------------------------------------- + + +static int jpg_transform(Tex* t, int new_flags) +{ + return TEX_CODEC_CANNOT_HANDLE; +} + + +static int jpg_decode(u8* file, size_t file_size, Tex* t, const char** perr_msg) +{ + // JFIF requires SOI marker at start of stream. + // we compare single bytes to be endian-safe. + if(file[0] != 0xff || file[1] == 0xd8) + return TEX_CODEC_CANNOT_HANDLE; + + int err = -1; + + // freed when ret is reached: + struct jpeg_decompress_struct cinfo; + // contains the JPEG decompression parameters and pointers to + // working space (allocated as needed by the JPEG library). + RowArray rows = 0; + // array of pointers to scanlines in img, set by alloc_rows. + // jpeg won't output more than a few scanlines at a time, + // so we need an output loop anyway, but passing at least 2..4 + // rows is more efficient in low-quality modes (due to less copying). + + // freed when fail is reached: + Handle img_hm; // decompressed image memory + + // set up our error handler, which overrides jpeg's default + // write-to-stderr-and-exit behavior. + // notes: + // - must be done before jpeg_create_decompress, in case that fails + // (unlikely, but possible if out of memory). + // - valid over cinfo lifetime (avoids dangling pointer in cinfo) + JpgErrMgr jerr; + cinfo.err = jpeg_std_error(&jerr.pub); + jerr.pub.error_exit = jpg_error_exit; + jerr.pub.output_message = jpg_output_message; + jerr.msg[0] = '\0'; + // required for "already have message" check in output_message + if(setjmp(jerr.call_site)) + { +fail: + // either JPEG has raised an error, or code below failed. + mem_free_h(img_hm); + goto ret; + } + + // goto scoping + { + jpeg_create_decompress(&cinfo); + + jpeg_mem_src(&cinfo, file, file_size); + + + // + // read header, determine format + // + + (void) jpeg_read_header(&cinfo, TRUE); + // we can ignore the return value since: + // - suspension is not possible with the mem data source + // - we passed TRUE to raise an error if table-only JPEG file + + int bpp = cinfo.num_components * 8; + // preliminary; set below to reflect output params + + // make sure we get a colour format we know + // (exception: if bpp = 8, go greyscale below) + // necessary to support non-standard CMYK files written by Photoshop. + cinfo.out_color_space = JCS_RGB; + + int flags = 0; + if(bpp == 8) + { + flags |= TEX_GREY; + cinfo.out_color_space = JCS_GRAYSCALE; + } + + // lower quality, but faster + cinfo.dct_method = JDCT_IFAST; + cinfo.do_fancy_upsampling = FALSE; + + + (void) jpeg_start_decompress(&cinfo); + // we can ignore the return value since + // suspension is not possible with the mem data source. + + // scaled output image dimensions and final bpp are now available. + int w = cinfo.output_width; + int h = cinfo.output_height; + bpp = cinfo.output_components * 8; + + // note: since we've set out_color_space, JPEG will always + // return an acceptable image format; no need to check. + + + // + // allocate memory for uncompressed image + // + + size_t pitch = w * bpp / 8; + // needed by alloc_rows + const size_t img_size = pitch * h; + // cannot free old t->hm until after jpeg_finish_decompress, + // but need to set to this handle afterwards => need tmp var. + u8* img = (u8*)mem_alloc(img_size, 64*KiB, 0, &img_hm); + if(!img) + { + err = ERR_NO_MEM; + goto fail; + } + int ret = tex_codec_alloc_rows(img, h, pitch, TEX_TOP_DOWN, rows); + if(ret < 0) + { + err = ret; + goto fail; + } + + + // could use cinfo.output_scanline to keep track of progress, + // but we need to count lines_left anyway (paranoia). + JSAMPARRAY row = (JSAMPARRAY)rows; + JDIMENSION lines_left = h; + while(lines_left != 0) + { + JDIMENSION lines_read = jpeg_read_scanlines(&cinfo, row, lines_left); + row += lines_read; + lines_left -= lines_read; + + // we've decoded in-place; no need to further process + } + + (void)jpeg_finish_decompress(&cinfo); + // we can ignore the return value since suspension + // is not possible with the mem data source. + + if(jerr.pub.num_warnings != 0) + debug_printf("jpg_decode: corrupt-data warning(s) occurred\n"); + + // store image info + // .. transparently switch handles - free the old (compressed) + // buffer and replace it with the decoded-image memory handle. + mem_free_h(t->hm); + t->hm = img_hm; + t->ofs = 0; // jpeg returns decoded image data; no header + t->w = w; + t->h = h; + t->bpp = bpp; + t->flags = flags; + + err = 0; + + } + + // shared cleanup +ret: + jpeg_destroy_decompress(&cinfo); + // releases a "good deal" of memory + + free(rows); + + return err; +} + + + +// limitation: palette images aren't supported +static int jpg_encode(const char* ext, Tex* t, u8** out, size_t* out_size, const char** perr_msg) +{ + if(stricmp(ext, "jpg") && stricmp(ext, "jpeg")) + return TEX_CODEC_CANNOT_HANDLE; + + const char* msg = 0; + int err = -1; + + // freed when ret is reached: + struct jpeg_compress_struct cinfo; + // contains the JPEG compression parameters and pointers to + // working space (allocated as needed by the JPEG library). + RowArray rows = 0; + // array of pointers to scanlines in img, set by alloc_rows. + // jpeg won't output more than a few scanlines at a time, + // so we need an output loop anyway, but passing at least 2..4 + // rows is more efficient in low-quality modes (due to less copying). + + Handle hf = 0; + + // set up our error handler, which overrides jpeg's default + // write-to-stderr-and-exit behavior. + // notes: + // - must be done before jpeg_create_compress, in case that fails + // (unlikely, but possible if out of memory). + // - valid over cinfo lifetime (avoids dangling pointer in cinfo) + JpgErrMgr jerr; + cinfo.err = jpeg_std_error(&jerr.pub); + jerr.pub.error_exit = jpg_error_exit; + jerr.pub.output_message = jpg_output_message; + jerr.msg[0] = '\0'; + // required for "already have message" check in output_message + if(setjmp(jerr.call_site)) + { +fail: + // either JPEG has raised an error, or code below failed. + goto ret; + } + + // goto scoping + { + + jpeg_create_compress(&cinfo); +/* + hf = vfs_open(fn, FILE_WRITE|FILE_NO_AIO); + if(hf <= 0) + { + err = (int)hf; + goto fail; + } + jpeg_vfs_dst(&cinfo, hf); +*/ + + // + // describe input image + // + + // required: + cinfo.image_width = t->w; + cinfo.image_height = t->h; + cinfo.input_components = t->bpp / 8; + cinfo.in_color_space = (t->bpp == 8)? JCS_GRAYSCALE : JCS_RGB; + + // defaults depend on cinfo.in_color_space already having been set! + jpeg_set_defaults(&cinfo); + + // more settings (e.g. quality) + + + jpeg_start_compress(&cinfo, TRUE); + // TRUE ensures that we will write a complete interchange-JPEG file. + // don't change unless you are very sure of what you're doing. + + + // make sure we have RGB + const int bgr_transform = t->flags & TEX_BGR; // JPG is native RGB. + WARN_ERR(tex_transform(t, bgr_transform)); + + const size_t pitch = t->w * t->bpp / 8; + u8* data = tex_get_data(t); + int ret = tex_codec_alloc_rows(data, t->h, pitch, TEX_TOP_DOWN, rows); + if(ret < 0) + { + err = ret; + goto fail; + } + + + // could use cinfo.output_scanline to keep track of progress, + // but we need to count lines_left anyway (paranoia). + JSAMPARRAY row = (JSAMPARRAY)rows; + JDIMENSION lines_left = t->h; + while(lines_left != 0) + { + JDIMENSION lines_read = jpeg_write_scanlines(&cinfo, row, lines_left); + row += lines_read; + lines_left -= lines_read; + + // we've decoded in-place; no need to further process + } + + jpeg_finish_compress(&cinfo); + + if(jerr.pub.num_warnings != 0) + debug_printf("jpg_encode: corrupt-data warning(s) occurred\n"); + + err = 0; + + } + + // shared cleanup +ret: + jpeg_destroy_compress(&cinfo); + // releases a "good deal" of memory + + free(rows); + vfs_close(hf); + + return err; +} + +TEX_CODEC_REGISTER(jpg); diff --git a/source/lib/res/graphics/tex_png.cpp b/source/lib/res/graphics/tex_png.cpp new file mode 100644 index 0000000000..6b107ce025 --- /dev/null +++ b/source/lib/res/graphics/tex_png.cpp @@ -0,0 +1,303 @@ +#include "precompiled.h" + +// include libpng header. we also prevent it from including windows.h, which +// would conflict with other headers. instead, WINAPI is defined here. +#if OS_WIN +# define _WINDOWS_ +# define WINAPI __stdcall +# define WINAPIV __cdecl +#endif // OS_WIN +#include "libpng13/png.h" + +#include "lib/byte_order.h" +#include "lib/res/res.h" +#include "tex_codec.h" + + + +#if MSC_VERSION + +// squelch "dtor / setjmp interaction" warnings. +// all attempts to resolve the underlying problem failed; apparently +// the warning is generated if setjmp is used at all in C++ mode. +// (png_decode has no code that would trigger ctors/dtors, nor are any +// called in its prolog/epilog code). +# pragma warning(disable: 4611) + +// pull in the appropriate debug/release library +# ifdef NDEBUG +# pragma comment(lib, "libpng13.lib") +# else +# pragma comment(lib, "libpng13d.lib") +# endif // NDEBUG + +#endif // MSC_VERSION + + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- + + +struct PngMemFile +{ + const u8* p; + size_t size; + + size_t pos; // 0-initialized if no initializer +}; + +// pass data from PNG file in memory to libpng +static void png_io_read(png_struct* png_ptr, u8* data, png_size_t length) +{ + PngMemFile* f = (PngMemFile*)png_ptr->io_ptr; + + void* src = (u8*)(f->p + f->pos); + + // make sure there's enough new data remaining in the buffer + f->pos += length; + if(f->pos > f->size) + png_error(png_ptr, "png_read: not enough data to satisfy request!"); + + memcpy(data, src, length); +} + + +// write libpng output to PNG file +static void png_io_write(png_struct* png_ptr, u8* data, png_size_t length) +{ + void* p = (void*)data; + Handle hf = *(Handle*)png_ptr->io_ptr; + if(vfs_io(hf, length, &p) != (ssize_t)length) + png_error(png_ptr, "png_write: !"); +} + + +static void png_io_flush(png_structp) +{ +} + + + +//----------------------------------------------------------------------------- + +static int png_transform(Tex* t, int new_flags) +{ + return TEX_CODEC_CANNOT_HANDLE; +} + + +// note: it's not worth combining png_encode and png_decode, due to +// libpng read/write interface differences (grr). + +// split out of png_decode to simplify resource cleanup and avoid +// "dtor / setjmp interaction" warning. +static int png_decode_impl(Tex* t, u8* file, size_t file_size, + png_structp png_ptr, png_infop info_ptr, + u8*& img, RowArray& rows, const char** perr_msg) +{ + PngMemFile f = { file, file_size }; + png_set_read_fn(png_ptr, &f, png_io_read); + + // read header and determine format + png_read_info(png_ptr, info_ptr); + png_uint_32 w, h; + int bit_depth, colour_type; + png_get_IHDR(png_ptr, info_ptr, &w, &h, &bit_depth, &colour_type, 0, 0, 0); + const size_t pitch = png_get_rowbytes(png_ptr, info_ptr); + const u32 bpp = (u32)(pitch / w * 8); + + int flags = 0; + if(bpp == 32) + flags |= TEX_ALPHA; + if(colour_type == PNG_COLOR_TYPE_GRAY) + flags |= TEX_GREY; + + // make sure format is acceptable + const char* err = 0; + if(bit_depth != 8) + err = "channel precision != 8 bits"; + if(colour_type & PNG_COLOR_MASK_PALETTE) + err = "colour type is invalid (must be direct colour)"; + if(err) + { + *perr_msg = err; + return -1; + } + + const size_t img_size = pitch * h; + Handle img_hm; + // cannot free old t->hm until after png_read_end, + // but need to set to this handle afterwards => need tmp var. + img = (u8*)mem_alloc(img_size, 64*KiB, 0, &img_hm); + if(!img) + return ERR_NO_MEM; + + CHECK_ERR(tex_codec_alloc_rows(img, h, pitch, TEX_TOP_DOWN, rows)); + + png_read_image(png_ptr, (png_bytepp)rows); + png_read_end(png_ptr, info_ptr); + + // success; make sure all data was consumed. + debug_assert(f.p == file && f.size == file_size && f.pos == f.size); + + // store image info + // .. transparently switch handles - free the old (compressed) + // buffer and replace it with the decoded-image memory handle. + mem_free_h(t->hm); + t->hm = img_hm; + t->ofs = 0; // libpng returns decoded image data; no header + t->w = w; + t->h = h; + t->bpp = bpp; + t->flags = flags; + + return 0; +} + + +// split out of png_encode to simplify resource cleanup and avoid +// "dtor / setjmp interaction" warning. +static int png_encode_impl(Tex* t, + png_structp png_ptr, png_infop info_ptr, + RowArray& rows, Handle& hf, const char** perr_msg) +{ + UNUSED2(perr_msg); // we don't produce any error messages ATM. + + const int png_transforms = (t->flags & TEX_BGR)? PNG_TRANSFORM_BGR : PNG_TRANSFORM_IDENTITY; + // PNG is native RGB. + + const png_uint_32 w = t->w, h = t->h; + const size_t pitch = w * t->bpp / 8; + + int colour_type; + switch(t->flags & (TEX_GREY|TEX_ALPHA)) + { + case TEX_GREY|TEX_ALPHA: + colour_type = PNG_COLOR_TYPE_GRAY_ALPHA; + break; + case TEX_GREY: + colour_type = PNG_COLOR_TYPE_GRAY; + break; + case TEX_ALPHA: + colour_type = PNG_COLOR_TYPE_RGB_ALPHA; + break; + default: + colour_type = PNG_COLOR_TYPE_RGB; + break; + } +/* + hf = vfs_open(fn, FILE_WRITE|FILE_NO_AIO); + CHECK_ERR(hf); + png_set_write_fn(png_ptr, &hf, png_io_write, png_io_flush); +*/ + png_set_IHDR(png_ptr, info_ptr, w, h, 8, colour_type, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + + u8* data = tex_get_data(t); + CHECK_ERR(tex_codec_alloc_rows(data, h, pitch, TEX_TOP_DOWN, rows)); + + png_set_rows(png_ptr, info_ptr, (png_bytepp)rows); + png_write_png(png_ptr, info_ptr, png_transforms, 0); + + return 0; +} + + + +// limitation: palette images aren't supported +static int png_decode(u8* file, size_t file_size, Tex* t, const char** perr_msg) +{ + // don't use png_sig_cmp, so we don't pull in libpng for + // this check alone (it might not actually be used). + if(*(u32*)file != FOURCC('\x89','P','N','G')) + return TEX_CODEC_CANNOT_HANDLE; + + int err = -1; + + // freed when ret is reached: + png_structp png_ptr = 0; + png_infop info_ptr = 0; + RowArray rows = 0; + + // freed if fail is reached: + u8* img = 0; // decompressed image memory + + // allocate PNG structures; use default stderr and longjmp error handlers + png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0); + if(!png_ptr) + goto fail; + info_ptr = png_create_info_struct(png_ptr); + if(!info_ptr) + goto fail; + + // setup error handling + if(setjmp(png_jmpbuf(png_ptr))) + { + // reached if PNG triggered a longjmp +fail: + mem_free(img); + goto ret; + } + + err = png_decode_impl(t, file, file_size, png_ptr, info_ptr, img, rows, perr_msg); + if(err < 0) + goto fail; + + // shared cleanup +ret: + free(rows); + + if(png_ptr) + png_destroy_read_struct(&png_ptr, &info_ptr, 0); + + return err; +} + + +// limitation: palette images aren't supported +static int png_encode(const char* ext, Tex* t, u8** out, size_t* UNUSED(out_size), const char** perr_msg) +{ + if(stricmp(ext, "png")) + return TEX_CODEC_CANNOT_HANDLE; + + int err = -1; + + // freed when ret is reached. + png_structp png_ptr = 0; + png_infop info_ptr = 0; + RowArray rows = 0; + Handle hf = 0; + + // allocate PNG structures; use default stderr and longjmp error handlers + png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0); + if(!png_ptr) + goto fail; + info_ptr = png_create_info_struct(png_ptr); + if(!info_ptr) + goto fail; + + // setup error handling + if(setjmp(png_jmpbuf(png_ptr))) + { + // reached if libpng triggered a longjmp +fail: + // currently no extra resources that need to be freed + goto ret; + } + + err = png_encode_impl(t, png_ptr, info_ptr, rows, hf, perr_msg); + if(err < 0) + goto fail; + + // shared cleanup +ret: + png_destroy_write_struct(&png_ptr, &info_ptr); + + free(rows); + vfs_close(hf); + + return err; +} + +TEX_CODEC_REGISTER(png); diff --git a/source/lib/res/graphics/tex_tga.cpp b/source/lib/res/graphics/tex_tga.cpp new file mode 100644 index 0000000000..100b3f56fc --- /dev/null +++ b/source/lib/res/graphics/tex_tga.cpp @@ -0,0 +1,157 @@ +#include "precompiled.h" + +#include "lib/byte_order.h" +#include "tex_codec.h" + + +#pragma pack(push, 1) + +enum TgaImgType +{ + TGA_TRUE_COLOUR = 2, // uncompressed 24 or 32 bit direct RGB + TGA_GREY = 3 // uncompressed 8 bit direct greyscale +}; + +enum TgaImgDesc +{ + TGA_RIGHT_TO_LEFT = BIT(4), + TGA_TOP_DOWN = BIT(5), + TGA_BOTTOM_UP = 0 // opposite of TGA_TOP_DOWN +}; + +typedef struct +{ + u8 img_id_len; // 0 - no image identifier present + u8 colour_map_type; // 0 - no colour map present + u8 img_type; // see TgaImgType + u8 colour_map[5]; // unused + + u16 x_origin; // unused + u16 y_origin; // unused + + u16 w; + u16 h; + u8 bpp; // bits per pixel + + u8 img_desc; +} +TgaHeader; + +// TGA file: header [img id] [colour map] image data + +#pragma pack(pop) + + +static int tga_transform(Tex* t, int new_flags) +{ + return TEX_CODEC_CANNOT_HANDLE; +} + + +// requirements: uncompressed, direct colour, bottom up +static int tga_decode(u8* file, size_t file_size, Tex* t, const char** perr_msg) +{ + TgaHeader* hdr = (TgaHeader*)file; + + // the first TGA header doesn't have a magic field; + // we can only check if the first 4 bytes are valid + // .. not direct colour + if(hdr->colour_map_type != 0) + return TEX_CODEC_CANNOT_HANDLE; + // .. wrong colour type (not uncompressed greyscale or RGB) + if(hdr->img_type != TGA_TRUE_COLOUR && hdr->img_type != TGA_GREY) + return TEX_CODEC_CANNOT_HANDLE; + + // make sure we can access all header fields + const size_t hdr_size = sizeof(TgaHeader) + hdr->img_id_len; + if(file_size < hdr_size) + { + *perr_msg = "header not completely read"; +fail: + return ERR_CORRUPTED; + } + + const u8 type = hdr->img_type; + const uint w = read_le16(&hdr->w); + const uint h = read_le16(&hdr->h); + const uint bpp = hdr->bpp; + const u8 desc = hdr->img_desc; + + const u8 alpha_bits = desc & 0x0f; + const int orientation = (desc & TGA_TOP_DOWN)? TEX_TOP_DOWN : TEX_BOTTOM_UP; + u8* const img = file + hdr_size; + const size_t img_size = w * h * bpp/8; + + + int flags = 0; + if(alpha_bits != 0) + flags |= TEX_ALPHA; + if(bpp == 8) + flags |= TEX_GREY; + if(type == TGA_TRUE_COLOUR) + flags |= TEX_BGR; + + // sanity checks + const char* err = 0; + // .. storing right-to-left is just stupid; + // we're not going to bother converting it. + if(desc & TGA_RIGHT_TO_LEFT) + err = "image is stored right-to-left"; + if(bpp != 8 && bpp != 16 && bpp != 24 && bpp != 32) + err = "invalid bpp"; + if(file_size < hdr_size + img_size) + err = "size < image size"; + if(err) + { + *perr_msg = err; + goto fail; + } + + t->ofs = hdr_size; + t->w = w; + t->h = h; + t->bpp = bpp; + t->flags = flags; + + tex_codec_set_orientation(t, orientation); + + return 0; +} + + +static int tga_encode(const char* ext, Tex* t, u8** out, size_t* out_size, const char** perr_msg) +{ + if(stricmp(ext, "tga")) + return TEX_CODEC_CANNOT_HANDLE; +/* + CHECK_ERR(tex_codec_validate_format(t->bpp, t->flags)); + + u8 img_desc = (t->flags & TEX_TOP_DOWN)? TGA_TOP_DOWN : TGA_BOTTOM_UP; + if(t->bpp == 32) + img_desc |= 8; // size of alpha channel + TgaImgType img_type = (t->flags & TEX_GREY)? TGA_GREY : TGA_TRUE_COLOUR; + + // transform + int transforms = t->flags; + transforms &= ~TEX_ORIENTATION; // no flip needed - we can set top-down bit. + transforms ^= TEX_BGR; // TGA is native BGR. + transform(t, transforms); + + TgaHeader hdr = + { + 0, // no image identifier present + 0, // no colour map present + (u8)img_type, + {0,0,0,0,0}, // unused (colour map) + 0, 0, // unused (origin) + t->w, + t->h, + t->bpp, + img_desc + }; + return write_img(fn, &hdr, sizeof(hdr), img, img_size); +*/ + return 0; +} + +TEX_CODEC_REGISTER(tga);