| # basisu_py/transcoder.py | |
| import numpy as np | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from basisu_py.constants import ( | |
| TranscoderTextureFormat, | |
| ) | |
| import importlib | |
| import ctypes | |
| # --------------------------------------------------------------------------- | |
| # Enum to select backend | |
| # --------------------------------------------------------------------------- | |
| class TranscoderBackend: | |
| NATIVE = "native" | |
| WASM = "wasm" | |
| AUTO = "auto" | |
| # --------------------------------------------------------------------------- | |
| # Wrapper class storing pointer+handle | |
| # --------------------------------------------------------------------------- | |
| @dataclass | |
| class KTX2Handle: | |
| ptr: int | |
| handle: int | |
| # --------------------------------------------------------------------------- | |
| # Main Transcoder class | |
| # --------------------------------------------------------------------------- | |
| class Transcoder: | |
| def __init__(self, backend=TranscoderBackend.AUTO): | |
| self._native = None | |
| self._wasm = None | |
| self.backend_name = None | |
| self.backend = None | |
| use_native = False | |
| # ------------------------------------------------------------------ | |
| # Try native backend first if AUTO or NATIVE | |
| # ------------------------------------------------------------------ | |
| if backend in (TranscoderBackend.AUTO, TranscoderBackend.NATIVE): | |
| try: | |
| native_mod = importlib.import_module("basisu_py.basisu_transcoder_python") | |
| native_mod.init() | |
| self._native = native_mod | |
| self.backend = native_mod | |
| self.backend_name = "NATIVE" | |
| use_native = True | |
| print("[Transcoder] Using native backend") | |
| except Exception as e: | |
| if backend == TranscoderBackend.NATIVE: | |
| # Caller explicitly requested native - fail hard | |
| raise RuntimeError(f"Native transcoder backend failed: {e}") | |
| print("[Transcoder] Native backend unavailable, reason:", e) | |
| self._native = None | |
| # ------------------------------------------------------------------ | |
| # Fallback to WASM if native is not being used | |
| # ------------------------------------------------------------------ | |
| if not use_native: | |
| try: | |
| from basisu_py.wasm.wasm_transcoder import BasisuWasmTranscoder | |
| except Exception as e: | |
| raise RuntimeError( | |
| f"WASM backend cannot be imported: {e}\n" | |
| "Ensure that:\n" | |
| " - 'wasmtime' is installed\n" | |
| " - basisu_py/wasm/*.wasm files are present in the install\n" | |
| ) | |
| wasm_path = Path(__file__).parent / "wasm" / "basisu_transcoder_module_st.wasm" | |
| self._wasm = BasisuWasmTranscoder(str(wasm_path)) | |
| self._wasm.load() | |
| self.backend = self._wasm | |
| self.backend_name = "WASM" | |
| print("[Transcoder] Using WASM backend") | |
| # Finally, bind the unified API to whichever backend we chose | |
| self._bind_backend(self.backend) | |
| # ----------------------------------------------------------------------- | |
| # Unified backend binding (native or wasm) | |
| # ----------------------------------------------------------------------- | |
| def _bind_backend(self, b): | |
| self.backend = b | |
| # ------------------ memory operations ------------------ | |
| memory_mapping = [ | |
| ("_alloc", "alloc"), | |
| ("_free", "free"), | |
| ("_write", "write_memory"), | |
| ("_read", "read_memory"), | |
| ] | |
| # ------------------ KTX2 core ------------------ | |
| basis_mapping = [ | |
| # basis_tex_format helpers | |
| ("basis_tex_format_is_xuastc_ldr", "basis_tex_format_is_xuastc_ldr"), | |
| ("basis_tex_format_is_astc_ldr", "basis_tex_format_is_astc_ldr"), | |
| ("basis_tex_format_is_xubc7", "basis_tex_format_is_xubc7"), | |
| ("basis_tex_format_get_block_width", "basis_tex_format_get_block_width"), | |
| ("basis_tex_format_get_block_height", "basis_tex_format_get_block_height"), | |
| ("basis_tex_format_is_hdr", "basis_tex_format_is_hdr"), | |
| ("basis_tex_format_is_ldr", "basis_tex_format_is_ldr"), | |
| # transcoder_texture_format helpers | |
| ("basis_get_bytes_per_block_or_pixel", "basis_get_bytes_per_block_or_pixel"), | |
| ("basis_transcoder_format_has_alpha", "basis_transcoder_format_has_alpha"), | |
| ("basis_transcoder_format_is_hdr", "basis_transcoder_format_is_hdr"), | |
| ("basis_transcoder_format_is_ldr", "basis_transcoder_format_is_ldr"), | |
| ("basis_transcoder_texture_format_is_astc", "basis_transcoder_texture_format_is_astc"), | |
| ("basis_transcoder_format_is_uncompressed", "basis_transcoder_format_is_uncompressed"), | |
| ("basis_get_uncompressed_bytes_per_pixel", "basis_get_uncompressed_bytes_per_pixel"), | |
| ("basis_get_block_width", "basis_get_block_width"), | |
| ("basis_get_block_height", "basis_get_block_height"), | |
| ("basis_get_transcoder_texture_format_from_basis_tex_format","basis_get_transcoder_texture_format_from_basis_tex_format"), | |
| ("basis_is_format_supported", "basis_is_format_supported"), | |
| ("basis_compute_transcoded_image_size_in_bytes","basis_compute_transcoded_image_size_in_bytes"), | |
| ] | |
| ktx2_mapping = [ | |
| ("ktx2_open", "ktx2_open"), | |
| ("ktx2_close", "ktx2_close"), | |
| ("ktx2_get_width", "ktx2_get_width"), | |
| ("ktx2_get_height", "ktx2_get_height"), | |
| ("ktx2_get_levels", "ktx2_get_levels"), | |
| ("ktx2_get_faces", "ktx2_get_faces"), | |
| ("ktx2_get_layers", "ktx2_get_layers"), | |
| ("ktx2_get_basis_tex_format", "ktx2_get_basis_tex_format"), | |
| ("ktx2_get_block_width", "ktx2_get_block_width"), | |
| ("ktx2_get_block_height", "ktx2_get_block_height"), | |
| ("ktx2_get_deblocking_filter_index", "ktx2_get_deblocking_filter_index"), | |
| ("ktx2_has_alpha", "ktx2_has_alpha"), | |
| # flags | |
| ("ktx2_is_hdr", "ktx2_is_hdr"), | |
| ("ktx2_is_hdr_4x4", "ktx2_is_hdr_4x4"), | |
| ("ktx2_is_hdr_6x6", "ktx2_is_hdr_6x6"), | |
| ("ktx2_is_ldr", "ktx2_is_ldr"), | |
| ("ktx2_is_srgb", "ktx2_is_srgb"), | |
| ("ktx2_is_etc1s", "ktx2_is_etc1s"), | |
| ("ktx2_is_uastc_ldr_4x4", "ktx2_is_uastc_ldr_4x4"), | |
| ("ktx2_is_xuastc_ldr", "ktx2_is_xuastc_ldr"), | |
| ("ktx2_is_astc_ldr", "ktx2_is_astc_ldr"), | |
| ("ktx2_is_xubc7", "ktx2_is_xubc7"), | |
| ("ktx2_is_video", "ktx2_is_video"), | |
| ("ktx2_get_ldr_hdr_upconversion_nit_multiplier", "ktx2_get_ldr_hdr_upconversion_nit_multiplier"), | |
| # DFD access | |
| ("ktx2_get_dfd_flags", "ktx2_get_dfd_flags"), | |
| ("ktx2_get_dfd_total_samples", "ktx2_get_dfd_total_samples"), | |
| ("ktx2_get_dfd_channel_id0", "ktx2_get_dfd_channel_id0"), | |
| ("ktx2_get_dfd_channel_id1", "ktx2_get_dfd_channel_id1"), | |
| ("ktx2_get_dfd_color_model", "ktx2_get_dfd_color_model"), | |
| ("ktx2_get_dfd_color_primaries", "ktx2_get_dfd_color_primaries"), | |
| ("ktx2_get_dfd_transfer_func", "ktx2_get_dfd_transfer_func"), | |
| # per-level info | |
| ("ktx2_get_level_orig_width", "ktx2_get_level_orig_width"), | |
| ("ktx2_get_level_orig_height", "ktx2_get_level_orig_height"), | |
| ("ktx2_get_level_actual_width", "ktx2_get_level_actual_width"), | |
| ("ktx2_get_level_actual_height", "ktx2_get_level_actual_height"), | |
| ("ktx2_get_level_num_blocks_x", "ktx2_get_level_num_blocks_x"), | |
| ("ktx2_get_level_num_blocks_y", "ktx2_get_level_num_blocks_y"), | |
| ("ktx2_get_level_total_blocks", "ktx2_get_level_total_blocks"), | |
| ("ktx2_get_level_alpha_flag", "ktx2_get_level_alpha_flag"), | |
| ("ktx2_get_level_iframe_flag", "ktx2_get_level_iframe_flag"), | |
| # transcoding | |
| ("ktx2_start_transcoding", "ktx2_start_transcoding"), | |
| ("ktx2_transcode_image_level", "ktx2_transcode_image_level"), | |
| # version | |
| ("get_version_fn", "get_version"), | |
| ] | |
| # Apply all mappings | |
| for public_name, backend_name in (memory_mapping + ktx2_mapping + basis_mapping): | |
| setattr(self, public_name, getattr(b, backend_name)) | |
| # ----------------------------------------------------------------------- | |
| # Public version query | |
| # ----------------------------------------------------------------------- | |
| def get_version(self): | |
| return self.get_version_fn() | |
| # ----------------------------------------------------------------------- | |
| # Enable library debug printing to stdout (also set BASISU_FORCE_DEVEL_MESSAGES to 1 in transcoder/basisu.h) | |
| # ----------------------------------------------------------------------- | |
| def enable_debug_printf(self, flag: bool = True): | |
| return self.backend.enable_debug_printf(flag) | |
| # ----------------------------------------------------------------------- | |
| # KTX2 Handle API: open/close + all queries | |
| # ----------------------------------------------------------------------- | |
| def open(self, ktx2_bytes: bytes) -> KTX2Handle: | |
| ptr = self._alloc(len(ktx2_bytes)) | |
| self._write(ptr, ktx2_bytes) | |
| handle = self.ktx2_open(ptr, len(ktx2_bytes)) | |
| return KTX2Handle(ptr, handle) | |
| def close(self, ktx2_handle: KTX2Handle): | |
| self.ktx2_close(ktx2_handle.handle) | |
| self._free(ktx2_handle.ptr) | |
| # ---- Basic queries ---- | |
| def get_width(self, ktx2_handle: KTX2Handle): | |
| return self.ktx2_get_width(ktx2_handle.handle) | |
| def get_height(self, ktx2_handle: KTX2Handle): | |
| return self.ktx2_get_height(ktx2_handle.handle) | |
| def get_levels(self, ktx2_handle: KTX2Handle): | |
| return self.ktx2_get_levels(ktx2_handle.handle) | |
| def get_faces(self, ktx2_handle: KTX2Handle): | |
| return self.ktx2_get_faces(ktx2_handle.handle) | |
| def get_layers(self, ktx2_handle: KTX2Handle): | |
| return self.ktx2_get_layers(ktx2_handle.handle) | |
| def get_basis_tex_format(self, ktx2_handle: KTX2Handle): | |
| return self.ktx2_get_basis_tex_format(ktx2_handle.handle) | |
| def has_alpha(self, ktx2_handle: KTX2Handle) -> bool: | |
| """ | |
| Return true if the KTX2 container has alpha. | |
| """ | |
| return bool(self.ktx2_has_alpha(ktx2_handle.handle)) | |
| # ---- Format flags ---- | |
| def is_hdr(self, ktx2_handle): return bool(self.ktx2_is_hdr(ktx2_handle.handle)) | |
| def is_hdr_4x4(self, ktx2_handle): return bool(self.ktx2_is_hdr_4x4(ktx2_handle.handle)) | |
| def is_hdr_6x6(self, ktx2_handle): return bool(self.ktx2_is_hdr_6x6(ktx2_handle.handle)) | |
| def is_ldr(self, ktx2_handle): return bool(self.ktx2_is_ldr(ktx2_handle.handle)) | |
| def is_srgb(self, ktx2_handle): return bool(self.ktx2_is_srgb(ktx2_handle.handle)) | |
| def is_video(self, ktx2_handle): return bool(self.ktx2_is_video(ktx2_handle.handle)) | |
| def get_ldr_hdr_upconversion_nit_multiplier(self, ktx2_handle): return self.ktx2_get_ldr_hdr_upconversion_nit_multiplier(ktx2_handle.handle) | |
| def is_etc1s(self, ktx2_handle): return bool(self.ktx2_is_etc1s(ktx2_handle.handle)) | |
| def is_uastc_ldr_4x4(self, ktx2_handle): return bool(self.ktx2_is_uastc_ldr_4x4(ktx2_handle.handle)) | |
| def is_xuastc_ldr(self, ktx2_handle): return bool(self.ktx2_is_xuastc_ldr(ktx2_handle.handle)) | |
| def is_astc_ldr(self, ktx2_handle): return bool(self.ktx2_is_astc_ldr(ktx2_handle.handle)) | |
| def is_xubc7(self, ktx2_handle): return bool(self.ktx2_is_xubc7(ktx2_handle.handle)) | |
| # ---- DFD access | |
| def get_dfd_flags(self, ktx2_handle): return self.ktx2_get_dfd_flags(ktx2_handle.handle) | |
| def get_dfd_total_samples(self, ktx2_handle): return self.ktx2_get_dfd_total_samples(ktx2_handle.handle) | |
| def get_dfd_color_model(self, ktx2_handle): return self.ktx2_get_dfd_color_model(ktx2_handle.handle) | |
| def get_dfd_color_primaries(self, ktx2_handle): return self.ktx2_get_dfd_color_primaries(ktx2_handle.handle) | |
| def get_dfd_transfer_func(self, ktx2_handle): return self.ktx2_get_dfd_transfer_func(ktx2_handle.handle) | |
| def get_dfd_channel_id0(self, ktx2_handle): return self.ktx2_get_dfd_channel_id0(ktx2_handle.handle) | |
| def get_dfd_channel_id1(self, ktx2_handle): return self.ktx2_get_dfd_channel_id1(ktx2_handle.handle) | |
| # ---- Block dimensions ---- | |
| def get_block_width(self, ktx2_handle): return self.ktx2_get_block_width(ktx2_handle.handle) | |
| def get_block_height(self, ktx2_handle): return self.ktx2_get_block_height(ktx2_handle.handle) | |
| def get_deblocking_filter_index(self, ktx2_handle): return self.ktx2_get_deblocking_filter_index(ktx2_handle.handle) | |
| # ----------------------------------------------------------------------- | |
| # Explicit: start transcoding on an already-open KTX2 file | |
| # ----------------------------------------------------------------------- | |
| def start_transcoding(self, ktx2_handle: KTX2Handle): | |
| """ | |
| Must be called before per-level iframe flags become valid. | |
| """ | |
| ok = self.ktx2_start_transcoding(ktx2_handle.handle) | |
| if not ok: | |
| raise RuntimeError("start_transcoding() failed") | |
| return True | |
| # ---- Level info ---- | |
| def get_level_orig_width(self, ktx2_handle, level, layer=0, face=0): | |
| return self.ktx2_get_level_orig_width(ktx2_handle.handle, level, layer, face) | |
| def get_level_orig_height(self, ktx2_handle, level, layer=0, face=0): | |
| return self.ktx2_get_level_orig_height(ktx2_handle.handle, level, layer, face) | |
| def get_level_actual_width(self, ktx2_handle, level, layer=0, face=0): | |
| return self.ktx2_get_level_actual_width(ktx2_handle.handle, level, layer, face) | |
| def get_level_actual_height(self, ktx2_handle, level, layer=0, face=0): | |
| return self.ktx2_get_level_actual_height(ktx2_handle.handle, level, layer, face) | |
| def get_level_num_blocks_x(self, ktx2_handle, level, layer=0, face=0): | |
| return self.ktx2_get_level_num_blocks_x(ktx2_handle.handle, level, layer, face) | |
| def get_level_num_blocks_y(self, ktx2_handle, level, layer=0, face=0): | |
| return self.ktx2_get_level_num_blocks_y(ktx2_handle.handle, level, layer, face) | |
| def get_level_total_blocks(self, ktx2_handle, level, layer=0, face=0): | |
| return self.ktx2_get_level_total_blocks(ktx2_handle.handle, level, layer, face) | |
| def get_level_alpha_flag(self, ktx2_handle, level, layer=0, face=0): | |
| return bool(self.ktx2_get_level_alpha_flag(ktx2_handle.handle, level, layer, face)) | |
| def get_level_iframe_flag(self, ktx2_handle, level, layer=0, face=0): | |
| return bool(self.ktx2_get_level_iframe_flag(ktx2_handle.handle, level, layer, face)) | |
| # ----------------------------------------------------------------------- | |
| # Low-level: Decode RGBA8 from an already-open KTX2 handle | |
| # ----------------------------------------------------------------------- | |
| def decode_rgba_handle(self, ktx2_handle: KTX2Handle, level=0, layer=0, face=0): | |
| """ | |
| Low-level fast decode. Requires an already-open KTX2Handle. | |
| Returns HxWx4 uint8 NumPy array. | |
| """ | |
| w = self.ktx2_get_level_orig_width(ktx2_handle.handle, level, layer, face) | |
| h = self.ktx2_get_level_orig_height(ktx2_handle.handle, level, layer, face) | |
| out_size = w * h * 4 | |
| out_ptr = self._alloc(out_size) | |
| # MUST start transcoding before ANY decode | |
| ok = self.ktx2_start_transcoding(ktx2_handle.handle) | |
| if not ok: | |
| self._free(out_ptr) | |
| raise RuntimeError("start_transcoding failed") | |
| ok = self.ktx2_transcode_image_level( | |
| ktx2_handle.handle, | |
| level, layer, face, | |
| out_ptr, | |
| out_size // 4, | |
| TranscoderTextureFormat.TF_RGBA32, | |
| 0, 0, 0, -1, -1, 0 | |
| ) | |
| if not ok: | |
| self._free(out_ptr) | |
| raise RuntimeError("transcode_image_level failed") | |
| raw_bytes = self._read(out_ptr, out_size) | |
| self._free(out_ptr) | |
| arr = np.frombuffer(raw_bytes, dtype=np.uint8) | |
| return arr.reshape((h, w, 4)) | |
| # ----------------------------------------------------------------------- | |
| # High-level: Decode RGBA8 directly from KTX2 file data | |
| # ----------------------------------------------------------------------- | |
| def decode_rgba(self, ktx2_bytes: bytes, level=0, layer=0, face=0): | |
| """ | |
| High-level convenience decode. Opens the KTX2 file bytes for you. | |
| """ | |
| ktx2_handle = self.open(ktx2_bytes) | |
| try: | |
| return self.decode_rgba_handle(ktx2_handle, level, layer, face) | |
| finally: | |
| self.close(ktx2_handle) | |
| # ----------------------------------------------------------------------- | |
| # Low-level: Decode HDR (RGBA float32) from open KTX2 | |
| # ----------------------------------------------------------------------- | |
| def decode_rgba_hdr_handle(self, ktx2_handle: KTX2Handle, level=0, layer=0, face=0): | |
| """ | |
| Low-level HDR decode. Returns HxWx4 float32 NumPy array. | |
| """ | |
| w = self.ktx2_get_level_orig_width(ktx2_handle.handle, level, layer, face) | |
| h = self.ktx2_get_level_orig_height(ktx2_handle.handle, level, layer, face) | |
| bytes_per_pixel = 8 # 4 * half-float | |
| out_size = w * h * bytes_per_pixel | |
| out_ptr = self._alloc(out_size) | |
| ok = self.ktx2_start_transcoding(ktx2_handle.handle) | |
| if not ok: | |
| self._free(out_ptr) | |
| raise RuntimeError("start_transcoding failed") | |
| ok = self.ktx2_transcode_image_level( | |
| ktx2_handle.handle, | |
| level, layer, face, | |
| out_ptr, | |
| out_size // bytes_per_pixel, | |
| TranscoderTextureFormat.TF_RGBA_HALF, | |
| 0, 0, 0, -1, -1, 0 | |
| ) | |
| if not ok: | |
| self._free(out_ptr) | |
| raise RuntimeError("transcode_image_level failed") | |
| raw_bytes = self._read(out_ptr, out_size) | |
| self._free(out_ptr) | |
| arr = np.frombuffer(raw_bytes, dtype=np.float16).astype(np.float32) | |
| return arr.reshape((h, w, 4)) | |
| # ----------------------------------------------------------------------- | |
| # High-level: Decode HDR (RGBA float32) from KTX2 file data | |
| # ----------------------------------------------------------------------- | |
| def decode_rgba_hdr(self, ktx2_bytes: bytes, level=0, layer=0, face=0): | |
| """ | |
| High-level convenience HDR decode. Opens the KTX2 file bytes for you. | |
| """ | |
| ktx2_handle = self.open(ktx2_bytes) | |
| try: | |
| return self.decode_rgba_hdr_handle(ktx2_handle, level, layer, face) | |
| finally: | |
| self.close(ktx2_handle) | |
| # ----------------------------------------------------------------------- | |
| # Low-level: General-purpose transcode using a chosen TranscoderTextureFormat format | |
| # ----------------------------------------------------------------------- | |
| def transcode_tfmt_handle(self, ktx2_handle: KTX2Handle, tfmt: int, | |
| level=0, layer=0, face=0, decode_flags=0, | |
| channel0=-1, channel1=-1): | |
| """ | |
| Low-level direct transcoding from an already-open KTX2 handle. | |
| Parameters: | |
| ktx2_handle: KTX2Handle -> already-open KTX2 | |
| tfmt: int -> TranscoderTextureFormat to transcode to (for ASTC: block size and LDR/HDR MUST match the KTX2 file, for HDR: must be a HDR texture format) | |
| level/layer/face: int -> which image slice to decode | |
| decode_flags: int -> basist::decode_flags | |
| row_pitch, rows_in_pixels, channel0, channel1 -> advanced options | |
| Returns: bytes (transcoded GPU texture data or uncompressed image) | |
| """ | |
| # Determine actual output size in bytes | |
| ow = self.ktx2_get_level_orig_width(ktx2_handle.handle, level, layer, face) | |
| oh = self.ktx2_get_level_orig_height(ktx2_handle.handle, level, layer, face) | |
| out_size = self.basis_compute_transcoded_image_size_in_bytes(tfmt, ow, oh) | |
| if out_size == 0: | |
| raise RuntimeError("basis_compute_transcoded_image_size_in_bytes returned 0") | |
| # print(f"*** ow={ow}, oh={oh}, out_size={out_size}") | |
| out_ptr = self._alloc(out_size) | |
| # Call transcoder | |
| ok = self.ktx2_start_transcoding(ktx2_handle.handle) | |
| if not ok: | |
| self._free(out_ptr) | |
| raise RuntimeError("start_transcoding failed") | |
| ok = self.ktx2_transcode_image_level( | |
| ktx2_handle.handle, | |
| level, layer, face, | |
| out_ptr, | |
| out_size // self.basis_get_bytes_per_block_or_pixel(tfmt), | |
| tfmt, | |
| decode_flags, | |
| 0, | |
| 0, | |
| channel0, channel1, | |
| 0 # no per-thread state object | |
| ) | |
| if not ok: | |
| self._free(out_ptr) | |
| raise RuntimeError("ktx2_transcode_image_level failed") | |
| # Extract bytes | |
| raw_bytes = self._read(out_ptr, out_size) | |
| self._free(out_ptr) | |
| return raw_bytes | |
| # ----------------------------------------------------------------------- | |
| # High-level: General-purpose transcode (opens the KTX2 for you) | |
| # tfmt: the TranscoderTextureFormat to transcode too | |
| # ----------------------------------------------------------------------- | |
| def transcode_tfmt(self, ktx2_bytes: bytes, tfmt: int, | |
| level=0, layer=0, face=0, decode_flags=0, | |
| channel0=-1, channel1=-1): | |
| """ | |
| High-level convenience wrapper for transcode_tfmt_handle(). | |
| Automatically opens/closes the KTX2 file. | |
| """ | |
| ktx2_handle = self.open(ktx2_bytes) | |
| try: | |
| return self.transcode_tfmt_handle( | |
| ktx2_handle, tfmt, | |
| level=level, | |
| layer=layer, | |
| face=face, | |
| decode_flags=decode_flags, | |
| channel0=channel0, | |
| channel1=channel1 | |
| ) | |
| finally: | |
| self.close(ktx2_handle) | |
| # ----------------------------------------------------------------------- | |
| # Low-level: choose a specific transcoder_texture_format from a family string | |
| # ----------------------------------------------------------------------- | |
| def choose_transcoder_format(self, ktx2_handle: KTX2Handle, family: str) -> int: | |
| """ | |
| Given an already-opened KTX2 and a desired family string, choose a concrete | |
| TranscoderTextureFormat enum. | |
| family: one of: | |
| "ASTC", "BC1", "BC3", "BC4", "BC5", "BC6H", "BC7", | |
| "PVRTC1", "PVRTC2", | |
| "ETC1", "ETC2", "ETC2_EAC_R11", "ETC2_EAC_RG11", | |
| "ATC", "FXT1", | |
| "RGBA32", "RGB_HALF", "RGBA_HALF", | |
| "RGB_9E5" | |
| Returns: | |
| int: TranscoderTextureFormat value | |
| """ | |
| s = family.strip().upper().replace(" ", "") | |
| hdr_tex = self.is_hdr(ktx2_handle) | |
| has_alpha = self.has_alpha(ktx2_handle) | |
| basis_fmt = self.get_basis_tex_format(ktx2_handle) | |
| tfmt = None | |
| # ------------------------------------------------------------------- | |
| # Uncompressed families | |
| # ------------------------------------------------------------------- | |
| if s in ("RGBA32", "RGBA8", "UNCOMPRESSED"): | |
| tfmt = TranscoderTextureFormat.TF_RGBA32 | |
| elif s in ("RGBHALF", "RGB16F"): | |
| tfmt = TranscoderTextureFormat.TF_RGB_HALF | |
| elif s in ("RGBAHALF", "RGBA16F"): | |
| tfmt = TranscoderTextureFormat.TF_RGBA_HALF | |
| elif s in ("RGB9E5", "RGB_9E5"): | |
| tfmt = TranscoderTextureFormat.TF_RGB_9E5 | |
| # ------------------------------------------------------------------- | |
| # BC families | |
| # ------------------------------------------------------------------- | |
| elif s == "BC1": | |
| tfmt = TranscoderTextureFormat.TF_BC1_RGB | |
| elif s == "BC3": | |
| tfmt = TranscoderTextureFormat.TF_BC3_RGBA | |
| elif s == "BC4": | |
| tfmt = TranscoderTextureFormat.TF_BC4_R | |
| elif s == "BC5": | |
| tfmt = TranscoderTextureFormat.TF_BC5_RG | |
| elif s == "BC6H": | |
| tfmt = TranscoderTextureFormat.TF_BC6H | |
| elif s == "BC7": | |
| tfmt = TranscoderTextureFormat.TF_BC7_RGBA | |
| # ------------------------------------------------------------------- | |
| # PVRTC families | |
| # ------------------------------------------------------------------- | |
| elif s == "PVRTC1": | |
| tfmt = (TranscoderTextureFormat.TF_PVRTC1_4_RGBA | |
| if has_alpha else TranscoderTextureFormat.TF_PVRTC1_4_RGB) | |
| elif s == "PVRTC2": | |
| tfmt = (TranscoderTextureFormat.TF_PVRTC2_4_RGBA | |
| if has_alpha else TranscoderTextureFormat.TF_PVRTC2_4_RGB) | |
| # ------------------------------------------------------------------- | |
| # ETC / EAC families | |
| # ------------------------------------------------------------------- | |
| elif s == "ETC1": | |
| tfmt = TranscoderTextureFormat.TF_ETC1_RGB | |
| elif s == "ETC2": | |
| tfmt = TranscoderTextureFormat.TF_ETC2_RGBA | |
| elif s in ("ETC2_EAC_R11", "EAC_R11"): | |
| tfmt = TranscoderTextureFormat.TF_ETC2_EAC_R11 | |
| elif s in ("ETC2_EAC_RG11", "EAC_RG11"): | |
| tfmt = TranscoderTextureFormat.TF_ETC2_EAC_RG11 | |
| # ------------------------------------------------------------------- | |
| # ATC / FXT | |
| # ------------------------------------------------------------------- | |
| elif s == "ATC": | |
| tfmt = (TranscoderTextureFormat.TF_ATC_RGBA | |
| if has_alpha else TranscoderTextureFormat.TF_ATC_RGB) | |
| elif s == "FXT1": | |
| tfmt = TranscoderTextureFormat.TF_FXT1_RGB | |
| # ------------------------------------------------------------------- | |
| # ASTC family | |
| # ------------------------------------------------------------------- | |
| elif s == "ASTC": | |
| # Let BasisU decide correct ASTC format (block size + LDR/HDR) | |
| tfmt = self.basis_get_transcoder_texture_format_from_basis_tex_format(basis_fmt) | |
| else: | |
| # Unknown family: choose a safe uncompressed default | |
| if hdr_tex: | |
| tfmt = TranscoderTextureFormat.TF_RGBA_HALF | |
| else: | |
| tfmt = TranscoderTextureFormat.TF_RGBA32 | |
| # ------------------------------------------------------------------- | |
| # Validate HDR/LDR compatibility (optional but recommended) | |
| # ------------------------------------------------------------------- | |
| # Use helpers to ensure we don't do HDR->LDR or LDR->HDR accidentally. | |
| is_tfmt_hdr = self.basis_transcoder_format_is_hdr(tfmt) | |
| if hdr_tex and not is_tfmt_hdr: | |
| raise ValueError(f"Requested {family} (LDR transcoder format) for HDR KTX2.") | |
| if not hdr_tex and is_tfmt_hdr: | |
| raise ValueError(f"Requested {family} (HDR transcoder format) for LDR KTX2.") | |
| return tfmt | |
| # ----------------------------------------------------------------------- | |
| # Low-level: General-purpose transcode using a family string | |
| # from an already opened ktx2 file. | |
| # Returns: | |
| # (data_bytes, chosen_tfmt, block_width, block_height) | |
| # ----------------------------------------------------------------------- | |
| def transcode_handle( | |
| self, | |
| ktx2_handle: KTX2Handle, | |
| family: str, | |
| level=0, | |
| layer=0, | |
| face=0, | |
| decode_flags=0, | |
| channel0=-1, | |
| channel1=-1 | |
| ): | |
| """ | |
| Low-level direct transcoding from an already-open KTX2 handle, | |
| using a high-level family string such as: | |
| "BC7", "BC3", "BC1", "ETC1", "ETC2", "ASTC", "PVRTC1", | |
| "RGBA32", "RGB_HALF", "RGBA_HALF", "RGB_9E5", etc. | |
| See choose_transcoder_format(). | |
| Returns: | |
| (data_bytes, tfmt, block_width, block_height) | |
| """ | |
| # Decide the exact transcoder format (BC1/BC7/etc.) | |
| tfmt = self.choose_transcoder_format(ktx2_handle, family) | |
| # Get original dims of the requested slice | |
| ow = self.get_level_orig_width(ktx2_handle, level, layer, face) | |
| oh = self.get_level_orig_height(ktx2_handle, level, layer, face) | |
| # Compute correct output size for the chosen format | |
| out_size = self.basis_compute_transcoded_image_size_in_bytes(tfmt, ow, oh) | |
| if out_size == 0: | |
| raise RuntimeError( | |
| f"Computed output size is 0 for tfmt={tfmt}, dims={ow}x{oh}" | |
| ) | |
| # Allocate output buffer | |
| out_ptr = self._alloc(out_size) | |
| # Ensure transcoding tables are ready | |
| ok = self.ktx2_start_transcoding(ktx2_handle.handle) | |
| if not ok: | |
| self._free(out_ptr) | |
| raise RuntimeError("start_transcoding failed") | |
| # Perform the transcode | |
| ok = self.ktx2_transcode_image_level( | |
| ktx2_handle.handle, | |
| level, layer, face, | |
| out_ptr, | |
| out_size // self.basis_get_bytes_per_block_or_pixel(tfmt), | |
| tfmt, | |
| decode_flags, | |
| 0, # row_pitch_in_blocks_or_pixels | |
| 0, # rows_in_pixels | |
| channel0, | |
| channel1, | |
| 0 # no thread-local state | |
| ) | |
| if not ok: | |
| self._free(out_ptr) | |
| raise RuntimeError("ktx2_transcode_image_level failed") | |
| # Extract bytes from native/WASM memory | |
| data_bytes = self._read(out_ptr, out_size) | |
| # Free the output buffer | |
| self._free(out_ptr) | |
| # Determine block dims for this texture format | |
| if self.basis_transcoder_format_is_uncompressed(tfmt): | |
| bw = None | |
| bh = None | |
| else: | |
| bw = self.basis_get_block_width(tfmt) | |
| bh = self.basis_get_block_height(tfmt) | |
| return data_bytes, tfmt, bw, bh | |
| # ----------------------------------------------------------------------- | |
| # High-level: one-shot transcode using a family string | |
| # directly from ktx2 file data. (Slower if you're transcoding multiple | |
| # levels/faces/layers.) | |
| # ----------------------------------------------------------------------- | |
| def transcode( | |
| self, | |
| ktx2_bytes: bytes, | |
| family: str, | |
| level=0, | |
| layer=0, | |
| face=0, | |
| decode_flags=0, | |
| channel0=-1, | |
| channel1=-1 | |
| ): | |
| """ | |
| High-level version of transcode_handle(). | |
| Calls transcode_handle() internally. | |
| Returns: | |
| (data_bytes, tfmt, block_width, block_height) | |
| """ | |
| ktx2_handle = self.open(ktx2_bytes) | |
| try: | |
| return self.transcode_handle( | |
| ktx2_handle, | |
| family, | |
| level=level, | |
| layer=layer, | |
| face=face, | |
| decode_flags=decode_flags, | |
| channel0=channel0, | |
| channel1=channel1 | |
| ) | |
| finally: | |
| self.close(ktx2_handle) | |
| def tfmt_name(self, tfmt: int): | |
| return TranscoderTextureFormat(tfmt).name |