| # 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_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_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_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)) |
| |
| # ---- 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) |
| |
| # ----------------------------------------------------------------------- |
| # 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, |
| 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, |
| 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, |
| 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_FLOAT", "RGBA_FLOAT", |
| "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", "RGB_FLOAT", "RGBFLOAT"): |
| tfmt = TranscoderTextureFormat.TF_RGB_HALF |
| |
| elif s in ("RGBAHALF", "RGBA16F", "RGBA_FLOAT", "RGBAFLOAT"): |
| 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, |
| 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 |