1
0
Fork 0
mirror of https://github.com/zeldaret/oot.git synced 2025-06-08 01:21:52 +00:00
oot/tools/assets/n64texconv/src/libn64texconv/n64texconv.c
Tharo faf2003d37
Force skybox split palettes to always contain 128 colors (#2535)
* Force skybox split palettes to always contain 128 colors, padding with 0 if necessary

* Review
2025-05-25 21:50:34 +02:00

1317 lines
38 KiB
C

/* SPDX-FileCopyrightText: Copyright (C) 2025 ZeldaRET */
/* SPDX-License-Identifier: MIT */
#include <assert.h>
#include <inttypes.h>
#include <math.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "spng.h"
#include "libimagequant.h"
#include "endian.h"
#include "bin2c.h"
#include "n64texconv.h"
// ********************************************************************************************************************
// Helpers
// ********************************************************************************************************************
#define UNUSED __attribute__((unused))
#define ALIGN8(x) (((x) + 7) & ~7)
#define ABS(x) (((x) < 0) ? -(x) : (x))
static uint8_t
texel_to_greyscale(uint8_t r, uint8_t g, uint8_t b)
{
// Approximately (0.2126729, 0.7151522, 0.0721750) in 1.8 fixed point
// Same as Blender:
// https://github.com/blender/blender/blob/594f47ecd2d5367ca936cf6fc6ec8168c2b360d0/intern/cycles/render/shader.cpp#L387
return (r * 54 + g * 183 + b * 19 + 128) / 256;
}
UNUSED static void
texels_to_greyscale(struct color *out, struct color *in, size_t ntexels, bool alpha)
{
assert(out != NULL);
assert(in != NULL);
for (size_t i = 0; i < ntexels; i++) {
struct color color = in[i];
uint8_t grey = texel_to_greyscale(color.r, color.g, color.b);
uint8_t a = color.a;
out[i].r = grey;
out[i].g = grey;
out[i].b = grey;
out[i].a = (alpha) ? a : grey;
}
}
static void
palette_dim(size_t count, size_t *width, size_t *height)
{
assert(width != NULL);
assert(height != NULL);
size_t best_width = count;
size_t best_height = 1;
size_t best_diff = count - 1;
if (count >= 4) {
// Find a factorization of count for which the two factors are close to equal, and width >= height.
// This makes the output palette image approximately square.
for (size_t i = 2; i <= (size_t)sqrtf(count); i++) {
size_t div = count / i;
size_t new_diff = ABS((signed)(div - i));
if (div * i == count && new_diff < best_diff) {
// (div, i) is a factorization and is closer to a square than before
best_width = div;
best_height = i;
best_diff = new_diff;
}
}
}
*width = best_width;
*height = best_height;
}
static void
n64texconv_quantize(uint8_t *out_indices, struct color *out_pal, size_t *out_pal_count, struct color *texels,
size_t width, size_t height, unsigned int max_colors, float dither_level)
{
assert(out_indices != NULL);
assert(out_pal != NULL);
assert(out_pal_count != NULL);
assert(texels != NULL);
// Set options
liq_attr *attr = liq_attr_create();
liq_set_max_colors(attr, max_colors);
// Quantize
liq_image *img = liq_image_create_rgba(attr, (void *)texels, width, height, 0.0);
liq_result *result;
liq_error err = liq_image_quantize(img, attr, &result);
assert(err == LIQ_OK);
// Write output bitmap
liq_set_dithering_level(result, dither_level);
liq_write_remapped_image(result, img, (void *)out_indices, width * height);
// Write output palette
const liq_palette *pal = liq_get_palette(result);
assert(pal->count <= max_colors);
*out_pal_count = pal->count;
memcpy(out_pal, pal->entries, pal->count * 4);
// Cleanup
liq_result_destroy(result);
liq_image_destroy(img);
liq_attr_destroy(attr);
}
/**
* out_indices, out_pal, out_pal_count, texels, widths, heights are all arrays of size num_images
* texels[i] and out_indices[i] are arrays of size widths[i] * heights[i]
*/
int
n64texconv_quantize_shared(uint8_t **out_indices, struct color *out_pal, size_t *out_pal_count, struct color **texels,
size_t *widths, size_t *heights, size_t num_images, unsigned int max_colors,
float dither_level)
{
assert(out_indices != NULL);
assert(out_pal != NULL);
assert(out_pal_count != NULL);
assert(texels != NULL);
assert(widths != NULL);
assert(heights != NULL);
int rv = 0;
// Set options
liq_attr *attr = liq_attr_create();
liq_set_max_colors(attr, max_colors);
// Create histogram
liq_histogram *hist = liq_histogram_create(attr);
// Add images to histogram
liq_image *images[num_images];
for (size_t i = 0; i < num_images; i++) {
images[i] = liq_image_create_rgba(attr, (void *)texels[i], widths[i], heights[i], 0);
liq_histogram_add_image(hist, attr, images[i]);
}
// Simultaneous quantization
liq_result *result;
liq_error err = liq_histogram_quantize(hist, attr, &result);
if (err != LIQ_OK) {
rv = -1;
goto exit;
}
// Remap images
liq_set_dithering_level(result, dither_level);
for (size_t i = 0; i < num_images; i++) {
liq_write_remapped_image(result, images[i], (void *)out_indices[i], widths[i] * heights[i]);
}
// Write output palette
const liq_palette *pal = liq_get_palette(result);
assert(pal->count <= max_colors);
*out_pal_count = pal->count;
memcpy(out_pal, pal->entries, pal->count * 4);
// Cleanup
liq_result_destroy(result);
exit:
for (size_t i = 0; i < num_images; i++) {
liq_image_destroy(images[i]);
}
liq_histogram_destroy(hist);
liq_attr_destroy(attr);
return rv;
}
// ********************************************************************************************************************
// Format Packing and Unpacking
// ********************************************************************************************************************
#define UNPACK_FUNC(fmt_name) static int fmt_name##_unpack(struct color *out, void *in, size_t ntexels)
#define UNPACK_CI_FUNC(fmt_name) static int fmt_name##_unpack(uint8_t *out, void *in, size_t ntexels, size_t npal)
#define PACK_FUNC(fmt_name) static int fmt_name##_pack(void *out, struct color *in, size_t ntexels)
#define PACK_CI_FUNC(fmt_name) static int fmt_name##_pack(void *out, uint8_t *in, size_t ntexels, size_t npal)
/* RGBA16 */
UNPACK_FUNC(rgba16)
{
uint16_t *in16 = in;
for (size_t n = 0; n < ntexels; n++) {
uint16_t c = be16toh(in16[n]);
uint8_t r = (c >> 11) & 0b11111;
uint8_t g = (c >> 6) & 0b11111;
uint8_t b = (c >> 1) & 0b11111;
uint8_t a = (c >> 0) & 1;
out[n].r = (r << 3) | (r >> 2);
out[n].g = (g << 3) | (g >> 2);
out[n].b = (b << 3) | (b >> 2);
out[n].a = 255 * a;
}
return 0;
}
PACK_FUNC(rgba16)
{
uint16_t *out16 = out;
for (size_t n = 0; n < ntexels; n++) {
uint8_t r = (in[n].r >> 3) & 0b11111;
uint8_t g = (in[n].g >> 3) & 0b11111;
uint8_t b = (in[n].b >> 3) & 0b11111;
uint8_t a = in[n].a != 0;
out16[n] = htobe16((r << 11) | (g << 6) | (b << 1) | a);
}
return 0;
}
/* RGBA32 */
UNPACK_FUNC(rgba32)
{
uint32_t *in32 = in;
for (size_t n = 0; n < ntexels; n++) {
uint32_t c = be32toh(in32[n]);
out[n].r = (c >> 24) & 0xFF;
out[n].g = (c >> 16) & 0xFF;
out[n].b = (c >> 8) & 0xFF;
out[n].a = (c >> 0) & 0xFF;
}
return 0;
}
PACK_FUNC(rgba32)
{
uint32_t *out32 = out;
for (size_t n = 0; n < ntexels; n++) {
out32[n] = htobe32((in[n].r << 24) | (in[n].g << 16) | (in[n].b << 8) | in[n].a);
}
return 0;
}
/* CI4 */
UNPACK_CI_FUNC(ci4)
{
uint8_t *in8 = in;
unsigned sft = 4;
for (size_t n = 0; n < ntexels; n++, sft ^= 4) {
uint8_t c = (in8[n >> 1] >> sft) & 0xF;
if (!(c < npal))
return -1; // palette too small for image data
out[n] = c;
}
return 0;
}
PACK_CI_FUNC(ci4)
{
uint8_t *out8 = out;
unsigned sft = 4;
for (size_t n = 0; n < ntexels; n++, sft ^= 4) {
uint8_t c = in[n];
if (!(c < npal))
return -1; // palette too small for image data
if (sft == 4)
out8[n >> 1] = 0;
out8[n >> 1] |= c << sft;
}
return 0;
}
/* CI8 */
UNPACK_CI_FUNC(ci8)
{
uint8_t *in8 = in;
for (size_t n = 0; n < ntexels; n++) {
uint8_t c = in8[n] & 0xFF;
if (!(c < npal))
return -1; // palette too small for image data
out[n] = c;
}
return 0;
}
PACK_CI_FUNC(ci8)
{
uint8_t *out8 = out;
for (size_t n = 0; n < ntexels; n++) {
uint8_t c = in[n];
if (!(c < npal))
return -1; // palette too small for image data
out8[n] = c;
}
return 0;
}
/* IA4 */
UNPACK_FUNC(ia4)
{
uint8_t *in8 = in;
unsigned sft = 4;
for (size_t n = 0; n < ntexels; n++, sft ^= 4) {
uint8_t c = (in8[n >> 1] >> sft) & 0xF;
uint8_t i = c & 0b1110;
uint8_t a = c & 0b0001;
i = (i << 4) | (i << 1) | (i >> 2);
a = 255 * a;
out[n].r = i;
out[n].g = i;
out[n].b = i;
out[n].a = a;
}
return 0;
}
PACK_FUNC(ia4)
{
uint8_t *out8 = out;
unsigned sft = 4;
for (size_t n = 0; n < ntexels; n++, sft ^= 4) {
uint8_t g = texel_to_greyscale(in[n].r, in[n].g, in[n].b);
uint8_t i = (g >> 5) & 0b111;
uint8_t a = in[n].a != 0;
if (sft == 4)
out8[n >> 1] = 0;
out8[n >> 1] |= ((i << 1) | a) << sft;
}
return 0;
}
/* IA8 */
UNPACK_FUNC(ia8)
{
uint8_t *in8 = in;
for (size_t n = 0; n < ntexels; n++) {
uint8_t c = in8[n];
uint8_t i = (c >> 4) & 0xF;
uint8_t a = (c >> 0) & 0xF;
i = (i << 4) | i;
a = (a << 4) | a;
out[n].r = i;
out[n].g = i;
out[n].b = i;
out[n].a = a;
}
return 0;
}
PACK_FUNC(ia8)
{
uint8_t *out8 = out;
for (size_t n = 0; n < ntexels; n++) {
uint8_t g = texel_to_greyscale(in[n].r, in[n].g, in[n].b);
uint8_t i = (g >> 4) & 0xF;
uint8_t a = (in[n].a >> 4) & 0xF;
out8[n] = (i << 4) | a;
}
return 0;
}
/* IA16 */
UNPACK_FUNC(ia16)
{
uint16_t *in16 = in;
for (size_t n = 0; n < ntexels; n++) {
uint16_t c = be16toh(in16[n]);
uint8_t i = (c >> 8) & 0xFF;
uint8_t a = (c >> 0) & 0xFF;
out[n].r = i;
out[n].g = i;
out[n].b = i;
out[n].a = a;
}
return 0;
}
PACK_FUNC(ia16)
{
uint16_t *out16 = out;
for (size_t n = 0; n < ntexels; n++) {
uint8_t i = texel_to_greyscale(in[n].r, in[n].g, in[n].b);
uint8_t a = in[n].a;
out16[n] = htobe16((i << 8) | a);
}
return 0;
}
/* I4 */
UNPACK_FUNC(i4)
{
uint8_t *in8 = in;
unsigned sft = 4;
for (size_t n = 0; n < ntexels; n++, sft ^= 4) {
uint8_t i = (in8[n >> 1] >> sft) & 0xF;
i = (i << 4) | i;
out[n].r = i;
out[n].g = i;
out[n].b = i;
out[n].a = i;
}
return 0;
}
PACK_FUNC(i4)
{
uint8_t *out8 = out;
unsigned sft = 4;
for (size_t n = 0; n < ntexels; n++, sft ^= 4) {
uint8_t g = texel_to_greyscale(in[n].r, in[n].g, in[n].b);
uint8_t i = (g >> 4) & 0xF;
if (sft == 4)
out8[n >> 1] = 0;
out8[n >> 1] |= i << sft;
}
return 0;
}
/* I8 */
UNPACK_FUNC(i8)
{
uint8_t *in8 = in;
for (size_t n = 0; n < ntexels; n++) {
uint8_t i = in8[n];
out[n].r = i;
out[n].g = i;
out[n].b = i;
out[n].a = i;
}
return 0;
}
PACK_FUNC(i8)
{
uint8_t *out8 = out;
for (size_t n = 0; n < ntexels; n++) {
uint8_t i = texel_to_greyscale(in[n].r, in[n].g, in[n].b);
out8[n] = i;
}
return 0;
}
struct texel_layout {
const char *name;
union {
int (*pack)(void *out, struct color *in, size_t ntexels);
int (*pack_ci)(void *out, uint8_t *in, size_t ntexels, size_t npal);
};
union {
int (*unpack)(struct color *out, void *in, size_t ntexels);
int (*unpack_ci)(uint8_t *out, void *in, size_t ntexels, size_t npal);
};
};
#define FMT_SIZ(fmt, siz) (((fmt) << 2) | (siz))
struct texel_layout texel_layouts[32] = {
[FMT_SIZ(G_IM_FMT_RGBA, G_IM_SIZ_16b)] = { "rgba16", .pack = rgba16_pack, .unpack = rgba16_unpack, },
[FMT_SIZ(G_IM_FMT_RGBA, G_IM_SIZ_32b)] = { "rgba32", .pack = rgba32_pack, .unpack = rgba32_unpack, },
[FMT_SIZ(G_IM_FMT_CI, G_IM_SIZ_4b) ] = { "ci4", .pack_ci = ci4_pack, .unpack_ci = ci4_unpack, },
[FMT_SIZ(G_IM_FMT_CI, G_IM_SIZ_8b) ] = { "ci8", .pack_ci = ci8_pack, .unpack_ci = ci8_unpack, },
[FMT_SIZ(G_IM_FMT_IA, G_IM_SIZ_4b) ] = { "ia4", .pack = ia4_pack, .unpack = ia4_unpack, },
[FMT_SIZ(G_IM_FMT_IA, G_IM_SIZ_8b) ] = { "ia8", .pack = ia8_pack, .unpack = ia8_unpack, },
[FMT_SIZ(G_IM_FMT_IA, G_IM_SIZ_16b)] = { "ia16", .pack = ia16_pack, .unpack = ia16_unpack, },
[FMT_SIZ(G_IM_FMT_I, G_IM_SIZ_4b) ] = { "i4", .pack = i4_pack, .unpack = i4_unpack, },
[FMT_SIZ(G_IM_FMT_I, G_IM_SIZ_8b) ] = { "i8", .pack = i8_pack, .unpack = i8_unpack, },
};
static int
n64texconv_data_sync(struct color *texels, uint8_t *color_indices, size_t ntexels, int fmt, int siz, size_t pal_count)
{
assert(texels != NULL);
void *temp = malloc(texel_size_bytes(ntexels, siz));
if (temp == NULL)
return -1;
struct texel_layout *layout = &texel_layouts[FMT_SIZ(fmt, siz)];
assert(layout != NULL);
if (fmt == G_IM_FMT_CI) {
assert(color_indices != NULL);
layout->pack_ci(temp, color_indices, ntexels, pal_count);
layout->unpack_ci(color_indices, temp, ntexels, pal_count);
// TODO update texels from color indices too?
} else {
layout->pack(temp, texels, ntexels);
layout->unpack(texels, temp, ntexels);
}
free(temp);
return 0;
}
// ********************************************************************************************************************
// Palette
// ********************************************************************************************************************
static int
n64texconv_palette_sync(struct n64_palette *pal)
{
assert(pal != NULL);
return n64texconv_data_sync(pal->texels, NULL, pal->count, pal->fmt, G_IM_SIZ_16b, 0);
}
struct n64_palette *
n64texconv_palette_new(size_t count, int fmt)
{
if (fmt != G_IM_FMT_RGBA && fmt != G_IM_FMT_IA)
return NULL;
struct n64_palette *pal = malloc(sizeof(struct n64_palette) + count * sizeof(struct color));
if (pal == NULL)
return NULL;
pal->texels = (struct color *)(pal + 1);
pal->fmt = fmt;
pal->count = count;
return pal;
}
void
n64texconv_palette_free(struct n64_palette *pal)
{
assert(pal != NULL);
free(pal);
}
struct n64_palette *
n64texconv_palette_copy(struct n64_palette *pal)
{
assert(pal != NULL);
// Create new palette
struct n64_palette *new_pal = n64texconv_palette_new(pal->count, pal->fmt);
if (new_pal == NULL)
return NULL;
// Copy texels
memcpy(new_pal->texels, pal->texels, new_pal->count * sizeof(struct color));
return new_pal;
}
struct n64_palette *
n64texconv_palette_reformat(struct n64_palette *pal, int fmt)
{
assert(pal != NULL);
// If the target format and source format are the same, just copy
if (pal->fmt == fmt)
return n64texconv_palette_copy(pal);
// Create new palette
struct n64_palette *new_pal = n64texconv_palette_new(pal->count, pal->fmt);
if (new_pal == NULL)
return NULL;
n64texconv_palette_sync(new_pal);
return new_pal;
}
struct n64_palette *
n64texconv_palette_resize(struct n64_palette *pal, size_t new_count)
{
assert(pal != NULL);
struct n64_palette *new_pal = n64texconv_palette_new(new_count, pal->fmt);
size_t min_count = (new_count < pal->count) ? new_count : pal->count;
size_t i;
for (i = 0; i < min_count; i++)
new_pal->texels[i] = pal->texels[i];
for (i = min_count; i < new_count; i++)
new_pal->texels[i] = (struct color){ .w = 0 };
return new_pal;
}
struct n64_palette *
n64texconv_palette_from_png(const char *path, int fmt)
{
assert(path != NULL);
FILE *png = fopen(path, "rb");
if (png == NULL)
return NULL;
spng_ctx *ctx = spng_ctx_new(0);
assert(ctx != NULL);
int rv;
rv = spng_set_png_file(ctx, png);
assert(rv == 0);
size_t ntexels;
rv = spng_decoded_image_size(ctx, SPNG_FMT_RGBA8, &ntexels);
assert(rv == 0);
struct n64_palette *pal = n64texconv_palette_new(ntexels, fmt);
if (pal == NULL)
return NULL;
rv = spng_decode_image(ctx, (void *)pal->texels, ntexels, SPNG_FMT_RGBA8, 0);
assert(rv == 0);
for (size_t i = 0; i < ntexels; i++) {
pal->texels[i].w = htobe32(pal->texels[i].w);
}
spng_ctx_free(ctx);
fclose(png);
rv = n64texconv_palette_sync(pal);
assert(rv == 0);
return pal;
}
struct n64_palette *
n64texconv_palette_from_bin(void *data, size_t count, int fmt)
{
assert(data != NULL);
// Reserve new palette
struct n64_palette *pal = n64texconv_palette_new(count, fmt);
if (pal == NULL)
return NULL;
// Unpack data
// The format is always valid at this point since the above NULL check
// would be hit if an invalid palette format was passed in.
texel_layouts[FMT_SIZ(fmt, G_IM_SIZ_16b)].unpack(pal->texels, data, count);
return pal;
}
int
n64texconv_palette_to_png(const char *outpath, struct n64_palette *pal)
{
assert(outpath != NULL);
assert(pal != NULL);
// Compute arbitrary dimensions for palette
size_t width, height;
palette_dim(pal->count, &width, &height);
// Sync data to format
int rv;
rv = n64texconv_palette_sync(pal);
assert(rv == 0);
// Write texels
struct spng_ctx *ctx = spng_ctx_new(SPNG_CTX_ENCODER);
if (ctx == NULL)
return -1;
FILE *png = fopen(outpath, "wb");
if (png == NULL)
return -2;
rv = spng_set_png_file(ctx, png);
assert(rv == 0);
rv = spng_set_image_limits(ctx, width, height);
assert(rv == 0);
struct spng_ihdr ihdr = {
.width = width,
.height = height,
.bit_depth = 8,
.color_type = SPNG_COLOR_TYPE_TRUECOLOR_ALPHA,
.compression_method = 0,
.filter_method = 0,
.interlace_method = 0,
};
rv = spng_set_ihdr(ctx, &ihdr);
assert(rv == 0);
rv = spng_encode_image(ctx, pal->texels, pal->count * sizeof(struct color), SPNG_FMT_PNG, SPNG_ENCODE_FINALIZE);
assert(rv == 0);
spng_ctx_free(ctx);
fclose(png);
return 0;
}
void *
n64texconv_palette_to_bin(struct n64_palette *pal, bool pad_to_8b)
{
assert(pal != NULL);
// Prepare output buffer
size_t size_unaligned = texel_size_bytes(pal->count, G_IM_SIZ_16b);
size_t size_aligned = ALIGN8(size_unaligned);
void *out = malloc(size_aligned);
if (out == NULL)
return NULL;
// Pack into output format
assert(pal->fmt == G_IM_FMT_RGBA || pal->fmt == G_IM_FMT_IA);
texel_layouts[FMT_SIZ(pal->fmt, G_IM_SIZ_16b)].pack(out, pal->texels, pal->count);
// Pad to 8-byte alignment if requested
if (pad_to_8b && size_aligned != size_unaligned)
memset((uint8_t *)out + size_unaligned, 0, size_aligned - size_unaligned);
return out;
}
int
n64texconv_palette_to_c(char **out, size_t *size_out, struct n64_palette *pal, bool pad_to_8b, unsigned int byte_width)
{
assert(out != NULL);
assert(size_out != NULL);
assert(pal != NULL);
size_t nbytes = texel_size_bytes(pal->count, G_IM_SIZ_16b);
void *bin = n64texconv_palette_to_bin(pal, pad_to_8b);
if (bin == NULL)
return -1;
nbytes = pad_to_8b ? ALIGN8(nbytes) : nbytes;
int rv = bin2c(out, size_out, bin, nbytes, 0, byte_width);
free(bin);
return rv;
}
int
n64texconv_palette_to_c_file(const char *out_path, struct n64_palette *pal, bool pad_to_8b, unsigned int byte_width)
{
assert(out_path != NULL);
assert(pal != NULL);
size_t nbytes = texel_size_bytes(pal->count, G_IM_SIZ_16b);
void *bin = n64texconv_palette_to_bin(pal, pad_to_8b);
if (bin == NULL)
return -1;
nbytes = pad_to_8b ? ALIGN8(nbytes) : nbytes;
int rv = bin2c_file(out_path, bin, nbytes, 0, byte_width);
free(bin);
return rv;
}
// ********************************************************************************************************************
// Image
// ********************************************************************************************************************
static int
n64texconv_image_sync(struct n64_image *img)
{
assert(img != NULL);
return n64texconv_data_sync(img->texels, img->color_indices, img->width * img->height, img->fmt, img->siz,
(img->pal != NULL) ? img->pal->count : 0);
}
struct n64_image *
n64texconv_image_new(size_t width, size_t height, int fmt, int siz, struct n64_palette *pal)
{
// TODO if fmt is CI, assert(pal != NULL) ?
if (fmt >= FMT_MAX || siz >= SIZ_MAX)
return NULL;
size_t ntexels = width * height;
struct n64_image *img = malloc(sizeof(struct n64_image) + ntexels * (sizeof(struct color) + sizeof(uint8_t)));
if (img == NULL)
return NULL;
struct color *texels = (void *)((uint8_t *)img + sizeof(struct n64_image));
uint8_t *color_indices = (fmt == G_IM_FMT_CI) ? (uint8_t *)(texels + ntexels) : NULL;
img->width = width;
img->height = height;
img->fmt = fmt;
img->siz = siz;
img->pal = pal;
img->texels = texels;
img->color_indices = color_indices;
return img;
}
void
n64texconv_image_free(struct n64_image *img)
{
assert(img != NULL);
free(img);
}
struct n64_image *
n64texconv_image_copy(struct n64_image *img)
{
if (img == NULL)
return NULL;
// Create new image
struct n64_image *new_img = n64texconv_image_new(img->width, img->height, img->fmt, img->siz, img->pal);
if (new_img == NULL)
return NULL;
// Copy texels/index data
size_t ntexels = new_img->width * new_img->height;
memcpy(new_img->texels, img->texels, ntexels * sizeof(struct color));
if (img->fmt == G_IM_FMT_CI) // == new_img->fmt
memcpy(new_img->color_indices, img->color_indices, ntexels * sizeof(uint8_t));
return new_img;
}
/**
* Reads in the png image at `path`.
*
* For all choices of `fmt`, the png data is converted to a common RGBA32 format and stored that way, but may be
* subject to various edits described below.
*
* - If `fmt` is None (default), the best format and texel size are determined from the RGBA32 data, which is then
* processed accordingly (described below).
*
* - If `fmt` is RGBA, the RGBA32 texels are left as-is, with possible reduction if `siz` is 16-bit.
*
* - If `fmt` is greyscale (I or IA), the RGBA32 texels are converted to greyscale. If the texels were already
* greyscale this operation will leave them unaffected.
*
* - If `fmt` is CI and the png is an indexed png, the palette and indices in the png are saved in addition to the
* texels themselves. This is required for a roundtrip conversion of 'bin -> png -> file -> png -> bin' to match.
*
* - If `fmt` is CI and the png is not indexed, the image is quantized and a palette is generated automatically.
*/
struct n64_image *
n64texconv_image_from_png(const char *path, int fmt, int siz, int pal_fmt)
{
assert(path != NULL);
FILE *png = fopen(path, "rb");
if (png == NULL)
return NULL; // Input file not found
spng_ctx *ctx = spng_ctx_new(0);
int rv = spng_set_png_file(ctx, png);
assert(rv == 0);
struct spng_ihdr ihdr;
rv = spng_get_ihdr(ctx, &ihdr);
assert(rv == 0);
size_t nbytes;
rv = spng_decoded_image_size(ctx, SPNG_FMT_RGBA8, &nbytes);
assert(rv == 0);
uint32_t width = ihdr.width;
uint32_t height = ihdr.height;
struct n64_image *img = n64texconv_image_new(width, height, fmt, siz, NULL);
if (img == NULL)
goto error_post_open_png;
struct n64_palette *pal = NULL;
if (fmt == G_IM_FMT_CI) {
assert(siz == G_IM_SIZ_4b || siz == G_IM_SIZ_8b);
assert(pal_fmt != FMT_NONE);
size_t max_colors = (siz == G_IM_SIZ_8b) ? 256 : 16;
if (ihdr.color_type == SPNG_COLOR_TYPE_INDEXED) {
// Input is an indexed png, obtain palette and color indices
struct spng_plte plte;
rv = spng_get_plte(ctx, &plte);
assert(rv == 0); // must have a palette chunk if it's indexed
// Palette should not have 0 entries
if (plte.n_entries == 0)
goto error_post_create_img;
// Palette must have sufficiently few colors for the target format. If there are too
// many, requantize to the maximum amount for the target format.
if (plte.n_entries > max_colors)
goto requantize;
pal = n64texconv_palette_new(plte.n_entries, pal_fmt);
if (pal == NULL)
goto error_post_create_img;
struct spng_trns trns;
rv = spng_get_trns(ctx, &trns);
if (rv == 0) {
// Read palette transparency from here
assert(trns.n_type3_entries == plte.n_entries);
for (size_t i = 0; i < plte.n_entries; i++) {
pal->texels[i].r = plte.entries[i].red;
pal->texels[i].g = plte.entries[i].green;
pal->texels[i].b = plte.entries[i].blue;
pal->texels[i].a = trns.type3_alpha[i];
}
} else {
// Treat as opaque if trns is not present
for (size_t i = 0; i < plte.n_entries; i++) {
pal->texels[i].r = plte.entries[i].red;
pal->texels[i].g = plte.entries[i].green;
pal->texels[i].b = plte.entries[i].blue;
pal->texels[i].a = 0xFF;
}
}
// Fill color indices
size_t nidxbytes;
rv = spng_decoded_image_size(ctx, SPNG_FMT_RAW, &nidxbytes);
assert(rv == 0);
assert(nidxbytes * 4 == nbytes);
rv = spng_decode_image(ctx, (void *)img->color_indices, nidxbytes, SPNG_FMT_RAW, 0);
assert(rv == 0);
} else {
// Input is not an indexed png, quantize and generate palette
requantize: // Input is an indexed png but has too many colors, requantize with new palette
rv = spng_decode_image(ctx, (void *)img->texels, nbytes, SPNG_FMT_RGBA8, 0);
assert(rv == 0);
pal = n64texconv_palette_new(max_colors, pal_fmt);
if (pal == NULL)
goto error_post_create_img;
n64texconv_quantize(img->color_indices, pal->texels, &pal->count, img->texels, width, height, max_colors,
0.5f);
}
// Populate texels from color indices and palette
for (size_t i = 0; i < width * height; i++)
img->texels[i] = pal->texels[img->color_indices[i]];
} else {
rv = spng_decode_image(ctx, (void *)img->texels, nbytes, SPNG_FMT_RGBA8, 0);
assert(rv == 0);
}
spng_ctx_free(ctx);
fclose(png);
img->pal = pal;
n64texconv_image_sync(img);
return img;
error_post_create_img:
n64texconv_image_free(img);
error_post_open_png:
spng_ctx_free(ctx);
fclose(png);
return NULL;
}
struct n64_image *
n64texconv_image_from_bin(void *data, size_t width, size_t height, int fmt, int siz, struct n64_palette *pal,
bool preswapped)
{
assert(data != NULL);
assert((fmt == G_IM_FMT_CI && pal != NULL) || (fmt != G_IM_FMT_CI && pal == NULL));
size_t ntexels = width * height;
struct n64_image *img = n64texconv_image_new(width, height, fmt, siz, pal);
if (img == NULL)
return NULL;
if (preswapped) {
// TODO unswap data
}
struct texel_layout *layout = &texel_layouts[FMT_SIZ(fmt, siz)];
assert(layout != NULL);
if (fmt == G_IM_FMT_CI) {
layout->unpack_ci(img->color_indices, data, ntexels, img->pal->count);
for (size_t i = 0; i < ntexels; i++)
img->texels[i].w = pal->texels[img->color_indices[i]].w;
} else {
layout->unpack(img->texels, data, ntexels);
}
return img;
}
/**
* Given an existing image, create a new image with the new format and size.
*
* This operation is very often a lossy process, do not expect matching round-trips through reformat; depending on the
* target format quality is often lost, sometimes severely.
*
* This process is typically for converting truecolor png to the optimal N64 format if the format was left unspecified
* when reading in the png, in a matching context the format should always be explicitly specified so that no
* reformatting is done.
*/
struct n64_image *
n64texconv_image_reformat(struct n64_image *img, int fmt, int siz, struct n64_palette *new_pal)
{
assert(img != NULL);
if (fmt == FMT_NONE || siz == SIZ_NONE) {
// TODO determine optimal format for data
}
size_t width = img->width;
size_t height = img->height;
size_t ntexels = width * height;
struct n64_image *new_img = n64texconv_image_new(width, height, fmt, siz, NULL);
if (new_img == NULL)
return NULL;
bool src_ci = img->fmt == G_IM_FMT_CI;
bool dst_ci = fmt == G_IM_FMT_CI;
bool siz_changed = siz != img->siz;
if (dst_ci && (!src_ci || siz_changed)) {
// Converting non-CI to CI, or CI->CI of different size
size_t max_colors = (siz == G_IM_SIZ_8b) ? 256 : 16;
// Set color indices pointer if coming from non-CI
if (new_img->color_indices == NULL)
new_img->color_indices = (void *)(new_img->texels + ntexels);
if (new_pal != NULL) {
// Use the provided new palette
new_img->pal = new_pal;
// Make sure the new palette is of an appropriate size for the bit depth
if (new_pal->count > max_colors) {
n64texconv_image_free(new_img);
return NULL;
}
// Map to the palette
for (size_t i = 0; i < width * height; i++) {
for (size_t j = 0; j < new_pal->count; j++) {
if (new_img->texels[i].w == new_pal->texels[j].w) {
new_img->color_indices[i] = j;
goto got;
}
}
// Failed to find texel in palette
n64texconv_image_free(new_img);
return NULL;
got:;
}
} else {
// Generate a new palette
struct n64_palette *pal = n64texconv_palette_new(max_colors, G_IM_FMT_RGBA); // TODO IA palettes
if (pal == NULL) {
n64texconv_image_free(new_img);
return NULL;
}
new_img->pal = pal;
// Quantize and fill color indices
n64texconv_quantize(new_img->color_indices, pal->texels, &pal->count, new_img->texels, width, height,
max_colors, 0.5f);
// Replace old texels
for (size_t i = 0; i < width * height; i++) {
new_img->texels[i] = pal->texels[new_img->color_indices[i]];
}
}
} else {
// Copy texels
memcpy(new_img->texels, img->texels, ntexels * sizeof(struct color));
if (dst_ci)
memcpy(new_img->color_indices, img->color_indices, ntexels * sizeof(uint8_t));
}
// Invalidate color indices if non-CI
if (!dst_ci)
new_img->color_indices = NULL;
// Sync texels to format
n64texconv_image_sync(new_img);
//#pragma GCC diagnostic push
//#pragma GCC diagnostic ignored "-Wanalyzer-malloc-leak"
// Annoyingly the GCC static analyzer considers pal to be leaked here, but it's reachable by the assignment
// to new_img...
return new_img;
//#pragma GCC diagnostic pop
}
/**
* Writes this texture out to a png file. If this texture is CI and was read in from binary or from an indexed png it
* will preserve the color indices and palette of the original if it has not since been reformatted in order to
* facilitate matching in a roundtrip of 'bin -> png -> file -> png -> bin'.
*/
int
n64texconv_image_to_png(const char *outpath, struct n64_image *img, bool intensity_alpha)
{
assert(outpath != NULL);
assert(img != NULL);
// For intensity images, we don't always want to write alpha (but sometimes we do), intensity images can be either
// RGB channel or Alpha channel but often not both. The `intensity_alpha` option determines whether to write PNGs
// with or without alpha, both will roundtrip identically.
bool has_alpha = (img->fmt != G_IM_FMT_I) || intensity_alpha;
size_t ntexels = img->width * img->height;
if (img->fmt == G_IM_FMT_CI) {
assert(img->color_indices != NULL); // CI textures must have color indices
assert(img->pal != NULL); // Writing CI to png must have a palette supplied
n64texconv_palette_sync(img->pal);
}
// Ensure output is visually precise
n64texconv_image_sync(img);
// Write the png, either truecolor or indexed
FILE *png = fopen(outpath, "wb");
if (png == NULL)
return -1;
struct spng_ctx *ctx = spng_ctx_new(SPNG_CTX_ENCODER);
int rv = spng_set_png_file(ctx, png);
assert(rv == 0);
rv = spng_set_image_limits(ctx, img->width, img->height);
assert(rv == 0);
uint8_t color_type;
if (img->fmt == G_IM_FMT_CI) {
color_type = SPNG_COLOR_TYPE_INDEXED;
} else {
color_type = (has_alpha) ? SPNG_COLOR_TYPE_TRUECOLOR_ALPHA : SPNG_COLOR_TYPE_TRUECOLOR;
}
struct spng_ihdr ihdr = {
.width = img->width,
.height = img->height,
.bit_depth = 8,
.color_type = color_type,
.compression_method = 0,
.filter_method = 0,
.interlace_method = 0,
};
rv = spng_set_ihdr(ctx, &ihdr);
assert(rv == 0);
if (img->fmt == G_IM_FMT_CI) {
struct n64_palette *pal = img->pal;
assert(pal != NULL);
// Ensure palette is visually precise
rv = n64texconv_palette_sync(pal);
assert(rv == 0);
// Write palette
struct spng_plte plte = { 0 };
plte.n_entries = pal->count;
assert(pal->count <= 256);
memcpy(plte.entries, pal->texels, pal->count * 4);
rv = spng_set_plte(ctx, &plte);
assert(rv == 0);
// Write palette alphas
struct spng_trns trns = { 0 };
trns.n_type3_entries = pal->count;
for (size_t i = 0; i < pal->count; i++) {
trns.type3_alpha[i] = pal->texels[i].a;
}
rv = spng_set_trns(ctx, &trns);
assert(rv == 0);
// Write color indices
rv = spng_encode_image(ctx, img->color_indices, ntexels * sizeof(uint8_t), SPNG_FMT_PNG, SPNG_ENCODE_FINALIZE);
assert(rv == 0);
} else {
void *out = img->texels;
size_t outsize = ntexels * sizeof(struct color);
if (!has_alpha) {
uint8_t *rgb = malloc(ntexels * 3);
assert(rgb != NULL);
size_t j = 0;
for (size_t i = 0; i < ntexels; i++) {
rgb[j++] = img->texels[i].r;
rgb[j++] = img->texels[i].g;
rgb[j++] = img->texels[i].b;
}
out = rgb;
outsize = ntexels * 3;
}
// Write texels
rv = spng_encode_image(ctx, out, outsize, SPNG_FMT_PNG, SPNG_ENCODE_FINALIZE);
assert(rv == 0);
if (!has_alpha)
free(out);
}
spng_ctx_free(ctx);
fclose(png);
return 0;
}
void *
n64texconv_image_to_bin(struct n64_image *img, bool pad_to_8b, bool preswap)
{
assert(img != NULL);
size_t ntexels = img->width * img->height;
int fmt = img->fmt;
int siz = img->siz;
// Reserve output buffer
size_t size_bytes = texel_size_bytes(ntexels, siz);
size_t size_bytes_aligned = ALIGN8(size_bytes);
void *out = malloc(size_bytes_aligned);
if (out == NULL)
return NULL;
// Pack into output format
struct texel_layout *layout = &texel_layouts[FMT_SIZ(fmt, siz)];
assert(layout != NULL);
if (fmt == G_IM_FMT_CI) {
assert(img->pal != NULL);
layout->pack_ci(out, img->color_indices, ntexels, img->pal->count);
} else {
assert(img->pal == NULL);
layout->pack(out, img->texels, ntexels);
}
// Pad to 8-byte alignment if requested
if (pad_to_8b && size_bytes != size_bytes_aligned) {
memset((uint8_t *)out + size_bytes, 0, size_bytes_aligned - size_bytes);
}
// Perform dxt=0 word swapping if requested
if (preswap) {
// TODO implement preswap
}
return out;
}
int
n64texconv_image_to_c(char **out, size_t *size_out, struct n64_image *img, bool pad_to_8b, bool preswap,
unsigned int byte_width)
{
assert(out != NULL);
assert(size_out != NULL);
assert(img != NULL);
size_t nbytes = texel_size_bytes(img->width * img->height, img->siz);
void *bin = n64texconv_image_to_bin(img, pad_to_8b, preswap);
if (bin == NULL)
return -1;
nbytes = pad_to_8b ? ALIGN8(nbytes) : nbytes;
int rv = bin2c(out, size_out, bin, nbytes, 0, byte_width);
free(bin);
return rv;
}
int
n64texconv_image_to_c_file(const char *out_path, struct n64_image *img, bool pad_to_8b, bool preswap,
unsigned int byte_width)
{
assert(out_path != NULL);
assert(img != NULL);
size_t nbytes = texel_size_bytes(img->width * img->height, img->siz);
void *bin = n64texconv_image_to_bin(img, pad_to_8b, preswap);
if (bin == NULL)
return -1;
nbytes = pad_to_8b ? ALIGN8(nbytes) : nbytes;
int rv = bin2c_file(out_path, bin, nbytes, 0, byte_width);
free(bin);
return rv;
}
// ********************************************************************************************************************
// Others
// ********************************************************************************************************************
const char *
n64texconv_png_extension(struct n64_image *img)
{
assert(img != NULL);
struct texel_layout *layout = &texel_layouts[FMT_SIZ(img->fmt, img->siz)];
assert(layout != NULL);
return layout->name;
}
/* This is just for the python interface */
void
n64texconv_free(void *p)
{
assert(p != NULL);
free(p);
}