421 lines
11 KiB
C
421 lines
11 KiB
C
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
#include <assert.h>
|
|
#include "lodepng/lodepng.h"
|
|
#include "pngquant/libimagequant.h"
|
|
|
|
// This is a modified TIM2 image data header
|
|
// Reference: http://wiki.xentax.com/index.php/TM2_TIM2
|
|
typedef struct tcbHeader {
|
|
// 0x00
|
|
char magic[4];
|
|
char padding[12];
|
|
|
|
// 0x10
|
|
uint32_t totalLength;
|
|
uint32_t paletteLength;
|
|
uint32_t imageDataLength;
|
|
uint16_t headerLength;
|
|
uint16_t numPaletteEntries;
|
|
|
|
// 0x20
|
|
uint8_t imageType;
|
|
uint8_t numMipmaps;
|
|
uint8_t paletteType;
|
|
uint8_t bitsPerPixel;
|
|
uint16_t width;
|
|
uint16_t height;
|
|
uint64_t gsTEX0;
|
|
|
|
// 0x30
|
|
uint64_t gsTEX1;
|
|
uint32_t gsRegs;
|
|
uint32_t gsTexClut;
|
|
|
|
// 0x40
|
|
uint8_t userData[16];
|
|
} tcbHeader_t;
|
|
|
|
typedef struct rgbaImageData {
|
|
uint32_t *rgba;
|
|
unsigned int width;
|
|
unsigned int height;
|
|
} rgbaImageData_t;
|
|
|
|
#define LONIBBLE(x) ((uint8_t) ((uint8_t) (x) & (uint8_t) 0x0F))
|
|
#define HINIBBLE(x) ((uint8_t) ((uint8_t) (x) >> (uint8_t) 4))
|
|
|
|
/**
|
|
* Filters palette colors to/from the format used by PS2 games.
|
|
*
|
|
* @param unfiltered pointer to array of unfiltered RGBA colors.
|
|
* @param filtered pointer to store array of filtered RGBA colors.
|
|
* @param length number of elements in unfiltered/filtered color array.
|
|
*
|
|
* @pre unfiltered and filtered point to allocated memory of length*4 bytes.
|
|
* @post filtered colors copied to filtered.
|
|
*/
|
|
void palette_filter(uint32_t *unfiltered, uint32_t *filtered, int length)
|
|
{
|
|
int parts = length / 32;
|
|
int stripes = 2;
|
|
int colors = 8;
|
|
int blocks = 2;
|
|
|
|
int i = 0;
|
|
for (int part = 0; part < parts; part++)
|
|
{
|
|
for (int block = 0; block < blocks; block++)
|
|
{
|
|
for (int stripe = 0; stripe < stripes; stripe++)
|
|
{
|
|
|
|
for (int color = 0; color < colors; color++)
|
|
{
|
|
filtered[i++] = unfiltered[ part * colors * stripes * blocks + block * colors + stripe * stripes * colors + color ];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int tcb_to_rgba(uint8_t *tcbData, int tcbLength, rgbaImageData_t *imageData);
|
|
int rgba_to_tcb(uint8_t *tcbData, int tcbLength, rgbaImageData_t *imageData);
|
|
|
|
int png_decode(char *pngPath, rgbaImageData_t *imageData);
|
|
int png_encode(char *pngPath, rgbaImageData_t *imageData);
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
FILE *tcbFile;
|
|
int tcbLength;
|
|
uint8_t *tcbData;
|
|
char *tcbPath;
|
|
char pngPath[260];
|
|
char mode;
|
|
rgbaImageData_t imageData;
|
|
|
|
if(argc != 3)
|
|
{
|
|
printf("TCB Converter\n");
|
|
printf("Usage:\n");
|
|
printf("Convert TCB to PNG: %s e <image.tcb>\n", argv[0]);
|
|
printf("Inject PNG into TCB: %s i <image.tcb>\n", argv[0]);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
mode = tolower(argv[1][0]);
|
|
tcbPath = argv[2];
|
|
strncpy(pngPath, tcbPath, sizeof(pngPath) - 1);
|
|
strncat(pngPath, ".png", sizeof(pngPath) - 1);
|
|
|
|
tcbFile = fopen(tcbPath, "rb");
|
|
if(!tcbFile)
|
|
{
|
|
printf("Error opening %s\n", tcbPath);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
fseek(tcbFile, 0, SEEK_END);
|
|
tcbLength = ftell(tcbFile);
|
|
fseek(tcbFile, 0, SEEK_SET);
|
|
tcbData = malloc(tcbLength);
|
|
if(!tcbData)
|
|
{
|
|
printf("malloc error\n");
|
|
fclose(tcbFile);
|
|
return EXIT_FAILURE;
|
|
}
|
|
fread(tcbData, 1, tcbLength, tcbFile);
|
|
fclose(tcbFile);
|
|
|
|
if(mode == 'i')
|
|
{
|
|
printf("Injecting image from %s into %s...\n", pngPath, tcbPath);
|
|
|
|
if(png_decode(pngPath, &imageData))
|
|
{
|
|
free(tcbData);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
if(rgba_to_tcb(tcbData, tcbLength, &imageData))
|
|
{
|
|
free(tcbData);
|
|
free(imageData.rgba);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
tcbFile = fopen(tcbPath, "wb");
|
|
if(!tcbFile)
|
|
{
|
|
printf("Error opening %s.\n", tcbPath);
|
|
free(tcbData);
|
|
free(imageData.rgba);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
fwrite(tcbData, 1, tcbLength, tcbFile);
|
|
fclose(tcbFile);
|
|
|
|
printf("Success!\n");
|
|
}
|
|
else if(mode == 'e')
|
|
{
|
|
printf("Extracting image from %s to %s...\n", tcbPath, pngPath);
|
|
|
|
if(tcb_to_rgba(tcbData, tcbLength, &imageData))
|
|
{
|
|
free(tcbData);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
if(png_encode(pngPath, &imageData))
|
|
{
|
|
free(tcbData);
|
|
free(imageData.rgba);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
printf("Success!\n");
|
|
}
|
|
else
|
|
{
|
|
printf("Error: Unsupported mode '%c'\n", mode);
|
|
free(tcbData);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
free(tcbData);
|
|
free(imageData.rgba);
|
|
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* Convert TCB data to RGBA data.
|
|
*
|
|
* @param tcbData pointer to data from a TCB image.
|
|
* @param tcbLength size of tcbData.
|
|
* @param imageData pointer to struct to store RGBA image data.
|
|
*
|
|
* @post Image data from TCB is converted to 32-bit RGBA and stored in imageData->rgba.
|
|
* @return 1 on success, 0 on failure
|
|
*/
|
|
int tcb_to_rgba(uint8_t *tcbData, int tcbLength, rgbaImageData_t *imageData)
|
|
{
|
|
tcbHeader_t *header;
|
|
uint8_t index, alpha;
|
|
uint8_t *image;
|
|
uint32_t *palette;
|
|
|
|
if (!tcbData || tcbLength <= 0 || !imageData)
|
|
return EXIT_FAILURE;
|
|
|
|
header = (tcbHeader_t*) tcbData;
|
|
image = (uint8_t*) (tcbData + 0x50);
|
|
palette = (uint32_t*) (tcbData + header->imageDataLength + 0x50);
|
|
|
|
if(strncmp(header->magic, "TCB\0", 4))
|
|
{
|
|
for(int i = 0; i < sizeof(header->padding); i++)
|
|
{
|
|
if(header->padding[i] != 0)
|
|
{
|
|
printf("ERROR: Invalid TCB file.\n");
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
}
|
|
|
|
imageData->rgba = malloc(header->width * header->height * 4);
|
|
imageData->width = header->width;
|
|
imageData->height = header->height;
|
|
|
|
uint32_t *newPalette = NULL;
|
|
if(header->paletteLength > 0x50)
|
|
{
|
|
newPalette = calloc(256, 4);
|
|
palette_filter(palette, newPalette, (header->paletteLength - 0x10) / 4);
|
|
palette = newPalette;
|
|
}
|
|
|
|
if(header->paletteType == 0x05) // 8bpp
|
|
{
|
|
for(int i = 0; i < (header->width * header->height); i++)
|
|
{
|
|
index = image[i];
|
|
|
|
alpha = palette[index] >> 24;
|
|
if(alpha > 0)
|
|
alpha = (alpha << 1) - 1; // scale from 0-128 to 0-255 range
|
|
|
|
imageData->rgba[i] = palette[index] | (alpha << 24); // 255 alpha channel
|
|
}
|
|
}
|
|
else if(header->paletteType == 0x04) // 4bpp
|
|
{
|
|
for(int i = 0; i < (header->width * header->height); i++)
|
|
{
|
|
index = image[i/2];
|
|
|
|
if(i%2)
|
|
index = HINIBBLE(index);
|
|
else
|
|
index = LONIBBLE(index);
|
|
|
|
alpha = palette[index] >> 24;
|
|
if(alpha > 0)
|
|
alpha = (alpha << 1) - 1; // scale from 0-128 to 0-255 range
|
|
|
|
imageData->rgba[i] = palette[index] | (alpha << 24);
|
|
}
|
|
}
|
|
|
|
if(newPalette != NULL)
|
|
free(newPalette);
|
|
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* Inject RGBA data into existing TCB data.
|
|
*
|
|
* @param tcbData pointer to data from TCB image.
|
|
* @param tcbLength size of tcbData.
|
|
* @param imageData pointer to struct with RGBA image data.
|
|
*
|
|
* @post Image data from rgbaImageData is quantized and injected into tcbData.
|
|
* @return 0 on success, 1 on failure.
|
|
*/
|
|
int rgba_to_tcb(uint8_t *tcbData, int tcbLength, rgbaImageData_t *imageData)
|
|
{
|
|
tcbHeader_t *header;
|
|
uint8_t *image;
|
|
uint32_t *palette;
|
|
int len;
|
|
|
|
if(!tcbData || tcbLength <= 0 || !imageData)
|
|
return EXIT_FAILURE;
|
|
|
|
header = (tcbHeader_t*) tcbData;
|
|
image = (uint8_t*) (tcbData + 0x50);
|
|
palette = (uint32_t*) (tcbData + header->imageDataLength + 0x50);
|
|
|
|
if(header->width != imageData->width || header->height != imageData->height)
|
|
{
|
|
printf("Image size mismatch. PNG dimensions are %ux%u, but it needs to be %ux%u to be injected into the TCB.\n", imageData->width, imageData->height, header->width, header->height);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
liq_attr *attr = liq_attr_create();
|
|
if(header->paletteType == 0x04) // 4-bit color
|
|
{
|
|
liq_set_max_colors(attr, 16);
|
|
}
|
|
else if(header->paletteType != 0x05)
|
|
{
|
|
printf("Unsupported image type 0x%02X.\n", header->paletteType);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
/* Quantize image and create new image map and palette */
|
|
liq_image *imagel = liq_image_create_rgba(attr, imageData->rgba, imageData->width, imageData->height, 0);
|
|
liq_result *res = liq_quantize_image(attr, imagel);
|
|
|
|
if(header->paletteType == 0x05)
|
|
{
|
|
liq_write_remapped_image(res, imagel, image, imageData->height * imageData->width);
|
|
}
|
|
else if (header->paletteType == 0x04) // 4-bit color
|
|
{
|
|
uint8_t *fulldata = malloc(imageData->width * imageData->height);
|
|
liq_write_remapped_image(res, imagel, (uint32_t *)fulldata, imageData->width * imageData->height);
|
|
|
|
// Convert to 4-bit
|
|
for(int i = 0; i < (imageData->width * imageData->height)/2; i++)
|
|
{
|
|
uint8_t pixel1 = *((uint8_t *)fulldata + i*2);
|
|
uint8_t pixel2 = *((uint8_t *)fulldata + i*2 + 1);
|
|
|
|
image[i] = pixel1 | (pixel2 << 4);
|
|
}
|
|
|
|
free(fulldata);
|
|
}
|
|
|
|
/* Update palette in TCB */
|
|
const liq_palette *pal = liq_get_palette(res);
|
|
memcpy(palette, pal->entries, pal->count*4);
|
|
|
|
for(int i = 0; i < pal->count; i++)
|
|
{
|
|
uint8_t alpha = palette[i] >> 24;
|
|
if(alpha > 0)
|
|
alpha = (alpha >> 1) + 1; // scale down to 0-128 range
|
|
}
|
|
|
|
if(header->paletteType == 0x05) // Filter palette data
|
|
{
|
|
uint32_t *filtered = malloc(256 * 4);
|
|
palette_filter((uint32_t *) palette, filtered, 256);
|
|
memcpy((void *) palette, filtered, 256 * 4);
|
|
free(filtered);
|
|
}
|
|
|
|
liq_attr_destroy(attr);
|
|
liq_image_destroy(imagel);
|
|
liq_result_destroy(res);
|
|
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* Decode PNG file to RGBA data.
|
|
*
|
|
* @param pngPath Path to PNG file.
|
|
* @param imageData pointer to struct to store RGBA image data.
|
|
* @return 0 on success, 1 on failure.
|
|
*/
|
|
int png_decode(char *pngPath, rgbaImageData_t *imageData)
|
|
{
|
|
int ret;
|
|
if(!pngPath || !imageData)
|
|
return EXIT_FAILURE;
|
|
|
|
ret = lodepng_decode32_file((unsigned char **)&imageData->rgba, &imageData->width, &imageData->height, pngPath);
|
|
if(ret != 0)
|
|
{
|
|
printf("PNG decoding error %u: %s\n", ret, lodepng_error_text(ret));
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* Encode PNG file with RGBA data.
|
|
*
|
|
* @param pngPath Path to PNG file.
|
|
* @param imageData pointer to struct with RGBA image data.
|
|
* @return 0 on success, 1 on failure.
|
|
*/
|
|
int png_encode(char *pngPath, rgbaImageData_t *imageData)
|
|
{
|
|
int ret;
|
|
if(!pngPath || !imageData)
|
|
return EXIT_FAILURE;
|
|
|
|
ret = lodepng_encode32_file(pngPath, (const unsigned char *)imageData->rgba, imageData->width, imageData->height);
|
|
|
|
if(ret != 0)
|
|
{
|
|
printf("PNG encoding error %u: %s\n", ret, lodepng_error_text(ret));
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
return EXIT_SUCCESS;
|
|
}
|