1
0
Fork 0
mirror of https://github.com/zeldaret/oot.git synced 2025-02-23 15:55:47 +00:00
oot/tools/assets/n64texconv/__init__.py
Tharo 3d61fb85ef
Add n64texconv and bin2c tools to convert extracted .png and .bin to C arrays during build (#2477)
* n64texconv and bin2c

* mv tools/n64texconv tools/assets/

* fix

* more light fixes

* Silence -Wshadow for libimagequant

* Add reference counting gc for palette objects in python bindings

* Fix missing alignment in n64texconv_*_to_c functions

Co-authored-by: Dragorn421 <Dragorn421@users.noreply.github.com>

* Check palette size in n64texconv_image_from_png

* accept memoryview as well as bytes for binary data

* minimal doc on n64texconv_quantize_shared

* fix a buffer size passed to libimagequant

* assert pal count <= 256 on png write

* Disable palette size check for input pngs, ZAPD fails the check

* No OpenMP for clang

* When reading an indexed png into a CI format, requantize if there are too many colors for the target texel size

---------

Co-authored-by: Dragorn421 <Dragorn421@users.noreply.github.com>
2025-02-17 17:09:42 -05:00

472 lines
20 KiB
Python

# SPDX-FileCopyrightText: © 2025 ZeldaRET
# SPDX-License-Identifier: MIT
import os, sys
from ctypes import CDLL, Structure, POINTER, string_at, byref, cast
from ctypes import c_void_p, c_char_p, c_uint8, c_uint, c_bool, c_int, c_size_t
from typing import Optional
def ctypes_buffer_to_string(buffer) -> str:
return buffer.value.decode('utf-8')
def ctypes_pointer_to_bytes(ptr : c_void_p, size : int) -> bytes:
return string_at(ptr, size)
def deref(ptr):
if ptr:
return ptr.contents
return None
ln64texconv = CDLL(os.path.join(os.path.dirname(__file__), "libn64texconv.so"))
class RefCounter:
def __init__(self) -> None:
self.ref_counts = {}
def add_ref(self, ptr):
if not isinstance(ptr, POINTER(N64Palette)):
ptr = cast(ptr, POINTER(N64Palette))
if not ptr:
return
key = int.from_bytes(ptr, byteorder=sys.byteorder, signed=False)
if key not in self.ref_counts:
self.ref_counts[key] = 1
else:
self.ref_counts[key] += 1
def num_refs(self, ptr):
if not isinstance(ptr, POINTER(N64Palette)):
ptr = cast(ptr, POINTER(N64Palette))
if not ptr:
return 0
key = int.from_bytes(ptr, byteorder=sys.byteorder, signed=False)
if key not in self.ref_counts:
return 0
return self.ref_counts[key]
def rm_ref(self, ptr, free_func):
if not isinstance(ptr, POINTER(N64Palette)):
ptr = cast(ptr, POINTER(N64Palette))
if not ptr:
return
key = int.from_bytes(ptr, byteorder=sys.byteorder, signed=False)
assert key in self.ref_counts
count = self.ref_counts.pop(key)
count -= 1
if count == 0:
free_func(ptr)
else:
self.ref_counts[key] = count
# Simple reference counter for C allocations
_object_refcount = RefCounter()
#
# Private
#
# void n64texconv_free(void *p);
ln64texconv.n64texconv_free.argtypes = [c_void_p]
ln64texconv.n64texconv_free.restype = None
#
# bin2c.h
#
def bin2c(data : bytes | memoryview, pad_to_size : int = 0, byte_width : int = 8) -> Optional[str]:
if byte_width not in (1, 2, 4, 8):
raise ValueError("Invalid byte width, must be 1, 2, 4 or 8")
buffer = (c_uint8 * len(data)).from_buffer_copy(data)
ptr = c_char_p(None)
size = c_size_t(0)
if ln64texconv.bin2c(byref(ptr), byref(size), buffer, len(data), pad_to_size, byte_width) != 0:
return None
s = ctypes_buffer_to_string(ptr)
ln64texconv.n64texconv_free(ptr)
return s
def bin2c_file(out_path : str, data : bytes | memoryview, pad_to_size : int = 0, byte_width : int = 8) -> bool:
if byte_width not in (1, 2, 4, 8):
raise ValueError("Invalid byte width, must be 1, 2, 4 or 8")
buffer = (c_uint8 * len(data)).from_buffer_copy(data)
return ln64texconv.bin2c_file(out_path.encode("utf-8"), buffer, len(data), pad_to_size, byte_width) == 0
# int bin2c(char **out, size_t *size_out, void *bin, size_t size, size_t pad_to_size, unsigned int byte_width);
ln64texconv.bin2c.argtypes = [POINTER(c_char_p), POINTER(c_size_t), c_void_p, c_size_t, c_size_t, c_uint]
ln64texconv.bin2c.restype = c_int
# int bin2c_file(const char *out_path, void *bin, size_t size, size_t pad_to_size, unsigned int byte_width);
ln64texconv.bin2c_file.argtypes = [c_char_p, c_void_p, c_size_t, c_size_t, c_uint]
ln64texconv.bin2c_file.restype = c_int
#
# jfif.h
#
# struct JFIF
class JFIF(Structure):
_fields_ = [
("data", c_void_p),
("data_size", c_size_t),
]
# JFIF_BUFFER_SIZE
BUFFER_SIZE = 320 * 240 * 2
@staticmethod
def fromfile(path : str, max_size : int = BUFFER_SIZE) -> Optional["JFIF"]:
if not os.path.isfile(path):
raise ValueError(f"Cannot open \"{path}\", is not a file")
return deref(ln64texconv.jfif_fromfile(path.encode("utf-8"), max_size))
def to_c(self, pad_to_size : int = BUFFER_SIZE) -> Optional[str]:
ptr = c_char_p(None)
size = c_size_t(0)
if ln64texconv.jfif_to_c(byref(ptr), byref(size), byref(self), pad_to_size) != 0:
return None
s = ctypes_buffer_to_string(ptr)
ln64texconv.n64texconv_free(ptr)
return s
def to_c_file(self, out_path : str, pad_to_size : int = BUFFER_SIZE) -> bool:
return ln64texconv.jfif_to_c_file(out_path.encode("utf-8"), byref(self), pad_to_size) == 0
def __del__(self):
ln64texconv.jfif_free(byref(self))
# struct JFIF *jfif_fromfile(const char *path, size_t max_size);
ln64texconv.jfif_fromfile.argtypes = [c_char_p, c_size_t]
ln64texconv.jfif_fromfile.restype = POINTER(JFIF)
# void jfif_free(struct JFIF *jfif);
ln64texconv.jfif_free.argtypes = [POINTER(JFIF)]
ln64texconv.jfif_free.restype = None
# int jfif_to_c(char **out, size_t *size_out, struct JFIF *jfif, size_t pad_to_size)
ln64texconv.jfif_to_c.argtypes = [POINTER(c_char_p), POINTER(c_size_t), POINTER(JFIF), c_size_t]
ln64texconv.jfif_to_c.restype = c_int
# int jfif_to_c_file(const char *out_path, struct JFIF *jfif, size_t pad_to_size);
ln64texconv.jfif_to_c_file.argtypes = [c_char_p, POINTER(JFIF), c_size_t]
ln64texconv.jfif_to_c_file.restype = c_int
#
# n64texconv.h
#
FMT_NONE = -1
FMT_MAX = 5
G_IM_FMT_RGBA = 0
G_IM_FMT_YUV = 1
G_IM_FMT_CI = 2
G_IM_FMT_IA = 3
G_IM_FMT_I = 4
SIZ_NONE = -1
SIZ_MAX = 4
G_IM_SIZ_4b = 0
G_IM_SIZ_8b = 1
G_IM_SIZ_16b = 2
G_IM_SIZ_32b = 3
def fmt_name(fmt : int):
return {
G_IM_FMT_RGBA : "G_IM_FMT_RGBA",
G_IM_FMT_YUV : "G_IM_FMT_YUV",
G_IM_FMT_CI : "G_IM_FMT_CI",
G_IM_FMT_IA : "G_IM_FMT_IA",
G_IM_FMT_I : "G_IM_FMT_I",
}.get(fmt, str(fmt))
def siz_name(siz : int):
return {
G_IM_SIZ_4b : "G_IM_SIZ_4b",
G_IM_SIZ_8b : "G_IM_SIZ_8b",
G_IM_SIZ_16b : "G_IM_SIZ_16b",
G_IM_SIZ_32b : "G_IM_SIZ_32b",
}.get(siz, str(siz))
VALID_FORMAT_COMBINATIONS = (
(G_IM_FMT_RGBA, G_IM_SIZ_16b),
(G_IM_FMT_RGBA, G_IM_SIZ_32b),
(G_IM_FMT_CI, G_IM_SIZ_4b),
(G_IM_FMT_CI, G_IM_SIZ_8b),
(G_IM_FMT_IA, G_IM_SIZ_4b),
(G_IM_FMT_IA, G_IM_SIZ_8b),
(G_IM_FMT_IA, G_IM_SIZ_16b),
(G_IM_FMT_I, G_IM_SIZ_4b),
(G_IM_FMT_I, G_IM_SIZ_8b),
)
# struct color
class Color(Structure):
_fields_ = [
("r", c_uint8),
("g", c_uint8),
("b", c_uint8),
("a", c_uint8),
]
# static inline size_t texel_size_bytes(size_t ntexels, int siz)
def texel_size_bytes(ntexels : int, siz : int):
return (ntexels // 2) if (siz == G_IM_SIZ_4b) else (ntexels * ((1 << siz) >> 1))
# struct n64_palette
class N64Palette(Structure):
_fields_ = [
("texels", POINTER(Color)),
("fmt", c_int),
("count", c_size_t),
]
@staticmethod
def new(count : int, fmt : int) -> Optional["N64Palette"]:
if fmt not in (G_IM_FMT_RGBA, G_IM_FMT_IA):
raise ValueError("Palette format must be either G_IM_FMT_RGBA or G_IM_FMT_IA")
if count > 256:
raise ValueError("The largest possible palette size is 256")
pal = ln64texconv.n64texconv_palette_new(count, fmt)
_object_refcount.add_ref(pal)
return deref(pal)
def __del__(self):
# Free the underlying palette structure only if the refcount is 0
_object_refcount.rm_ref(byref(self), ln64texconv.n64texconv_palette_free)
def copy(self) -> Optional["N64Palette"]:
pal = ln64texconv.n64texconv_palette_copy(byref(self))
_object_refcount.add_ref(pal)
return deref(pal)
def reformat(self, fmt : int) -> Optional["N64Palette"]:
if fmt not in (G_IM_FMT_RGBA, G_IM_FMT_IA):
raise ValueError("Palette format must be either G_IM_FMT_RGBA or G_IM_FMT_IA")
pal = ln64texconv.n64texconv_palette_reformat(byref(self), fmt)
_object_refcount.add_ref(pal)
return deref(pal)
@staticmethod
def from_png(path : str, fmt : int) -> Optional["N64Palette"]:
if fmt not in (G_IM_FMT_RGBA, G_IM_FMT_IA):
raise ValueError("Palette format must be either G_IM_FMT_RGBA or G_IM_FMT_IA")
if not os.path.isfile(path):
raise ValueError(f"Cannot open \"{path}\", is not a file")
pal = ln64texconv.n64texconv_palette_from_png(path.encode("utf-8"), fmt)
_object_refcount.add_ref(pal)
return deref(pal)
@staticmethod
def from_bin(data : bytes | memoryview, fmt : int) -> Optional["N64Palette"]:
if fmt not in (G_IM_FMT_RGBA, G_IM_FMT_IA):
raise ValueError("Palette format must be either G_IM_FMT_RGBA or G_IM_FMT_IA")
buffer = (c_uint8 * len(data)).from_buffer_copy(data)
pal = ln64texconv.n64texconv_palette_from_bin(buffer, len(data) // 2, fmt)
_object_refcount.add_ref(pal)
return deref(pal)
def to_png(self, outpath : str) -> bool:
return ln64texconv.n64texconv_palette_to_png(outpath.encode("utf-8"), byref(self)) == 0
def to_bin(self, pad_to_8b : bool) -> Optional[bytes]:
nbytes = texel_size_bytes(self.count, G_IM_SIZ_16b)
if pad_to_8b:
nbytes = (nbytes + 7) & ~7
ptr = ln64texconv.n64texconv_palette_to_bin(byref(self), pad_to_8b)
if not ptr:
return None
data = ctypes_pointer_to_bytes(ptr, nbytes)
ln64texconv.n64texconv_free(ptr)
return data
def to_c(self, pad_to_8b : bool, byte_width : int) -> Optional[str]:
ptr = c_char_p(None)
size = c_size_t(0)
if ln64texconv.n64texconv_palette_to_c(byref(ptr), byref(size), byref(self), pad_to_8b, byte_width) != 0:
return None
s = ctypes_buffer_to_string(ptr)
ln64texconv.n64texconv_free(ptr)
return s
def to_c_file(self, out_path : str, pad_to_8b : bool, byte_width : int) -> bool:
return ln64texconv.n64texconv_palette_to_c_file(out_path.encode("utf-8"), byref(self), pad_to_8b, byte_width) == 0
# struct n64_palette *n64texconv_palette_new(size_t count, int fmt);
ln64texconv.n64texconv_palette_new.argtypes = [c_size_t, c_int]
ln64texconv.n64texconv_palette_new.restype = POINTER(N64Palette)
# void n64texconv_palette_free(struct n64_palette *pal);
ln64texconv.n64texconv_palette_free.argtypes = [POINTER(N64Palette)]
ln64texconv.n64texconv_palette_free.restype = None
# struct n64_palette *n64texconv_palette_copy(struct n64_palette *pal);
ln64texconv.n64texconv_palette_copy.argtypes = [POINTER(N64Palette)]
ln64texconv.n64texconv_palette_copy.restype = POINTER(N64Palette)
# struct n64_palette *n64texconv_palette_reformat(struct n64_palette *pal, int fmt);
ln64texconv.n64texconv_palette_reformat.argtypes = [POINTER(N64Palette), c_int]
ln64texconv.n64texconv_palette_reformat.restype = POINTER(N64Palette)
# struct n64_palette *n64texconv_palette_from_png(const char *path, int fmt);
ln64texconv.n64texconv_palette_from_png.argtypes = [c_char_p, c_int]
ln64texconv.n64texconv_palette_from_png.restype = POINTER(N64Palette)
# struct n64_palette *n64texconv_palette_from_bin(void *data, size_t count, int fmt);
ln64texconv.n64texconv_palette_from_bin.argtypes = [c_void_p, c_size_t, c_int]
ln64texconv.n64texconv_palette_from_bin.restype = POINTER(N64Palette)
# int n64texconv_palette_to_png(const char *outpath, struct n64_palette *pal);
ln64texconv.n64texconv_palette_to_png.argtypes = [c_char_p, POINTER(N64Palette)]
ln64texconv.n64texconv_palette_to_png.restype = c_int
# void *n64texconv_palette_to_bin(struct n64_palette *pal, bool pad_to_8b);
ln64texconv.n64texconv_palette_to_bin.argtypes = [POINTER(N64Palette), c_bool]
ln64texconv.n64texconv_palette_to_bin.restype = c_void_p
# int n64texconv_palette_to_c(char **out, size_t *size_out, struct n64_palette *pal, bool pad_to_8b, unsigned int byte_width);
ln64texconv.n64texconv_palette_to_c.argtypes = [POINTER(c_char_p), POINTER(c_size_t), POINTER(N64Palette), c_bool, c_uint]
ln64texconv.n64texconv_palette_to_c.restype = c_int
# int n64texconv_palette_to_c_file(const char *out_path, struct n64_palette *pal, bool pad_to_8b, unsigned int byte_width);
ln64texconv.n64texconv_palette_to_c_file.argtypes = [c_char_p, POINTER(N64Palette), c_bool, c_uint]
ln64texconv.n64texconv_palette_to_c_file.restype = c_int
# struct n64_image
class N64Image(Structure):
_fields_ = [
("width", c_size_t),
("height", c_size_t),
("fmt", c_int),
("siz", c_int),
("pal", POINTER(N64Palette)),
("texels", POINTER(Color)),
("color_indices", POINTER(c_uint8)),
]
def get_palette(self) -> Optional[N64Palette]:
return deref(self.pal)
@staticmethod
def new(width : int, height : int, fmt : int, siz : int, pal : N64Palette = None) -> Optional["N64Image"]:
if not any((fmt, siz) == fmtsiz for fmtsiz in VALID_FORMAT_COMBINATIONS):
raise ValueError(f"Invalid fmt/siz combination ({fmt_name(fmt)}, {siz_name(siz)})")
if pal is not None:
_object_refcount.add_ref(byref(pal))
return deref(ln64texconv.n64texconv_image_new(width, height, fmt, siz, pal))
def __del__(self):
ln64texconv.n64texconv_image_free(byref(self))
# Also free the palette if the reference count drops to 0
_object_refcount.rm_ref(self.pal, ln64texconv.n64texconv_palette_free)
def copy(self) -> Optional["N64Image"]:
_object_refcount.add_ref(self.pal)
return deref(ln64texconv.n64texconv_image_copy(byref(self)))
@staticmethod
def from_png(path : str, fmt : int, siz : int, pal_fmt : int = FMT_NONE) -> Optional["N64Image"]:
if not os.path.isfile(path):
raise ValueError(f"Cannot open \"{path}\", is not a file")
if not any((fmt, siz) == fmtsiz for fmtsiz in VALID_FORMAT_COMBINATIONS):
raise ValueError(f"Invalid fmt/siz combination ({fmt_name(fmt)}, {siz_name(siz)})")
if fmt == G_IM_FMT_CI and pal_fmt not in (G_IM_FMT_RGBA, G_IM_FMT_IA):
raise ValueError(f"Invalid palette format {fmt_name(pal_fmt)}, must be either G_IM_FMT_RGBA or G_IM_FMT_IA")
img = deref(ln64texconv.n64texconv_image_from_png(path.encode("utf-8"), fmt, siz, pal_fmt))
_object_refcount.add_ref(img.pal)
return img
@staticmethod
def from_bin(data : bytes | memoryview, width : int, height : int, fmt : int, siz : int, pal : Optional[N64Palette] = None,
preswapped : bool = False) -> Optional["N64Image"]:
if not any((fmt, siz) == fmtsiz for fmtsiz in VALID_FORMAT_COMBINATIONS):
raise ValueError(f"Invalid fmt/siz combination ({fmt_name(fmt)}, {siz_name(siz)})")
expected_size = texel_size_bytes(width * height, siz)
if len(data) < expected_size:
raise ValueError(f"Not enough data to extract the specified image. " +
f"Expected at least 0x{expected_size:X} bytes but only got 0x{len(data):X} bytes")
buffer = (c_uint8 * len(data)).from_buffer_copy(data)
if pal:
pal = byref(pal)
_object_refcount.add_ref(pal)
img = ln64texconv.n64texconv_image_from_bin(buffer, width, height, fmt, siz, pal, preswapped)
return deref(img)
def reformat(self, fmt : int, siz : int, pal : Optional[N64Palette] = None) -> Optional["N64Image"]:
if not any((fmt, siz) == fmtsiz for fmtsiz in VALID_FORMAT_COMBINATIONS):
raise ValueError(f"Invalid fmt/siz combination ({fmt_name(fmt)}, {siz_name(siz)})")
if pal:
pal = byref(pal)
_object_refcount.add_ref(pal)
return deref(ln64texconv.n64texconv_image_reformat(byref(self), fmt, siz, pal))
def to_png(self, outpath : str, intensity_alpha : bool) -> bool:
return ln64texconv.n64texconv_image_to_png(outpath.encode("utf-8"), byref(self), intensity_alpha) == 0
def to_bin(self, pad_to_8b : bool, preswap : bool) -> Optional[bytes]:
nbytes = texel_size_bytes(self.width * self.height, self.siz)
if pad_to_8b:
nbytes = (nbytes + 7) & ~7
ptr = ln64texconv.n64texconv_image_to_bin(byref(self), pad_to_8b, preswap)
if not ptr:
return None
data = ctypes_pointer_to_bytes(ptr, nbytes)
ln64texconv.n64texconv_free(ptr)
return data
def to_c(self, pad_to_8b : bool, preswap : bool, byte_width : int) -> Optional[str]:
ptr = c_char_p(None)
size = c_size_t(0)
if ln64texconv.n64texconv_image_to_c(byref(ptr), byref(size), byref(self), pad_to_8b, preswap, byte_width) != 0:
return None
s = ctypes_buffer_to_string(ptr)
ln64texconv.n64texconv_free(ptr)
return s
def to_c_file(self, out_path : str, pad_to_8b : bool, preswap : bool, byte_width : int) -> bool:
return ln64texconv.n64texconv_image_to_c_file(out_path.encode("utf-8"), byref(self), pad_to_8b, preswap, byte_width) == 0
def png_extension(self) -> str:
return ln64texconv.n64texconv_png_extension(byref(self)).decode("utf-8")
# struct n64_image *n64texconv_image_new(size_t width, size_t height, int fmt, int siz, struct n64_palette *pal);
ln64texconv.n64texconv_image_new.argtypes = [c_size_t, c_size_t, c_int, c_int, POINTER(N64Palette)]
ln64texconv.n64texconv_image_new.restype = POINTER(N64Image)
# void n64texconv_image_free(struct n64_image *img);
ln64texconv.n64texconv_image_free.argtypes = [POINTER(N64Image)]
ln64texconv.n64texconv_image_free.restype = None
# struct n64_image *n64texconv_image_copy(struct n64_image *img);
ln64texconv.n64texconv_image_copy.argtypes = [POINTER(N64Image)]
ln64texconv.n64texconv_image_copy.restype = POINTER(N64Image)
# struct n64_image *n64texconv_image_from_png(const char *path, int fmt, int siz, int pal_fmt);
ln64texconv.n64texconv_image_from_png.argtypes = [c_char_p, c_int, c_int, c_int]
ln64texconv.n64texconv_image_from_png.restype = POINTER(N64Image)
# 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);
ln64texconv.n64texconv_image_from_bin.argtypes = [c_void_p, c_size_t, c_size_t, c_int, c_int, POINTER(N64Palette), c_bool]
ln64texconv.n64texconv_image_from_bin.restype = POINTER(N64Image)
# struct n64_image *n64texconv_image_reformat(struct n64_image *img, int fmt, int siz, struct n64_palette *pal);
ln64texconv.n64texconv_image_reformat.argtypes = [POINTER(N64Image), c_int, c_int, POINTER(N64Palette)]
ln64texconv.n64texconv_image_reformat.restype = POINTER(N64Image)
# int n64texconv_image_to_png(const char *outpath, struct n64_image *img, bool intensity_alpha);
ln64texconv.n64texconv_image_to_png.argtypes = [c_char_p, POINTER(N64Image), c_bool]
ln64texconv.n64texconv_image_to_png.restype = c_int
# void *n64texconv_image_to_bin(struct n64_image *img, bool pad_to_8b, bool preswap);
ln64texconv.n64texconv_image_to_bin.argtypes = [POINTER(N64Image), c_bool, c_bool]
ln64texconv.n64texconv_image_to_bin.restype = c_void_p
# 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);
ln64texconv.n64texconv_image_to_c.argtypes = [POINTER(c_char_p), POINTER(c_size_t), POINTER(N64Image), c_bool, c_bool, c_uint]
ln64texconv.n64texconv_image_to_c.restype = c_int
# int n64texconv_image_to_c_file(const char *out_path, struct n64_image *img, bool pad_to_8b, bool preswap, unsigned int byte_width);
ln64texconv.n64texconv_image_to_c_file.argtypes = [c_char_p, POINTER(N64Image), c_bool, c_bool, c_uint]
ln64texconv.n64texconv_image_to_c_file.restype = c_int
# const char *n64texconv_png_extension(struct n64_image *img);
ln64texconv.n64texconv_png_extension.argtypes = [POINTER(N64Image)]
ln64texconv.n64texconv_png_extension.restype = c_char_p