/* SETUP: * 1. STANDARD FORMATS (PNG, JPG, etc.) * 2. DOOM FORMATS (Gfx, flat.) * 3. OBSOLETE DOOM FORMATS (Alpha formats) * 4. HEXEN FORMATS (Planar-related) * 5. SOURCE PORT FORMATS (Legacy, ZDoom) * 6. OTHER GAMES FORMATS (Shadowcaster, Amulets & Armor, etc.) * 7. FONT FORMATS (including standard, Alpha and ZDoom fonts) *******************************************************************/ /******************************************************************* * STANDARD FORMATS *******************************************************************/ // Define grAb chunk struct struct grab_chunk_t { char name[4]; int32_t xoff; int32_t yoff; }; /* SImage::loadImage * Loads an image (if its format is supported by FreeImage) * Returns false if the given image data was invalid, true otherwise *******************************************************************/ bool SImage::loadImage(const uint8_t* img_data, int size) { // Check data if (!img_data) { Global::error = "Invalid data given"; return false; } // Check size if (size < 8) { Global::error = "Invalid data size"; return false; } // Reset offsets offset_x = 0; offset_y = 0; // Reset index numimages = 1; imgindex = 0; // Create FreeImage bitmap from entry data FIMEMORY* mem = FreeImage_OpenMemory((BYTE*)img_data, size); FREE_IMAGE_FORMAT fif = FreeImage_GetFileTypeFromMemory(mem, 0); FIBITMAP *bm = FreeImage_LoadFromMemory(fif, mem, 0); FreeImage_CloseMemory(mem); // Check it created/read ok if (!bm) { Global::error = "Unable to read image data (unsupported format?)"; return false; } // Get image palette if it exists RGBQUAD* bm_pal = FreeImage_GetPalette(bm); if (bm_pal) { has_palette = true; int a = 0; int b = FreeImage_GetColorsUsed(bm); if (b > 256) b = 256; for (; a < b; a++) palette.setColour(a, rgba_t(bm_pal[a].rgbRed, bm_pal[a].rgbGreen, bm_pal[a].rgbBlue, 255)); } else has_palette = false; // Get width & height width = FreeImage_GetWidth(bm); height = FreeImage_GetHeight(bm); // Set format type = RGBA; // Convert to 32bpp & flip vertically FIBITMAP *rgb = FreeImage_ConvertTo32Bits(bm); FreeImage_FlipVertical(rgb); // Clear current data if it exists clearData(); // Load raw RGBA data data = new uint8_t[width * height * 4]; uint8_t* bits_rgb = FreeImage_GetBits(rgb); int c = 0; for (int a = 0; a < width * height; a++) { data[c++] = bits_rgb[a * 4 + 2]; // Red data[c++] = bits_rgb[a * 4 + 1]; // Green data[c++] = bits_rgb[a * 4]; // Blue data[c++] = bits_rgb[a * 4 + 3]; // Alpha } // If the image was a PNG if (fif == FIF_PNG) { bool alPh_chunk = false; bool grAb_chunk = false; // Find offsets if present int32_t xoff = 0; int32_t yoff = 0; for (int a = 0; a < size; a++) { // Check for 'grAb' header if (!grAb_chunk && img_data[a] == 'g' && img_data[a + 1] == 'r' && img_data[a + 2] == 'A' && img_data[a + 3] == 'b') { memcpy(&xoff, img_data + a + 4, 4); memcpy(&yoff, img_data + a + 8, 4); xoff = wxINT32_SWAP_ON_LE(xoff); yoff = wxINT32_SWAP_ON_LE(yoff); grAb_chunk = true; } // Check for 'alPh' header if (!alPh_chunk && img_data[a] == 'a' && img_data[a + 1] == 'l' && img_data[a + 2] == 'P' && img_data[a + 3] == 'h') { alPh_chunk = true; } // Found both lumps so no need to search further if (grAb_chunk && alPh_chunk) break; // Stop when we get to the 'IDAT' chunk if (img_data[a] == 'I' && img_data[a + 1] == 'D' && img_data[a + 2] == 'A' && img_data[a + 3] == 'T') break; } offset_x = xoff; offset_y = yoff; // If the picture is nothing more than a ZDoom-style alpha map if (alPh_chunk) { unsigned c = 0; for (int a = 0; a < width * height; a++) { uint8_t alpha = data[c]; data[c++] = 0; data[c++] = 0; data[c++] = 0; data[c++] = alpha; } } } // Free memory FreeImage_Unload(rgb); FreeImage_Unload(bm); // Announce change and return success announce("image_changed"); return true; } /* SImage::toPNG * Writes the image as PNG data to , keeping palette information * if it exists. Returns true on success *******************************************************************/ bool SImage::toPNG(MemChunk& out, Palette8bit* pal) { FIBITMAP* bm; if (type == RGBA) { // Init 32bpp FIBITMAP bm = FreeImage_Allocate(width, height, 32, 0x0000FF00, 0x00FF0000, 0x000000FF); // Write image data uint8_t* bits = FreeImage_GetBits(bm); uint32_t c = 0; for (int a = 0; a < width * height * 4; a += 4) { bits[c++] = data[a+2]; bits[c++] = data[a+1]; bits[c++] = data[a]; bits[c++] = data[a+3]; } } else if (type == PALMASK) { // Init 8bpp FIBITMAP bm = FreeImage_Allocate(width, height, 8); // Get palette to use Palette8bit usepal; if (has_palette) usepal.copyPalette(&palette); else if (pal) usepal.copyPalette(pal); // Set palette RGBQUAD* bm_pal = FreeImage_GetPalette(bm); for (int a = 0; a < 256; a++) { bm_pal[a].rgbRed = usepal.colour(a).r; bm_pal[a].rgbGreen = usepal.colour(a).g; bm_pal[a].rgbBlue = usepal.colour(a).b; } // Handle transparency if needed if (mask) { // Find unused colour (for transparency) short unused = findUnusedColour(); // Set any transparent pixels to this colour (if we found an unused colour) if (unused >= 0) { for (int a = 0; a < width * height; a++) { if (mask[a] == 0) data[a] = unused; } // Set palette transparency usepal.setTransIndex(unused); } // Set freeimage palette transparency if needed if (usepal.transIndex() >= 0) FreeImage_SetTransparentIndex(bm, usepal.transIndex()); } // Write image data for (int row = 0; row < height; row++) { uint8_t* scanline = FreeImage_GetScanLine(bm, row); memcpy(scanline, data + (row * width), width); } } else return false; // Flip the image FreeImage_FlipVertical(bm); // Write the image to a temp file FreeImage_Save(FIF_PNG, bm, CHR(appPath("temp.png", DIR_TEMP))); // Load it into a memchunk MemChunk png; png.importFile(appPath("temp.png", DIR_TEMP)); // Check it loaded ok if (png.getSize() == 0) { wxLogMessage("Error reading temporary file"); return false; } // Write PNG header and IHDR const uint8_t* png_data = png.getData(); out.clear(); out.write(png_data, 33); // Create grAb chunk with offsets int32_t xoff = offset_x; int32_t yoff = offset_y; uint32_t csize = wxUINT32_SWAP_ON_LE(8); grab_chunk_t gc = { { 'g', 'r', 'A', 'b' }, wxINT32_SWAP_ON_LE(xoff), wxINT32_SWAP_ON_LE(yoff) }; uint32_t dcrc = wxUINT32_SWAP_ON_LE(Misc::crc((uint8_t*)&gc, 12)); // Write grAb chunk out.write(&csize, 4); out.write(&gc, 12); out.write(&dcrc, 4); // Write remaining PNG data out.write(png_data + 33, png.getSize() - 33); // Clean up wxRemoveFile(appPath("temp.png", DIR_TEMP)); // Success return true; } /******************************************************************* * DOOM FORMATS *******************************************************************/ // Doom Gfx format structs struct post_t { uint8_t row_off; vector pixels; }; struct column_t { vector posts; }; /* SImage::loadDoomGfx * Loads a Doom Gfx format image, using the image palette. [version] * is the gfx 'version' to load as (0=normal, 1=press beta, 2=alpha). * Returns false if the image data was invalid, true otherwise *******************************************************************/ bool SImage::loadDoomGfx(const uint8_t* gfx_data, int size, uint8_t version) { // Check data if (!gfx_data) return false; // Check size if ((unsigned)size < (version > 1 ? sizeof(oldpatch_header_t) : sizeof(patch_header_t))) return false; // Init variables uint32_t* col_offsets = NULL; type = PALMASK; has_palette = false; numimages = 1; imgindex = 0; // Read header uint8_t hdr_size = 0; if (version > 1) { width = gfx_data[0]; height = gfx_data[1]; offset_x = (int8_t)gfx_data[2]; offset_y = (int8_t)gfx_data[3]; hdr_size = 4; } else { patch_header_t* header = (patch_header_t*)gfx_data; width = wxINT16_SWAP_ON_BE(header->width); height = wxINT16_SWAP_ON_BE(header->height); offset_x = wxINT16_SWAP_ON_BE(header->left); offset_y = wxINT16_SWAP_ON_BE(header->top); hdr_size = 8; } // Read column offsets col_offsets = new uint32_t[width]; if (version > 0) { uint16_t* c_ofs = (uint16_t*)((uint8_t*)gfx_data + hdr_size); for (int a = 0; a < width; a++) col_offsets[a] = wxUINT16_SWAP_ON_BE(c_ofs[a]); } else { uint32_t* c_ofs = (uint32_t*)((uint8_t*)gfx_data + hdr_size); for (int a = 0; a < width; a++) col_offsets[a] = wxUINT32_SWAP_ON_BE(c_ofs[a]); } // Clear current data if it exists clearData(); // Load data data = new uint8_t[width * height]; memset(data, 0, width * height); // Set colour to palette index 0 mask = new uint8_t[width * height]; memset(mask, 0, width * height); // Set mask to fully transparent for (int c = 0; c < width; c++) { // Get current column offset (byteswap if needed) uint32_t col_offset = col_offsets[c];//wxUINT32_SWAP_ON_BE(col_offsets[c]); // Check column offset is valid if (col_offset >= (unsigned)size) { clearData(); return false; } // Go to start of column const uint8_t* bits = gfx_data; bits += col_offset; // Read posts int top = -1; while (1) { // Get row offset uint8_t row = *bits; if (row == 0xFF) // End of column? break; // Tall patches support if (row <= top && version == 0) top += row; else top = row; // Get no. of pixels bits++; uint8_t n_pix = *bits; if (version == 0) bits++; // Skip buffer for (uint8_t p = 0; p < n_pix; p++) { // Get pixel position bits++; int pos = ((top + p)*width + c); // Stop if we're outside the image if (pos > width*height) break; // Stop if for some reason we're outside the gfx data if (bits > gfx_data + size) break; // Fail if bogus data gives a negative pos (this corrupts the heap!) if (pos < 0) { clearData(); return false; } // Write pixel data data[pos] = *bits; mask[pos] = 255; } if (version == 0) bits++; // Skip buffer bits++; // Go to next row offset } } // Announce change and return success announce("image_changed"); return true; } /* SImage::toDoomGfx * Writes the image as Doom Gfx data to . Returns false if the * image is not paletted, true otherwise *******************************************************************/ bool SImage::toDoomGfx(MemChunk& out, uint8_t alpha_threshold) { // Check if data is paletted if (type != PALMASK) { wxLogMessage("Cannot convert truecolour image to doom gfx format - convert to 256-colour first."); return false; } // Convert image to column/post structure vector columns; // Go through columns uint32_t offset = 0; for (int c = 0; c < width; c++) { column_t col; post_t post; post.row_off = 0; bool ispost = false; bool first_254 = true; // First 254 pixels should use absolute offsets offset = c; uint8_t row_off = 0; for (int r = 0; r < height; r++) { // If we're at offset 254, create a dummy post for tall doom gfx support if (row_off == 254) { // Finish current post if any if (ispost) { col.posts.push_back(post); post.pixels.clear(); ispost = false; } // Begin relative offsets first_254 = false; // Create dummy post post.row_off = 254; col.posts.push_back(post); // Clear post row_off = 0; ispost = false; } // If the current pixel is not transparent, add it to the current post if (mask[offset] > alpha_threshold) { // If we're not currently building a post, begin one and set its offset if (!ispost) { // Set offset post.row_off = row_off; // Reset offset if we're in relative offsets mode if (!first_254) row_off = 0; // Start post ispost = true; } // Add the pixel to the post post.pixels.push_back(data[offset]); } else if (ispost) { // If the current pixel is transparent and we are currently building // a post, add the current post to the list and clear it col.posts.push_back(post); post.pixels.clear(); ispost = false; } // Go to next row offset += width; row_off++; } // If the column ended with a post, add it if (ispost) col.posts.push_back(post); // Add the column data columns.push_back(col); // Go to next column offset++; } // Write doom gfx data to output out.clear(); out.seek(0, SEEK_SET); // Setup header patch_header_t header; header.top = offset_y; header.left = offset_x; header.width = getWidth(); header.height = getHeight(); // Byteswap header values if needed header.top = wxINT16_SWAP_ON_BE(header.top); header.left = wxINT16_SWAP_ON_BE(header.left); header.width = wxINT16_SWAP_ON_BE(header.width); header.height = wxINT16_SWAP_ON_BE(header.height); // Write it out.write(&header.width, 2); out.write(&header.height, 2); out.write(&header.left, 2); out.write(&header.top, 2); // Write dummy column offsets for now uint32_t *col_offsets = new uint32_t[columns.size()]; out.write(col_offsets, columns.size() * 4); // Write columns for (size_t c = 0; c < columns.size(); c++) { // Record column offset col_offsets[c] = wxUINT32_SWAP_ON_BE(out.currentPos()); // Determine column size (in bytes) uint32_t col_size = 0; for (size_t p = 0; p < columns[c].posts.size(); p++) col_size += columns[c].posts[p].pixels.size() + 4; // Allocate memory to write the column data out.reSize(out.getSize() + col_size, true); // Write column posts for (size_t p = 0; p < columns[c].posts.size(); p++) { // Write row offset out.write(&columns[c].posts[p].row_off, 1); // Write no. of pixels uint8_t npix = columns[c].posts[p].pixels.size(); out.write(&npix, 1); // Write unused byte uint8_t temp = 0; out.write(&temp, 1); // Write pixels for (size_t a = 0; a < columns[c].posts[p].pixels.size(); a++) out.write(&columns[c].posts[p].pixels[a], 1); // Write unused byte out.write(&temp, 1); } // Write '255' row to signal end of column uint8_t temp = 255; out.write(&temp, 1); } // Now we write column offsets out.seek(8, SEEK_SET); out.write(col_offsets, columns.size() * 4); return true; } // Define valid flat sizes uint32_t valid_flat_size[][3] = { { 2, 2, 0 }, // lol Heretic F_SKY1 { 10, 12, 0 }, // gnum format { 16, 16, 0 }, // | { 32, 64, 0 }, // Strife startup sprite { 48, 48, 0 }, // | { 64, 64, 1 }, // standard flat size { 64, 65, 0 }, // Heretic flat size variant { 64, 128, 0 }, // Hexen flat size variant { 128, 128, 1 }, // | { 256, 200, 0 }, // Rise of the Triad sky { 256, 256, 1 }, // hires flat size { 320, 200, 0 }, // full screen format { 512, 512, 1 }, // hires flat size {1024,1024, 1 }, // | {2048,2048, 1 }, // super hires flat size (SRB2) {4096,4096, 1 }, // | }; uint32_t n_valid_flat_sizes = 17; /* SImage::validFlatSize * Returns whether the image has dimensions appropriate for a flat * or another raw-type image (including things such as AUTOPAGE). *******************************************************************/ bool SImage::validFlatSize() { for (uint32_t a = 0; a < n_valid_flat_sizes; a++) { if (width == valid_flat_size[a][0] && height == valid_flat_size[a][1]) return true; } return (width == 320 || width == 256); } // FIXME: width == 256 is a temporary hack to handle translation tables. /* SImage::loadDoomFlat * Loads a Doom Flat format image, using the given palette. * Also loads about any raw image format that's the same as flats, * but with a different dimension. * Returns false if the image data was invalid, true otherwise. *******************************************************************/ bool SImage::loadDoomFlat(const uint8_t* gfx_data, int size, bool columnmajor) { // Check data if (!gfx_data) return false; // Check/setup size bool valid_size = false; for (uint32_t a = 0; a < n_valid_flat_sizes; a++) { uint32_t w = valid_flat_size[a][0]; uint32_t h = valid_flat_size[a][1]; if (size == w * h || size - 4 == w * h) { width = w; height = h; valid_size = true; break; } } if (size == 8776) { // Inkworks and its signature at the end of COLORMAPS size = 8704; } // Check valid size if (!valid_size) { if (size % 320 == 0) { // This should handle any custom AUTOPAGE width = 320; height = size/320; } else if (size % 256 == 0) { // This allows display of COLORMAPS width = 256; height = size/256; } else { return false; } } // Swap height and width for column-major formats if (columnmajor) { int tmp = width; width = height; height = tmp; } // Setup variables type = PALMASK; has_palette = false; numimages = 1; imgindex = 0; // Clear current data if it exists clearData(); // Read raw pixel data data = new uint8_t[width*height]; memcpy(data, gfx_data, width * height); // Create mask (all opaque) mask = new uint8_t[width*height]; memset(mask, 255, width*height); // Handle column-major flat formats if (columnmajor) { rotate(90); mirror(true); } // Announce change and return success announce("image_changed"); return true; } /* SImage::toDoomFlat * Writes the image as Doom Flat data to . Returns false if the * image is not paletted or the image is not a valid size (64x64, * 64x128, 128x128, 256x256), true otherwise *******************************************************************/ bool SImage::toDoomFlat(MemChunk& out) { // Check if data is paletted if (type != PALMASK) { wxLogMessage("Cannot convert truecolour image to doom flat format - convert to 256-colour first."); return false; } // Check image size if (!validFlatSize()) { wxLogMessage("Cannot convert to doom flat format, invalid size (must be either 64x64, 64x128, 128x128 or 320x200)"); return false; } // If it's paletted simply write out the data straight out.write(data, width * height); return true; } /******************************************************************* * OBSOLETE DOOM FORMATS *******************************************************************/ /* SImage::loadDoomArah * Loads a Doom Alpha Raw And Header image. * Returns false if the image data was invalid, true otherwise. * It seems index 255 is treated as transparent in this format. *******************************************************************/ bool SImage::loadDoomArah(const uint8_t* gfx_data, int size, int transindex) { // Check data if (!gfx_data) return false; // Setup variables patch_header_t* header = (patch_header_t*)gfx_data; width = wxINT16_SWAP_ON_BE(header->width); height = wxINT16_SWAP_ON_BE(header->height); offset_x = wxINT16_SWAP_ON_BE(header->left); offset_y = wxINT16_SWAP_ON_BE(header->top); type = PALMASK; has_palette = false; numimages = 1; imgindex = 0; // Clear current data if it exists clearData(); // Read raw pixel data data = new uint8_t[width*height]; memcpy(data, gfx_data+8, width * height); // Create mask (all opaque) mask = new uint8_t[width*height]; memset(mask, 255, width*height); // Mark as transparent all pixels that are set to 'transindex' for (size_t i = 0; i < (unsigned)(width*height); ++i) if (data[i] == transindex) mask[i] = 0; // Announce change and return success announce("image_changed"); return true; } /* SImage::loadDoomSnea * Loads a Doom alpha "snea" format image, using the given palette. * I have no idea if "snea" if the official name of the format, or * even what it is supposed to mean, but that's how DeuTex calls * them, and it's the only available tool that can read them as far * as I know, so this name will do. * Returns false if the image data was invalid, true otherwise. *******************************************************************/ bool SImage::loadDoomSnea(const uint8_t* gfx_data, int size) { // Check data if (!gfx_data) return false; // Check/setup size offset_x = offset_y = 0; uint8_t qwidth = gfx_data[0]; width = qwidth * 4; height = gfx_data[1]; // The TITLEPIC in the Doom Press-Release Beta has // two extraneous null bytes at the end for padding. if (size == width * height + 4) size -= 2; if (size != 2 + width * height) return false; // Setup variables type = PALMASK; has_palette = false; numimages = 1; imgindex = 0; // Clear current data if it exists clearData(); // Read raw pixel data data = new uint8_t[width*height]; // Since gfx_data is a const pointer, we can't work on it. uint8_t * tempdata = new uint8_t[size]; memcpy(tempdata, gfx_data, size); uint8_t* entryend = tempdata + size; uint8_t* dataend = data + size - 2; uint8_t* pixel = tempdata + 2; uint8_t* brush = data; // Algorithm taken from DeuTex. // I do not pretend to understand it, // but my own attempt didn't work. while (pixel < entryend) { *brush = *pixel++; brush += 4; if (brush >= dataend) brush -= size - 3; } delete[] tempdata; // Create mask (all opaque) mask = new uint8_t[width*height]; memset(mask, 255, width*height); // Announce change and return success announce("image_changed"); return true; } /******************************************************************* * HEXEN FORMATS *******************************************************************/ /* SImage::loadPlanar * Loads a planar graphic such as those used by Hexen's startup screen. * This was a convenient format back then as it could be sent directly * to the VGA memory; however this is now both obscure and obsolete. * The code is taken from ZDoom. * Returns false if the image data was invalid, true otherwise. *******************************************************************/ bool SImage::loadPlanar(const uint8_t* gfx_data, int size) { // Check data if (!gfx_data) return false; // Check size if (size != 153648) return false; // Init some variables width = 640; height = 480; type = PALMASK; has_palette = true; numimages = 1; imgindex = 0; union { RGBQUAD color; uint32_t quad; }; color.rgbReserved = 0; rgba_t colour(0, 0, 0, 0, -1); // Initialize the bitmap palette. for (int i = 0; i < 16; ++i) { color.rgbRed = gfx_data[i*3+0]; color.rgbGreen = gfx_data[i*3+1]; color.rgbBlue = gfx_data[i*3+2]; // Convert from 6-bit per component to 8-bit per component. quad = (quad << 2) | ((quad >> 4) & 0x03030303); colour.r = color.rgbRed; colour.g = color.rgbGreen; colour.b = color.rgbBlue; palette.setColour(i, colour); } // Fill the rest of the palette with clones of index 0 for (int i = 16; i<256; ++i) { palette.setColour(i, palette.colour(0)); } // Prepare data and mask (opaque) clearData(); data = new uint8_t[width*height]; mask = new uint8_t[width*height]; memset(mask, 0xFF, width*height); uint8_t * dest = data; int y, x; const uint8_t *src1, *src2, *src3, *src4; size_t plane_size = width / 8 * height; src1 = gfx_data + 48; // 80: 10000000 08: 00001000 src2 = src1 + plane_size; // 40: 01000000 04: 00000100 src3 = src2 + plane_size; // 20: 00100000 02: 00000010 src4 = src3 + plane_size; // 10: 00010000 01: 00000001 for (y = height; y > 0; --y) { for (x = width; x > 0; x -= 8) { dest[0] = ((*src4 & 0x80) >> 4) | ((*src3 & 0x80) >> 5) | ((*src2 & 0x80) >> 6) | ((*src1 & 0x80) >> 7); dest[1] = ((*src4 & 0x40) >> 3) | ((*src3 & 0x40) >> 4) | ((*src2 & 0x40) >> 5) | ((*src1 & 0x40) >> 6); dest[2] = ((*src4 & 0x20) >> 2) | ((*src3 & 0x20) >> 3) | ((*src2 & 0x20) >> 4) | ((*src1 & 0x20) >> 5); dest[3] = ((*src4 & 0x10) >> 1) | ((*src3 & 0x10) >> 2) | ((*src2 & 0x10) >> 3) | ((*src1 & 0x10) >> 4); dest[4] = ((*src4 & 0x08) ) | ((*src3 & 0x08) >> 1) | ((*src2 & 0x08) >> 2) | ((*src1 & 0x08) >> 3); dest[5] = ((*src4 & 0x04) << 1) | ((*src3 & 0x04) ) | ((*src2 & 0x04) >> 1) | ((*src1 & 0x04) >> 2); dest[6] = ((*src4 & 0x02) << 2) | ((*src3 & 0x02) << 1) | ((*src2 & 0x02) ) | ((*src1 & 0x02) >> 1); dest[7] = ((*src4 & 0x01) << 3) | ((*src3 & 0x01) << 2) | ((*src2 & 0x01) << 1) | ((*src1 & 0x01) ); dest += 8; src1 += 1; src2 += 1; src3 += 1; src4 += 1; } } // Announce change and return success announce("image_changed"); return true; } /* SImage::toPlanar * Writes the image as planar data to . Returns false if the * image is not a valid candidate (16 colors max, 640x480 dimension), * true otherwise *******************************************************************/ bool SImage::toPlanar(MemChunk& out, Palette8bit* pal) { // Check if data is paletted if (type != PALMASK) { wxLogMessage("Cannot convert truecolour image to planar format - convert to 16-colour first."); return false; } if (countColours() > 16) { wxLogMessage(S_FMT("Cannot convert to planar format, too many colors (%d)"), countColours()); return false; } // Check image size if (!(width == 640 && height == 480)) { wxLogMessage("Cannot convert to planar format, invalid size (must be 640x480)"); return false; } // Get palette to use Palette8bit usepal; if (has_palette) usepal.copyPalette(&palette); else if (pal) usepal.copyPalette(pal); // Backup current image data (since shrinpPalette remaps the image colours) MemChunk backup(width*height); backup.write(data, width*height); // Make sure all used colors are in the first 16 entries of the palette shrinkPalette(&usepal); // Create planar palette uint8_t * mycolors = new uint8_t[3]; for (size_t i = 0; i < 16; ++i) { mycolors[0] = usepal.colour(i).r>>2; mycolors[1] = usepal.colour(i).g>>2; mycolors[2] = usepal.colour(i).b>>2; out.write(mycolors, 3); } delete[] mycolors; // Create bitplanes uint8_t * planes = new uint8_t[153600]; uint8_t *pln1, *pln2, *pln3, *pln4, *read; size_t plane_size = 153600/4; read = data; pln1 = planes; // 80: 10000000 08: 00001000 pln2 = pln1 + plane_size; // 40: 01000000 04: 00000100 pln3 = pln2 + plane_size; // 20: 00100000 02: 00000010 pln4 = pln3 + plane_size; // 10: 00010000 01: 00000001 for (int y = height; y > 0; --y) { for (int x = width; x > 0; x -= 8) { *pln1 = ((read[0] & 0x01) << 7 | (read[1] & 0x01) << 6 | (read[2] & 0x01) << 5 | (read[3] & 0x01) << 4 |(read[4] & 0x01) << 3 | (read[5] & 0x01) << 2 | (read[6] & 0x01) << 1 | (read[7] & 0x01) ); *pln2 = ((read[0] & 0x02) << 6 | (read[1] & 0x02) << 5 | (read[2] & 0x02) << 4 | (read[3] & 0x02) << 3 |(read[4] & 0x02) << 2 | (read[5] & 0x02) << 1 | (read[6] & 0x02) | (read[7] & 0x02) >> 1); *pln3 = ((read[0] & 0x04) << 5 | (read[1] & 0x04) << 4 | (read[2] & 0x04) << 3 | (read[3] & 0x04) << 2 |(read[4] & 0x04) << 1 | (read[5] & 0x04) | (read[6] & 0x04) >> 1 | (read[7] & 0x04) >> 2); *pln4 = ((read[0] & 0x08) << 4 | (read[1] & 0x08) << 3 | (read[2] & 0x08) << 2 | (read[3] & 0x08) << 1 |(read[4] & 0x08) | (read[5] & 0x08) >> 1 | (read[6] & 0x08) >> 2 | (read[7] & 0x08) >> 3); read += 8; pln1 += 1; pln2 += 1; pln3 += 1; pln4 += 1; } } // Write image and cleanup out.write(planes, 153600); delete[] planes; backup.seek(0, SEEK_SET); backup.read(data, width*height); return true; } /* SImage::load4bitChunk * Loads a 4bit graphic such as those used by Hexen's startup screen. * Returns false if the image data was invalid, true otherwise. *******************************************************************/ bool SImage::load4bitChunk(const uint8_t* gfx_data, int size) { // Check data if (!gfx_data) return false; // Check size if (size == 32) { width = 4; height = 16; } else if (size == 184) { width = 16; height = 23; } else return false; type = PALMASK; has_palette = false; numimages = 1; imgindex = 0; clearData(); data = new uint8_t[width*height]; mask = new uint8_t[width*height]; memset(mask, 0xFF, width*height); for (int i = 0; i < size; ++i) { data[i*2 ] = ((gfx_data[i] & 0xF0)>>4); data[i*2+1] = (gfx_data[i] & 0x0F); } // Announce change and return success announce("image_changed"); return true; } /* SImage::to4bitChunk * Writes the image as 4-bit chunky data to . Returns false if the * image is not a valid candidate, true otherwise *******************************************************************/ bool SImage::to4bitChunk(MemChunk& out, Palette8bit* pal) { // Check if data is paletted if (type != PALMASK) { wxLogMessage("Cannot convert truecolour image to 4-bit format - convert to 16-colour first."); return false; } if (countColours() > 16) { wxLogMessage(S_FMT("Cannot convert to 4-bit format, too many colors (%d)"), countColours()); return false; } // Check image size if (!((width == 4 && height == 16) || (width == 16 && height == 23))) { wxLogMessage("No point in converting to 4-bit format, image isn't a valid Hexen size (4x16 or 16x23)"); return false; } // Get palette to use Palette8bit usepal; if (has_palette) usepal.copyPalette(&palette); else if (pal) usepal.copyPalette(pal); // Backup current image data (since shrinpPalette remaps the image colours) MemChunk backup(width*height); backup.write(data, width*height); // Make sure all used colors are in the first 16 entries of the palette shrinkPalette(); size_t filesize = width * height / 2; uint8_t * temp = new uint8_t[filesize]; for (int i = 0; i < width * height; i+=2) { temp[i/2] = data[i]<<4 | data[i+1]; } // Write image and cleanup out.write(temp, filesize); delete[] temp; backup.seek(0, SEEK_SET); backup.read(data, width*height); return true; } /******************************************************************* * SOURCE PORT FORMATS *******************************************************************/ /* SImage::loadImgz * Loads a picture in ZDoom's imgz format, using code adapted from it. * This format is special in that the info given is only for the alpha * channel, there is no color information at all. * Returns false if the image data was invalid, true otherwise. *******************************************************************/ bool SImage::loadImgz(const uint8_t* gfx_data, int size) { if (size < sizeof(imgz_header_t)) return false; // Setup variables imgz_header_t *header = (imgz_header_t *)gfx_data; width = wxINT16_SWAP_ON_BE(header->width); height = wxINT16_SWAP_ON_BE(header->height); offset_x = wxINT16_SWAP_ON_BE(header->left); offset_y = wxINT16_SWAP_ON_BE(header->top); has_palette = false; type = PALMASK; numimages = 1; imgindex = 0; // Create data (all white) and mask clearData(); data = new uint8_t[width*height]; mask = new uint8_t[width*height]; memset(data, 0xFF, width*height); if (!header->compression) { memcpy(mask, gfx_data + sizeof(imgz_header_t), size - sizeof(imgz_header_t)); // Announce change and return success announce("image_changed"); return true; } else { // Since gfx_data is a const pointer, we can't work on it. uint8_t * tempdata = new uint8_t[size]; memcpy(tempdata, gfx_data, size); // We'll use wandering pointers. The original pointer is kept for cleanup. uint8_t * read = tempdata + sizeof(imgz_header_t); uint8_t * readend = read + size - 1; uint8_t * dest = mask; uint8_t * destend = dest + width*height; uint8_t code = 0; size_t length = 0; while (read < readend && dest < destend) { code = *read++; if (code < 0x80) { length = code + 1; memcpy(dest, read, length); dest+=length; read+=length; } else if (code > 0x80) { length = 0x101 - code; code = *read++; memset(dest, code, length); dest+=length; } } delete[] tempdata; } // Announce change and return success announce("image_changed"); return true; }; /******************************************************************* * OTHER GAMES FORMATS *******************************************************************/ /* SImage::loadQuake * Loads a graphic in the Quake HUD picture format. * Returns false if the image data was invalid, true otherwise. *******************************************************************/ enum QuakePicType { QUAKE_PALETTE = 0, QUAKE_INTENSITY = 1, QUAKE_ALPHA = 2, QUAKE_RGB24 = 3, QUAKE_RGB32 = 4 }; bool SImage::loadQuake(const uint8_t* gfx_data, int size) { // Check data if (!gfx_data) return false; // Setup variables width = wxINT16_SWAP_ON_BE(*(const uint16_t*)(gfx_data)); height = wxINT16_SWAP_ON_BE(*(const uint16_t*)(gfx_data+4)); uint8_t mode = gfx_data[3]; offset_x = 0; offset_y = 0; numimages = 1; imgindex = 0; if (mode < QUAKE_RGB24) type = PALMASK; else type = RGBA; if (mode == 0 || mode > QUAKE_ALPHA) has_palette = false; else { // Picture is an intensity map, so use greyscale palette has_palette = true; for (size_t i = 0; i < 256; ++i) { palette.setColour(i, rgba_t(i, i, i, 255)); } } // Clear current data if it exists clearData(); if (mode == QUAKE_PALETTE) { // Read raw pixel data data = new uint8_t[width*height]; memcpy(data, gfx_data+8, width * height); // Create mask (all opaque) mask = new uint8_t[width*height]; memset(mask, 255, width*height); // Index 255 is transparent for (int i = 0; i < width*height; ++i) if (data[i] == 255) mask[i] = 0; } else if (mode == QUAKE_INTENSITY) { // Read raw pixel data data = mask = new uint8_t[width*height]; memcpy(data, gfx_data+8, width * height); } else if (mode == QUAKE_ALPHA) { // Read raw pixel data data = new uint8_t[width*height]; mask = new uint8_t[width*height]; for (int i = 0; i < size - 8; i+=2) { data[i/2] = gfx_data[i+8]; mask[i/2] = gfx_data[i+9]; } } else if (mode == QUAKE_RGB24) { // Read raw pixel data data = new uint8_t[width*height*4]; for (int i = 0; i < size - 8; i+=3) { data[(i/3)*4 ] = gfx_data[i+ 8]; data[(i/3)*4+1] = gfx_data[i+ 9]; data[(i/3)*4+2] = gfx_data[i+10]; data[(i/3)*4+3] = 255; } } else if (mode == QUAKE_RGB32) { // Read raw pixel data data = new uint8_t[width*height*4]; memcpy(data, gfx_data+8, width * height * 4); } // Announce change and return success announce("image_changed"); return true; } /* SImage::loadQuakeSprite * Loads a picture in Quake's sprite format. * Returns false if the image data was invalid, true otherwise. *******************************************************************/ bool SImage::loadQuakeSprite(const uint8_t* gfx_data, int size, int index) { if (size < 64) return false; uint32_t maxheight = READ_L32(gfx_data, 16); uint32_t maxwidth = READ_L32(gfx_data, 20); uint32_t nframes = READ_L32(gfx_data, 24); // Setup variables numimages = nframes; has_palette = false; type = PALMASK; // Makes sum of frame pictures, not just frames // Also gather offsets in a data structure vector pics; uint32_t imgofs = 36; for (size_t a = 0; a < nframes; ++a) { if (READ_L32(gfx_data, imgofs) != 0) { // We have a frame with a group of picture uint32_t grpsz = READ_L32(gfx_data, imgofs + 4); if (grpsz == 0) { Global::error = "Quake sprite data contains empty group"; return false; } // Move to end of group header imgofs += (grpsz+2)<<2; for (size_t b = 0; b < grpsz; ++b) { uint32_t pw = READ_L32(gfx_data, imgofs + 8); uint32_t ph = READ_L32(gfx_data, imgofs + 12); // Store image offset pics.push_back(imgofs); // Move to end of picture data imgofs += 16 + pw * ph; if (imgofs > (unsigned) size) { Global::error = "Quake sprite data too short"; return false; } } // We must have made it through all the group's picture, // so replace the count for the group (1) by the group's size. numimages += (grpsz - 1); } else { // We have a frame with a single picture imgofs += 4; uint32_t pw = READ_L32(gfx_data, imgofs + 8); uint32_t ph = READ_L32(gfx_data, imgofs + 12); // Store image offset pics.push_back(imgofs); // Move to end of picture data imgofs += 16 + pw * ph; } if (imgofs > (unsigned) size) { Global::error = "Quake sprite data too short"; return false; } } // Sanitize index if needed index %= numimages; if (index < 0) index = numimages + index; imgindex = index; // Setup variables using appropriate image data imgofs = pics[index]; offset_x = READ_L32(gfx_data, imgofs + 0); offset_y = READ_L32(gfx_data, imgofs + 4); width = READ_L32(gfx_data, imgofs + 8); height = READ_L32(gfx_data, imgofs +12); // Horizontal offsets seem computed differently from Doom, so translate them offset_x += width; // Clear current data if it exists clearData(); pics.clear(); // Load image data data = new uint8_t[width * height]; mask = new uint8_t[width * height]; memcpy(data, gfx_data + imgofs + 16, width * height); memset(mask, 0xFF, width * height); for (int a = 0; a < width * height; ++a) if (data[a] == 0xFF) mask[a] = 0; // Announce change and return success announce("image_changed"); return true; } /* SImage::loadQuakeTex * Loads a picture in Quake's texture format. * Returns false if the image data was invalid, true otherwise. *******************************************************************/ bool SImage::loadQuakeTex(const uint8_t* gfx_data, int size, int index) { if (size < 125) return false; // Setup variables numimages = 4; has_palette = false; type = PALMASK; width = READ_L32(gfx_data, 16); height = READ_L32(gfx_data, 20); // Sanitize index if needed index %= numimages; if (index < 0) index = numimages + index; imgindex = index; // Adjust variables according to mip level if (index) { width >>= index; height >>= index; } // Find offset uint32_t imgofs = READ_L32(gfx_data, 24+(index<<2)); // Clear current data if it exists clearData(); // Load image data data = new uint8_t[width * height]; memcpy(data, gfx_data + imgofs, width * height); // Announce change and return success announce("image_changed"); return true; } /* SImage::loadSCSprite * Loads a sprite in ShadowCaster's format. * Returns false if the image data was invalid, true otherwise. *******************************************************************/ bool SImage::loadSCSprite(const uint8_t* gfx_data, int size) { // Check data if (!gfx_data) return false; if (size < 3) return false; width = READ_L16(gfx_data, 2); if (width == 0) return false; height = 0; for (int j = 0; j < width; ++j) { int colstart = READ_L16(gfx_data, ((j<<1)+4)); // Columns with a null offset are skipped if (colstart == 0) continue; if (colstart < 0 || size < colstart+2 || colstart < (width*2+4)) { return false; } int start = gfx_data[colstart]; int stop = gfx_data[colstart+1]; int colheight = start - stop; if (colheight < 0 || size < colstart+colheight+1) { return false; } if (start > height) height = start; } if (height == 0) return false; // Clear current data if it exists clearData(); // Setup variables type = PALMASK; has_palette = false; numimages = 1; imgindex = 0; // Format has no offsets, so just set them automatically offset_x = width/2; offset_y = height; // Create raw pixel data int bound = width*height; data = new uint8_t[bound]; memset(data, 0, bound); mask = new uint8_t[bound]; memset(mask, 0, bound); // Read pixel data for (int h = 0, i = 4; h < width; ++h, i+=2) { int colstart = READ_L16(gfx_data, i); if (colstart) { int start = gfx_data[colstart]; int stop = gfx_data[colstart+1]; int colheight = start - stop; int startheight = height - start; for (int z = 0; z < colheight; ++z) { int mypixel = ((z+startheight)*width)+h; if (mypixel >= bound || (colstart+2+z) >= size) return false; if (mypixel < 0) return false; data[mypixel] = gfx_data[colstart+2+z]; if (data[mypixel]) mask[mypixel] = 0xFF; } } } // Announce change and return success announce("image_changed"); return true; } /* SImage::loadSCWall * Loads a wall in ShadowCaster's format. * Returns false if the image data was invalid, true otherwise. *******************************************************************/ #define SCWALLOFFSET 130 // Headers contain 129 bytes of junk. bool SImage::loadSCWall(const uint8_t* gfx_data, int size) { if (!size) return false; height = gfx_data[0]*4; width = 64; if (size != width * height + SCWALLOFFSET) return false; // Clear current data if it exists clearData(); data = new uint8_t[width*height]; memset(data, 0xFF, width*height); mask = new uint8_t[width*height]; memset(mask, 0xFF, width*height); // Setup variables type = PALMASK; has_palette = false; numimages = 1; imgindex = 0; // Read pixel data int pixelreader = SCWALLOFFSET; int brush = 0; for (int x = 0; x < width; ++x) { for (int y = 0; y < height; ++y, ++pixelreader) { brush = (y*64)+x; data[brush] = gfx_data[pixelreader]; if (data[brush] == 0) mask[brush] = 0; } } // Announce change and return success announce("image_changed"); return true; } /* SImage::loadAnaMip * Loads a picture in Amulets & Armor's format. * Returns false if the image data was invalid, true otherwise. *******************************************************************/ bool SImage::loadAnaMip(const uint8_t* gfx_data, int size) { if (size < 4) return false; // Setup variables width = READ_L16(gfx_data, 0); height = READ_L16(gfx_data, 2); has_palette = false; type = PALMASK; // Technically false, as there are multiple mipmap levels. // May implement them later. Not in any hurry about them. numimages = 1; imgindex = 0; if (size < (4 + (width * height))) return false; // Create data (all white) and mask clearData(); data = new uint8_t[width*height]; mask = new uint8_t[width*height]; memset(mask, 0xFF, width*height); memcpy(data, gfx_data + 4, width*height); // Announce change and return success announce("image_changed"); return true; } /* SImage::loadBuildTile * Loads a picture from an ART tile collection. * Returns false if the image data was invalid, true otherwise. *******************************************************************/ bool SImage::loadBuildTile(const uint8_t* gfx_data, int size, int index) { if (size < 16) return false; uint32_t firsttile = wxUINT32_SWAP_ON_BE(((uint32_t*)gfx_data)[2]); uint32_t lasttile = wxUINT32_SWAP_ON_BE(((uint32_t*)gfx_data)[3]); // Setup variables numimages = 1 + lasttile - firsttile; has_palette = false; type = PALMASK; // Sanitize index if needed index %= numimages; if (index < 0) index = numimages + index; // Each tile has a 2-byte width, a 2-byte height, and a 4-byte // picanm struct. The header itself is 16 bytes. size_t x_offs = 16; size_t y_offs = x_offs + (numimages<<1); size_t o_offs = y_offs + (numimages<<1); // Compute the address where our tile's graphic data starts size_t datastart = (numimages * 8) + 16; if (index > 0) { // We can skip these steps if looking at the first tile in the ART file. for (int i = 0; i < index; ++i) { width = READ_L16(gfx_data, (x_offs+(i<<1))); height = READ_L16(gfx_data, (y_offs+(i<<1))); datastart += (width * height); } // Increment values to that of the tile we want x_offs += (index<<1); y_offs += (index<<1); o_offs += (index<<2); } if ((unsigned)size < datastart) return false; // Setup variables // Little cheat here as they are defined in column-major format, // not row-major. So we'll just call the rotate function. height = READ_L16(gfx_data, x_offs); width = READ_L16(gfx_data, y_offs); if ((unsigned)size < (datastart+(width*height))) return false; // Offsets are signed bytes, so they need a cast offset_x = (int8_t)gfx_data[o_offs+1]; offset_y = (int8_t)gfx_data[o_offs+2]; imgindex = index; // Create data clearData(); data = new uint8_t[width*height]; // Fill data with pixel data memcpy(data, gfx_data + datastart, width * height); // Convert from column-major to row-major rotate(90); mirror(true); // Create mask mask = new uint8_t[width*height]; for (int i = 0; i < width*height; ++i) { if (data[i] == 0xFF) mask[i] = 0; else mask[i] = 0xFF; } // Offsets are not computed from the same reference point, so convert them offset_x += (width>>1); offset_y += height; // Announce change and return success announce("image_changed"); return true; } /* SImage::loadHeretic2M8 * Loads a Heretic II mipmapped paletted image. * Returns false if the image data was invalid, true otherwise. *******************************************************************/ bool SImage::loadHeretic2M8(const uint8_t* gfx_data, int size, int index) { if (size < 1040) return false; const uint32_t * g_data = (uint32_t *) gfx_data; int i = 9; while (i < 25 && g_data[i] != 0) ++i; numimages = i - 9; has_palette = true; type = PALMASK; // Sanitize index if needed index %= numimages; if (index < 0) index = numimages + index; // Setup variables imgindex = index; width = wxUINT32_SWAP_ON_BE(g_data[index+9]); height = wxUINT32_SWAP_ON_BE(g_data[index+25]); size_t data_offset = wxUINT32_SWAP_ON_BE(g_data[index+41]); if (!width || !height || ! data_offset || size < (int)data_offset + (width*height)) { Global::error = "M8 file: invalid data for mip level"; return false; } // Let's build the palette now. for (size_t c = 0; c < 256; ++c) { rgba_t color; color.r = gfx_data[(c*3)+0x104]; color.g = gfx_data[(c*3)+0x105]; color.b = gfx_data[(c*3)+0x106]; palette.setColour(c, color); } // Create data clearData(); data = new uint8_t[width*height]; // Fill data with pixel data memcpy(data, gfx_data + data_offset, width * height); // Announce change and return success announce("image_changed"); return true; } /* SImage::loadHeretic2M32 * Loads a Heretic II mipmapped truecolor image. * Returns false if the image data was invalid, true otherwise. *******************************************************************/ bool SImage::loadHeretic2M32(const uint8_t* gfx_data, int size, int index) { if (size < 968) return false; const uint32_t * g_data = (uint32_t *) gfx_data; int i = 129; while (i < 145 && g_data[i] != 0) ++i; numimages = i - 129; has_palette = false; type = RGBA; // Sanitize index if needed index %= numimages; if (index < 0) index = numimages + index; // Setup variables imgindex = index; width = wxUINT32_SWAP_ON_BE(g_data[index+129]); height = wxUINT32_SWAP_ON_BE(g_data[index+145]); size_t data_offset = wxUINT32_SWAP_ON_BE(g_data[index+161]); if (!width || !height || ! data_offset || size < (int)data_offset + (4*width*height)) { Global::error = "M32 file: invalid data for mip level"; return false; } // Create data clearData(); data = new uint8_t[width*height*4]; // Fill data with pixel data memcpy(data, gfx_data + data_offset, width * height * 4); // Announce change and return success announce("image_changed"); return true; } /* SImage::loadQuakeIIWal * Loads a Quake II wal-format texture. * Returns false if the image data was invalid, true otherwise. *******************************************************************/ bool SImage::loadQuakeIIWal(const uint8_t* gfx_data, int size, int index) { if (size < 101) return false; numimages = 4; has_palette = false; type = PALMASK; // Sanitize index if needed index %= numimages; if (index < 0) index = numimages + index; // Setup variables imgindex = index; width = READ_L32(gfx_data, 32) >> index; height = READ_L32(gfx_data, 36) >> index; size_t data_offset = READ_L32(gfx_data, 40+(index<<2)); if (!width || !height || ! data_offset || size < (int)data_offset + (width*height)) { Global::error = "WAL file: invalid data for mip level"; return false; } // Create data clearData(); data = new uint8_t[width*height]; // Fill data with pixel data memcpy(data, gfx_data + data_offset, width * height); // Announce change and return success announce("image_changed"); return true; } /* SImage::loadHalfLifeTex * Loads a Half-Life mipmapped paletted image. * Returns false if the image data was invalid, true otherwise. *******************************************************************/ bool SImage::loadHalfLifeTex(const uint8_t* gfx_data, int size, int index) { if (size < 812) return false; numimages = 4; has_palette = true; type = PALMASK; // Sanitize index if needed index %= numimages; if (index < 0) index = numimages + index; // Setup variables imgindex = index; width = READ_L32(gfx_data, 16) >> index; height = READ_L32(gfx_data, 20) >> index; size_t data_offset = READ_L32(gfx_data, 24+(index<<2)); if (!width || !height || ! data_offset || size < (int)data_offset + (width*height)) { Global::error = "HLT file: invalid data for mip level"; return false; } // Let's build the palette now. size_t pal_offset = READ_L32(gfx_data, 36) + ((READ_L32(gfx_data, 16) >> 3) * (READ_L32(gfx_data, 20) >> 3)); if (size < (int)pal_offset + 5) { Global::error = "HLT file: invalid palette offset"; return false; } size_t palsize = READ_L16(gfx_data, pal_offset); if (palsize == 0 || palsize > 256 || size < (int)(pal_offset + 2 + (palsize*3))) { wxLogMessage("palsize %d, paloffset %d, entry size %d", palsize, pal_offset, size); Global::error = "HLT file: invalid palette size"; return false; } for (size_t c = 0; c < palsize; ++c) { rgba_t color; color.r = gfx_data[(c*3)+pal_offset+2]; color.g = gfx_data[(c*3)+pal_offset+3]; color.b = gfx_data[(c*3)+pal_offset+4]; palette.setColour(c, color); } // Create data clearData(); data = new uint8_t[width*height]; // Fill data with pixel data memcpy(data, gfx_data + data_offset, width * height); // Announce change and return success announce("image_changed"); return true; } /* SImage::loadRottGfx * Loads a Rise of the Triad Gfx format image, using the image palette. * This format, descibed in hacker.txt from the ROTT source code pack, * is "extremely similar to the patches used in another game". * Returns false if the image data was invalid, true otherwise *******************************************************************/ bool SImage::loadRottGfx(const uint8_t* gfx_data, int size, bool transparent) { // Check data if (!gfx_data) return false; // Check size if ((unsigned)size < sizeof(rottpatch_header_t)) return false; // Init variables uint16_t* col_offsets = NULL; type = PALMASK; has_palette = false; numimages = 1; imgindex = 0; // Read header rottpatch_header_t* header = (rottpatch_header_t*)gfx_data; width = wxINT16_SWAP_ON_BE(header->width); height = wxINT16_SWAP_ON_BE(header->height); offset_x = wxINT16_SWAP_ON_BE(header->left); offset_y = wxINT16_SWAP_ON_BE(header->top); size_t hdr_size = sizeof(rottpatch_header_t); uint8_t alpha = 0xFF; if (transparent) { //wxLogMessage("Transparent"); //alpha = gfx_data[sizeof(rottpatch_header_t)]; hdr_size +=2; } // Read column offsets col_offsets = new uint16_t[width]; uint16_t* c_ofs = (uint16_t*)((uint8_t*)gfx_data + hdr_size); for (int a = 0; a < width; a++) col_offsets[a] = wxUINT16_SWAP_ON_BE(c_ofs[a]); // Clear current data if it exists clearData(); // Load data data = new uint8_t[width * height]; memset(data, 0, width * height); // Set colour to palette index 0 mask = new uint8_t[width * height]; memset(mask, 0, width * height); // Set mask to fully transparent for (int c = 0; c < width; c++) { // Get current column offset uint16_t col_offset = col_offsets[c]; //if (transparent) wxLogMessage("Column %i, offset at %i, offset %i", c, (c<<1) + hdr_size, col_offset); // Check column offset is valid if (col_offset >= (unsigned)size) { clearData(); return false; } // Go to start of column const uint8_t* bits = gfx_data + col_offset; // Read posts int counter = 0; while (1) { // Get row offset //if (transparent) wxLogMessage("Row number %i at offset %i", *bits, bits - gfx_data); uint8_t row = *bits++; if (row == 0xFF/* || row > height*/) // End of column? break; // This apparently triggers translucency mode, but I'm not sure how it works if (transparent && *bits == 254) { clearData(); Global::error = "Sorry, masked ROTT patches with translucency are not yet supported"; return false; } else { // Get no. of pixels uint8_t n_pix = *bits++; //if (transparent) wxLogMessage("Post %i, row = %i, numpixels = %i, offset %i", counter++, row, n_pix, bits - gfx_data); if (n_pix >= height && row != 0 && row < height) { wxLogMessage("Hack triggered!"); bits+=2; continue; } for (uint8_t p = 0; p < n_pix; p++) { // Get pixel position int pos = ((row + p)*width + c); //if (transparent) wxLogMessage("Pixel %i: pos %i, offset %i", p, pos, bits - gfx_data); // Stop if we're outside the image if (pos > width*height) break; // Stop if for some reason we're outside the gfx data if (bits > gfx_data + size) break; // Fail if bogus data gives a negative pos (this corrupts the heap!) if (pos < 0) { clearData(); return false; } // Write pixel data data[pos] = *bits++; mask[pos] = alpha; } } } } // Announce change and return success announce("image_changed"); return true; } /* SImage::loadRottLbm * Loads a Rise of the Triad LBM format image. * Returns false if the image data was invalid, true otherwise *******************************************************************/ bool SImage::loadRottLbm(const uint8_t* gfx_data, int size) { // Check data if (!gfx_data) return false; // Setup variables offset_x = offset_y = 0; width = READ_L16(gfx_data, 0); height = READ_L16(gfx_data, 2); has_palette = true; type = PALMASK; numimages = 1; imgindex = 0; // ROTT source code says: "LIMITATIONS - Only works with 320x200!!!" if (width != 320 || height != 200) return false; // Clear current data if it exists clearData(); // Let's build the palette now. for (size_t c = 0; c < 256; ++c) { rgba_t color; color.r = gfx_data[(c*3)+4]; color.g = gfx_data[(c*3)+5]; color.b = gfx_data[(c*3)+6]; palette.setColour(c, color); } // Read raw pixel data data = new uint8_t[width*height]; // Create some variables needed for LBM decompression const uint8_t * read = gfx_data + 768 +4; const uint8_t * readend = gfx_data + size; uint8_t * dest = data; uint8_t * destend = data + (width * height); uint8_t code = 0; uint8_t length = 0; uint8_t count = 0; while (read < readend && dest < destend && count < width) { code = *read++; if (code < 0x80) { length = code + 1; memcpy(dest, read, length); dest+=length; read+=length; } else if (code > 0x80) { length = (code^0xFF)+2;; code = *read++; memset(dest, code, length); dest+=length; } else length = 0; count += length; } // Announce change and return success announce("image_changed"); return true; } /* SImage::loadRottRaw * Loads a ROTT raw data + header image. *******************************************************************/ bool SImage::loadRottRaw(const uint8_t* gfx_data, int size) { // Check data if (!gfx_data) return false; // Setup variables patch_header_t* header = (patch_header_t*)gfx_data; height = wxINT16_SWAP_ON_BE(header->width); // Cheat a bit since width = wxINT16_SWAP_ON_BE(header->height); // it's column-major offset_x = wxINT16_SWAP_ON_BE(header->left); offset_y = wxINT16_SWAP_ON_BE(header->top); type = PALMASK; has_palette = false; numimages = 1; imgindex = 0; // Clear current data if it exists clearData(); // Read raw pixel data data = new uint8_t[width*height]; memcpy(data, gfx_data+8, width * height); // Convert from column-major to row-major rotate(90); mirror(true); // Announce change and return success announce("image_changed"); return true; } /* SImage::loadRottPic * Loads a ROTT picture: same as Doom snea, but with transparency. *******************************************************************/ bool SImage::loadRottPic(const uint8_t* gfx_data, int size) { // Check data if (!gfx_data) return false; // Check/setup size offset_x = offset_y = 0; uint8_t qwidth = gfx_data[0]; width = qwidth * 4; height = gfx_data[1]; if (size != 4 + width * height) return false; // Setup variables type = PALMASK; has_palette = false; numimages = 1; imgindex = 0; // Clear current data if it exists clearData(); // Read raw pixel data data = new uint8_t[width*height]; // Since gfx_data is a const pointer, we can't work on it. uint8_t * tempdata = new uint8_t[size]; memcpy(tempdata, gfx_data, size); uint8_t* entryend = tempdata + size - 2; uint8_t* dataend = data + size - 4; uint8_t* pixel = tempdata + 2; uint8_t* brush = data; while (pixel < entryend) { *brush = *pixel++; brush += 4; if (brush >= dataend) brush -= size - 5; } delete[] tempdata; // Create mask (index 255 is transparent) mask = new uint8_t[width*height]; memset(mask, 255, width*height); for (int i = 0; i < width*height; ++i) if (data[i] == 0xFF) mask[i] = 0; // Announce change and return success announce("image_changed"); return true; } /* SImage::loadWolfPic * Loads a Wolfenstein 3D picture. *******************************************************************/ bool SImage::loadWolfPic(const uint8_t* gfx_data, int size) { // Check data if (!gfx_data) return false; // Check/setup size offset_x = offset_y = 0; width = READ_L16(gfx_data, 0); height = READ_L16(gfx_data, 2); if (size != 4 + width * height) return false; // Setup variables type = PALMASK; has_palette = false; numimages = 1; imgindex = 0; // Clear current data if it exists clearData(); // Read raw pixel data data = new uint8_t[width*height]; const uint8_t* pixel = gfx_data + 4; const uint8_t* entryend = gfx_data + size; uint8_t* brush = data; uint8_t* dataend = data + size - 4; while (pixel < entryend) { *brush = *pixel++; brush += 4; if (brush >= dataend) brush -= size - 5; } // Create mask (index 255 is transparent) /*mask = new uint8_t[width*height]; memset(mask, 255, width*height); for (int i = 0; i < width*height; ++i) if (data[i] == 0xFF) mask[i] = 0;*/ // Announce change and return success announce("image_changed"); return true; } /* SImage::loadWolfSprite * Loads a Wolfenstein 3D sprite. *******************************************************************/ bool SImage::loadWolfSprite(const uint8_t* gfx_data, int size) { // Check data if (!gfx_data) return false; // Check/setup size uint8_t leftpix, rightpix; leftpix = gfx_data[0]; rightpix = gfx_data[2]; width = 1 + rightpix - leftpix; height = 64; offset_x = 32 - leftpix; offset_y = height; // Setup variables type = PALMASK; has_palette = false; numimages = 1; imgindex = 0; // Clear current data if it exists clearData(); // Read raw pixel data data = new uint8_t[width*height]; mask = new uint8_t[width*height]; memset(mask, 0, (width*height)); uint16_t * cmdptr = (uint16_t *)(gfx_data + 4); uint32_t i, x, y; for (x = 0 ; x < (unsigned)width ; ++x ) { int16_t * linecmds = (int16_t *)(gfx_data + wxINT16_SWAP_ON_BE( *cmdptr )); cmdptr++; for (; wxINT16_SWAP_ON_BE(*linecmds); linecmds += 3) { i = (wxINT16_SWAP_ON_BE(linecmds[2])>>1) + wxINT16_SWAP_ON_BE(linecmds[1]); for (y = (uint32_t)(wxINT16_SWAP_ON_BE(linecmds[2])>>1); y < (uint32_t)(wxINT16_SWAP_ON_BE(linecmds[0]) / 2); ++y, ++i) { data[y * width + x] = gfx_data[i]; mask[y * width + x] = 255; } } } // Announce change and return success announce("image_changed"); return true; } // These are defined in SIFJedi.h now bool JediRLE0(const uint8_t* src, int coloffs, int width, int height, uint8_t * data); bool JediRLE(const uint8_t* src, int coloffs, int width, int height, uint8_t * data); /* Jedi Engine bitmap header struct *******************************************************************/ struct JediBMHeader { char magic[4]; // "BM "0x1E uint16_t width; // If 1 and height not 1, multiple BM uint16_t height; // If height and width both 1, 1x1 bitmap uint16_t wid2; // Same as width except for multiple BM, then it's 65534 uint16_t hei2; // Same as height except for multiple BM, then it's number of images uint8_t flag; // Determines transparency among other stuff uint8_t log; // Worth either log2(height) or 0 uint16_t rle; // 0: not compressed, 1: compressed with RLE, 2: compressed with RLE0 uint32_t coloffs; // Column offsets in compressed bitmaps uint32_t pad[3]; // Padding, should be zero }; struct JediBMSubheader { uint16_t width; // If 1 and height not 1, multiple BM uint16_t height; // If height and width both 1, 1x1 bitmap uint16_t wid2; // Unused uint16_t hei2; // Unused uint32_t size; // Unused uint8_t log; // Worth log2(height) uint8_t pad1[11]; // No known use uint8_t flag; // Determines transparency among other stuff uint8_t pad2[3]; // No known use }; /* SImage::loadJediBM * Loads a Jedi Engine bitmap * Returns false if the image data was invalid, true otherwise. *******************************************************************/ bool SImage::loadJediBM(const uint8_t* gfx_data, int size, int index) { if (size < 32) return false; const JediBMHeader * header = (JediBMHeader *) gfx_data; bool multibm = false; bool transparent = !!(header->flag & 8); size_t data_offset = 32; // Check for multiple images if (wxINT16_SWAP_ON_BE(header->width) == 1 && wxINT16_SWAP_ON_BE(header->height) != 1) { if (header->rle == 0 && wxINT16_SWAP_ON_BE(header->wid2) == 65534) { numimages = wxINT16_SWAP_ON_BE(header->hei2); } else { Global::error = "Jedi BM file: invalid multi-BM file"; return false; } multibm = true; } else numimages = 1; // Sanitize index if needed index %= numimages; if (index < 0) index = numimages + index; // Setup variables has_palette = false; type = PALMASK; imgindex = index; if (multibm) { // 32 for the header, 2 for control data, then four bytes per subimage int offset_offset = 34 + (imgindex<<2); if (offset_offset + 4 > size) { Global::error = "Jedi BM file: invalid multi-BM file"; return false; } const uint32_t * header_poffset = (uint32_t *)(gfx_data + offset_offset); uint32_t header_offset = wxINT32_SWAP_ON_BE(*header_poffset)+34; const JediBMSubheader * subheader = (JediBMSubheader *) (gfx_data + header_offset); transparent = !!(subheader->flag & 8); // Little cheat here as they are defined in column-major format, // not row-major. So we'll just call the rotate function. height = wxINT16_SWAP_ON_BE(subheader->width); width = wxINT16_SWAP_ON_BE(subheader->height); data_offset = header_offset + 28; } else { // Same little cheat. height = wxINT16_SWAP_ON_BE(header->width); width = wxINT16_SWAP_ON_BE(header->height); } // Create data clearData(); data = new uint8_t[width*height]; // Fill data with pixel data if (header->rle == 0) { memcpy(data, gfx_data + data_offset, width * height); } else if (header->rle == 1) { JediRLE(gfx_data + data_offset, header->coloffs, height, width, data); } else if (header->rle == 2) { JediRLE0(gfx_data + data_offset, header->coloffs, height, width, data); } else { Global::error = "Jedi BM file: Invalid compression scheme"; clearData(); return false; } // Convert from column-major to row-major rotate(90); // Manage transparency if (transparent) { mask = new uint8_t[width*height]; memset(mask, 0xFF, width*height); for (int i = 0; i < width*height; ++i) if (data[i] == 0) mask[i] = 0; } // Announce change and return success announce("image_changed"); return true; } /* Jedi Engine frame header struct *******************************************************************/ struct JediFMEHeader1 { int32_t offsx; // X offset (right is positive) int32_t offsy; // Y offset (down is positive) uint32_t flag; // Only one flag used: 1 for flipped horizontally uint32_t head2; // Offset to secondary header uint32_t width; // Unused; the data from the secondary header is used instead uint32_t height; // Unused; the data from the secondary header is used instead uint32_t pad[2]; // Padding, should be zero }; struct JediFMEHeader2 { uint32_t width; // Used instead of the value in the primary header uint32_t height; // Used instead of the value in the primary header uint32_t flag; // Only one flag used: 1 for RLE0 compression uint32_t size; // Data size for compressed FME, same as file size - 32 uint32_t coloffs; // Offset to column data, practically always 0 uint32_t padding; // No known use }; /* SImage::JediFrame * Used by several Jedi Engine formats *******************************************************************/ bool SImage::JediFrame(const uint8_t* gfx_data, uint32_t hdroffs) { const JediFMEHeader1 * header1 = (JediFMEHeader1 *) (gfx_data + hdroffs); const JediFMEHeader2 * header2 = (JediFMEHeader2 *) (gfx_data + wxINT32_SWAP_ON_BE(header1->head2)); bool flip = !!(header1->flag & 1); size_t data_offset = header1->head2; // Setup variables offset_x = 0 - wxINT32_SWAP_ON_BE(header1->offsx); offset_y = 0 - wxINT32_SWAP_ON_BE(header1->offsy); // Little cheat here as they are defined in column-major format, // not row-major. So we'll just call the rotate function. height = wxINT32_SWAP_ON_BE(header2->width); width = wxINT32_SWAP_ON_BE(header2->height); // Create data clearData(); data = new uint8_t[width*height]; // Fill data with pixel data if (header2->flag == 0) memcpy(data, gfx_data + header1->head2 + 24, width * height); else JediRLE0(gfx_data + wxINT32_SWAP_ON_BE(header1->head2), 24, wxINT32_SWAP_ON_BE(header2->width), wxINT32_SWAP_ON_BE(header2->height), data); // Convert from column-major to row-major rotate(90); // Mirror if needed if (flip) mirror(false); // Manage transparency mask = new uint8_t[width*height]; memset(mask, 0xFF, width*height); for (int i = 0; i < width*height; ++i) if (data[i] == 0) mask[i] = 0; // Announce change and return success announce("image_changed"); return true; } /* SImage::loadJediFME * Loads a Jedi Engine frame * Returns false if the image data was invalid, true otherwise. *******************************************************************/ bool SImage::loadJediFME(const uint8_t* gfx_data, int size) { if (size < 32) return false; // Setup variables has_palette = false; type = PALMASK; imgindex = 0; numimages = 1; // The image loading code is in a different function // so that it can be used by WAXes too. return JediFrame(gfx_data, 0); } /* Jedi Engine wax header struct *******************************************************************/ struct JediWAXHeader { uint32_t version; // Constant worth 0x00100100 uint32_t numseqs; // Number of sequences uint32_t numframes; // Number of frames uint32_t numcells; // Number of cells uint32_t pad[4]; // Unused values uint32_t waxes[32]; // Offsets to the WAX subheaders }; struct JediWAXSubheader { uint32_t width; // Scaled width, in world units uint32_t height; // Scaled height, in world units uint32_t fps; // Frames per second of animation uint32_t pad[4]; // Unused values, must be zero uint32_t seqs[32]; // Offsets to sequences }; struct JediWAXSequence { uint32_t pad[4]; // Unused values, must be zero uint32_t frames[32];// Offsets to frames }; /* SImage::loadJediWAX * Loads a Jedi Engine frame * Returns false if the image data was invalid, true otherwise. *******************************************************************/ bool SImage::loadJediWAX(const uint8_t* gfx_data, int size, int index) { if (size < 460) return false; const JediWAXHeader * header = (JediWAXHeader *) gfx_data; vector frameoffs; // This is a recursive nightmare. What were the LucasArt devs smoking when they specced this format? for (int i = 0; i < 32; ++i) { uint32_t waxi = wxINT32_SWAP_ON_BE(header->waxes[i]); if (waxi > sizeof(header) && waxi < size - sizeof(JediWAXSubheader)) { const JediWAXSubheader * subhdr = (JediWAXSubheader *) (gfx_data + waxi); for (int j = 0; j < 32; ++j) { uint32_t seqj = wxINT32_SWAP_ON_BE(subhdr->seqs[j]); if (seqj > sizeof(header) && seqj < size - sizeof(JediWAXSequence)) { const JediWAXSequence * seq = (JediWAXSequence *) (gfx_data + seqj); for (int k = 0; k < 32; ++k) { uint32_t framk = wxINT32_SWAP_ON_BE(seq->frames[k]); if (framk > sizeof(header) && framk < size - sizeof(JediWAXSequence)) { const JediFMEHeader1 * frame = (JediFMEHeader1 *) (gfx_data + framk); uint32_t cell = wxINT32_SWAP_ON_BE(frame->head2); if (cell > sizeof(header) && cell < size - sizeof(JediFMEHeader2)) { bool notfound = true; for (size_t l = 0; l < frameoffs.size(); ++l) { if (frameoffs[l] == framk) { notfound = false; break; } } if (notfound) frameoffs.push_back(framk); } } } } } } } // Urgh. At least it's over now. // Sanitize index if needed numimages = frameoffs.size(); if (frameoffs.empty()) { Global::error = "Jedi WAX error: No cell found in wax!"; // What a surreal error message ;) return false; } index %= numimages; if (index < 0) index = numimages + index; // Setup variables has_palette = false; type = PALMASK; imgindex = index; // The one good thing about this format is that we can reuse the FME code for loading the actual image return JediFrame(gfx_data, frameoffs[imgindex]); }