adding new files

This commit is contained in:
Richard Geldreich
2026-01-19 01:59:35 -05:00
parent 9d87991078
commit ea6778b2b5
72 changed files with 31686 additions and 0 deletions

59
python/README.md Normal file
View File

@@ -0,0 +1,59 @@
Python support is still new and coming online, but is entirely functional.
The library's pure C (WASM friendly) API's are completely exposed to Python.
The Python integration first tries to use native .so's in the basisu_py
directory. If they don't exist, it tries the slower and single threaded WASM
fallbacks under basisu_py/wasm, which requires wasmtime for Python to be
installed. Some tests require an input.ktx2 or test.ktx2 to be in the current
directory.
Building:
Under the repo's root directory - build the native SO's:
```
mkdir build_python
cd build_python
cmake -DBASISU_BUILD_PYTHON=ON ..
make
```
Build the WASM modules (see README_WASI.md file for instructions on how to
install the WASI SDK, which is required):
```
mkdir build_wasm_st
cd build_wasm_st
cmake .. -DCMAKE_TOOLCHAIN_FILE=$WASI_SDK_PATH/share/cmake/wasi-sdk.cmake -DCMAKE_BUILD_TYPE=Release -DBASISU_WASM_THREADING=OFF
make
```
Running tests:
The tests assume the current directory is "python". Run them like this:
Higher-level tests:
python3 -m tests.test_backend_loading
python3 -m tests.test_basic_wasm_selection
python3 -m tests.test_basic_backend_selection
python3 -m tests.test_basic_decode
python3 -m tests.test_basic_transcode
python3 -m tests.test_compress_swirl
python3 -m tests.test_compress_swirl_hdr
python3 -m tests.test_transcoder_astc
python3 -m tests.test_transcoder_backend_loading
python3 -m tests.test_transcoder_end_to_end
python3 -m tests.test_transcoder_end_to_end_hdr
python3 -m tests.test_transcoder_helpers
Low-level tests (used while bringing up the codec):
python3 -m lowlevel_test_native.basic_test
python3 -m lowlevel_test_native.test_transcoder_basic
python3 -m lowlevel_test_native.example_capi_python
python3 -m lowlevel_test_wasm.basic_test
python3 -m lowlevel_test_wasm.compress_test
python3 -m lowlevel_test_wasm.compress_test_float

85
python/README_win.md Normal file
View File

@@ -0,0 +1,85 @@
Windows Native Python Build Instructions
========================================
This project uses pybind11 to build Python .pyd extension modules on Windows.
Because Windows installs multiple Python versions, and pybind11 currently only
supports up to Python 3.12, you must follow these steps exactly.
Requirements
------------
- Visual Studio Developer Command Prompt (VS C++ Build Tools installed)
- Python 3.12 (pybind11 does NOT support 3.13+ at the time of writing)
- pybind11 installed into Python 3.12
Check installed Python versions:
py -0
If Python 3.12 is missing:
winget install Python.Python.3.12
Install pybind11 for Python 3.12:
py -3.12 -m pip install pybind11
IMPORTANT:
You must build AND run with the same Python interpreter version (3.12).
Building the .pyd Modules
-------------------------
Open the "Developer Command Prompt for Visual Studio".
From the project root:
mkdir build_python_win
cd build_python_win
Run CMake using the exact path to python.exe for Python 3.12:
cmake -G "Visual Studio 17 2022" -A x64 -DBASISU_BUILD_PYTHON=ON -DBASISU_BUILD_WASM=OFF -DPYTHON_EXECUTABLE="C:\Users\<YOU>\AppData\Local\Programs\Python\Python312\python.exe" ..
Build:
cmake --build . --config Release
Output files will be created in:
python/basisu_py/basisu_python.pyd
python/basisu_py/basisu_transcoder_python.pyd
Running the Modules
-------------------
Always run using Python 3.12:
py -3.12
Inside Python:
import basisu_py
print("Modules loaded OK.")
While in the "python" directory:
py -m tests.test_backend_loading
WASM Backend (Optional)
-----------------------
Install wasmtime:
py -3.12 -m pip install wasmtime
Ensure these files exist:
python/basisu_py/wasm/*.wasm
Common Problems
---------------
1. "pybind11 not found"
-> Installed into wrong Python version. Use:
py -3.12 -m pip install pybind11
2. "Python config failure"
-> You are using Python 3.13 or 3.14. Must use Python 3.12.
3. Modules not loading
-> You must run them with the same interpreter used to build them:
py -3.12

83
python/astc_writer.py Normal file
View File

@@ -0,0 +1,83 @@
# astc_writer.py
#
# Minimal ASTC writer that mirrors the C/C++ write_astc_file() logic from example_capi.c.
# Writes a valid single-slice 2D ASTC texture file (no array slices, no 3D, no mips).
#
# Usage:
# from astc_writer import write_astc_file
# write_astc_file("output.astc", blocks, block_width, block_height, width, height)
#
# "blocks" must be a bytes-like object containing the full ASTC block data
# using 16 bytes per block (standard ASTC block size).
def write_astc_file(
filename: str,
blocks: bytes,
block_width: int,
block_height: int,
width: int,
height: int
) -> None:
"""
Write an ASTC file to disk.
Parameters:
filename : Output filename ("something.astc")
blocks : Bytes-like object containing ASTC blocks (16 bytes per block)
block_width : ASTC block width (e.g. 4-12)
block_height : ASTC block height (e.g. 4-12)
width : Original image width in pixels
height : Original image height in pixels
Notes:
- ASTC files use 2D blocks; depth is always 1.
- Block layout goes row-major: (num_blocks_y num_blocks_x) blocks.
- No mipmaps are stored in this format.
"""
# Validate block dimensions
if block_width < 4 or block_width > 12:
raise ValueError(f"ASTC block_width {block_width} out of range (412)")
if block_height < 4 or block_height > 12:
raise ValueError(f"ASTC block_height {block_height} out of range (412)")
# Compute block grid
num_blocks_x = (width + block_width - 1) // block_width
num_blocks_y = (height + block_height - 1) // block_height
total_blocks = num_blocks_x * num_blocks_y
expected_size = total_blocks * 16 # 16 bytes per ASTC block (always)
if len(blocks) != expected_size:
raise ValueError(
f"ASTC block buffer incorrect size: expected {expected_size}, got {len(blocks)}"
)
# Write file
with open(filename, "wb") as f:
# ASTC magic number (0x13AB A15C)
f.write(bytes([0x13, 0xAB, 0xA1, 0x5C]))
# Block dims: x, y, z (z=1)
f.write(bytes([
block_width & 0xFF,
block_height & 0xFF,
1
]))
# ASTC stores width/height/depth as 24-bit LE
def write_24bit_le(v: int):
f.write(bytes([
v & 0xFF,
(v >> 8) & 0xFF,
(v >> 16) & 0xFF
]))
write_24bit_le(width)
write_24bit_le(height)
write_24bit_le(1) # depth
# Write actual block payload
f.write(blocks)
print(f"[ASTC Writer] Wrote: {filename} ({width}x{height}, {block_width}x{block_height} blocks)")

View File

@@ -0,0 +1,109 @@
// File: basisu_encoder_pybind11.cpp
// pybind11 native bindings for the compressor's pure C API basisu_wasm_api.h
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <stdint.h>
// include the basisu compression plain C API
#include "../encoder/basisu_wasm_api.h"
namespace py = pybind11;
// Convert wasm_bool_t (uint32_t) ? Python bool
static inline bool to_bool(uint32_t v) { return v != 0; }
PYBIND11_MODULE(basisu_python, m) {
m.doc() = "Native Basis Universal encoder (pybind11 binding over basisu_wasm_api)";
//
// Initialization / Version
//
m.def("init", &bu_init, "Initialize the BasisU codec library");
m.def("get_version", &bu_get_version, "Return BASISU_LIB_VERSION");
//
// Memory allocation helpers
//
m.def("alloc", &bu_alloc,
"Allocate memory inside native heap and return pointer as uint64");
m.def("free", &bu_free,
"Free previously allocated pointer");
//
// Compression params handles
//
m.def("new_params", &bu_new_comp_params,
"Create a new comp_params struct inside native heap");
m.def("delete_params",
[](uint64_t h) { return to_bool(bu_delete_comp_params(h)); },
"Destroy a comp_params struct");
m.def("params_clear",
[](uint64_t h) { return to_bool(bu_comp_params_clear(h)); },
"Clear comp_params struct");
//
// Image upload API
//
m.def("set_image_rgba32",
[](uint64_t params, uint32_t index,
uint64_t img_ptr, uint32_t w, uint32_t h, uint32_t pitch) {
return to_bool(bu_comp_params_set_image_rgba32(
params, index, img_ptr, w, h, pitch));
},
"Set 8-bit RGBA32 image into parameters");
m.def("set_image_float_rgba",
[](uint64_t params, uint32_t index,
uint64_t img_ptr, uint32_t w, uint32_t h, uint32_t pitch) {
return to_bool(bu_comp_params_set_image_float_rgba(
params, index, img_ptr, w, h, pitch));
},
"Set float32 RGBA image into parameters");
//
// Compression
//
m.def("compress",
[](uint64_t params,
int tex_format,
int quality,
int effort,
uint64_t flags,
float rdo_quality)
{
return to_bool(bu_compress_texture(
params, tex_format, quality, effort, flags, rdo_quality));
},
py::arg("params"),
py::arg("tex_format"),
py::arg("quality"),
py::arg("effort"),
py::arg("flags"),
py::arg("rdo_quality") = 0.0f
);
//
// Output blob access
//
m.def("get_comp_data_size",
&bu_comp_params_get_comp_data_size,
"Return size (bytes) of compressed output");
m.def("get_comp_data_ofs",
&bu_comp_params_get_comp_data_ofs,
"Return pointer (uint64) to compressed output buffer");
// Memory read/write
m.def("read_memory",
[](uint64_t ptr, uint32_t size) {
return py::bytes((const char*)ptr, size);
},
"Read `size` bytes starting at native memory address `ptr`");
m.def("write_memory",
[](uint64_t dest_ptr, py::buffer src) {
py::buffer_info info = src.request();
memcpy((void*)dest_ptr, info.ptr, info.size * info.itemsize);
},
"Write bytes/buffer-like object into native memory at address `ptr`");
}

View File

@@ -0,0 +1,2 @@
recursive-include basisu_py *.py *.so *.wasm
include README.md

View File

@@ -0,0 +1,5 @@
This is the Python support directory for the Basis Universal KTX2 compressor
and transcoder modules.
License: Apache 2.0

View File

@@ -0,0 +1,35 @@
"""
basisu_py
=========
Python bindings for the Basis Universal encoder and transcoder, with
automatic fallback between native C++ extensions and WASM modules.
Main entry points:
- Transcoder : basisu_py.transcoder.Transcoder
- Encoder : basisu_py.codec.Encoder
- constants : basisu_py.constants
"""
from .codec import Encoder
from .transcoder import Transcoder, KTX2Handle
from .constants import (
BasisTexFormat,
BasisQuality,
BasisEffort,
BasisFlags,
TranscoderTextureFormat,
TranscodeDecodeFlags,
)
# What the package publicly exposes
__all__ = [
"Encoder",
"Transcoder",
"KTX2Handle",
"BasisTexFormat",
"BasisQuality",
"BasisEffort",
"BasisFlags",
"TranscoderTextureFormat",
"TranscodeDecodeFlags",
]

Binary file not shown.

Binary file not shown.

222
python/basisu_py/codec.py Normal file
View File

@@ -0,0 +1,222 @@
# basisu_py/codec.py
import importlib
import numpy as np
from PIL import Image
import ctypes
from .constants import BasisTexFormat, BasisQuality, BasisEffort, BasisFlags
from pathlib import Path
class EncoderBackend:
NATIVE = "native"
WASM = "wasm"
AUTO = "auto"
class Encoder:
def __init__(self, backend=EncoderBackend.AUTO):
self.backend = backend
self._native = None
self._wasm = None
self.backend_name = None
# ------------------------------------------------------------------
# Try native first (AUTO or NATIVE modes)
# ------------------------------------------------------------------
if backend in (EncoderBackend.AUTO, EncoderBackend.NATIVE):
try:
import basisu_py.basisu_python as native_encoder
native_encoder.init()
self._native = native_encoder
self._wasm = None
self.backend_name = "NATIVE"
print("[Encoder] Using native backend")
return
except Exception as e:
if backend == EncoderBackend.NATIVE:
raise RuntimeError(
f"[Encoder] Native backend requested but unavailable: {e}"
)
print("[Encoder] Native unavailable; falling back to WASM:", e)
# ------------------------------------------------------------------
# Fallback to WASM (AUTO or explicitly WASM)
# ------------------------------------------------------------------
try:
from basisu_py.wasm.wasm_encoder import BasisuWasmEncoder
except Exception as e:
raise RuntimeError(
f"[Encoder] WASM backend cannot be imported: {e}\n"
"Make sure wasmtime is installed and basisu_py/wasm/*.wasm exist."
)
wasm_path = Path(__file__).parent / "wasm" / "basisu_module_st.wasm"
self._wasm = BasisuWasmEncoder(str(wasm_path))
self._wasm.load()
self._native = None
self.backend_name = "WASM"
print("[Encoder] Using WASM backend")
# ------------------------------------------------------
# Public API
# ------------------------------------------------------
def compress(self,
image,
format=-1,
quality=BasisQuality.MAX,
effort=BasisEffort.DEFAULT,
flags=BasisFlags.KTX2_OUTPUT | BasisFlags.SRGB | BasisFlags.THREADED | BasisFlags.XUASTC_LDR_FULL_ZSTD):
rgba_bytes, w, h, is_hdr = self._convert_input_to_rgba_bytes(image)
# Auto-select format if user passed -1
if format == -1:
if is_hdr:
format = BasisTexFormat.cUASTC_HDR_6x6
else:
format = BasisTexFormat.cXUASTC_LDR_6x6
if self._native:
return self._compress_native(rgba_bytes, w, h, format, quality, effort, flags, is_hdr)
else:
return self._compress_wasm(rgba_bytes, w, h, format, quality, effort, flags, is_hdr)
def compress_float32(self, arr, **kwargs):
if not isinstance(arr, np.ndarray) or arr.dtype != np.float32:
raise ValueError("compress_float32 requires float32 NumPy HxWx4 array")
return self.compress(arr, **kwargs)
# ------------------------------------------------------
# Native backend
# ------------------------------------------------------
def _compress_native(self, bytes_data, w, h, fmt, quality, effort, flags, is_hdr=False):
enc = self._native
params = enc.new_params()
try:
buf_ptr = enc.alloc(len(bytes_data))
# Write raw bytes (uint8 or float32)
ctypes.memmove(buf_ptr, bytes_data, len(bytes_data))
if is_hdr:
ok = enc.set_image_float_rgba(params, 0, buf_ptr, w, h, w * 16) # 4 floats = 16 bytes per pixel
else:
ok = enc.set_image_rgba32(params, 0, buf_ptr, w, h, w * 4)
if not ok:
raise RuntimeError("Native encoder: set_image failed (HDR or LDR)")
ok = enc.compress(params, fmt, quality, effort, flags, 0.0)
if not ok:
raise RuntimeError("Native encoder: compress() failed")
size = enc.get_comp_data_size(params)
ofs = enc.get_comp_data_ofs(params)
blob = enc.read_memory(ofs, size)
return blob
finally:
enc.delete_params(params)
if buf_ptr:
enc.free(buf_ptr)
# ------------------------------------------------------
# WASM backend
# ------------------------------------------------------
def _compress_wasm(self, bytes_data, w, h, fmt, quality, effort, flags, is_hdr=False):
enc = self._wasm
params = enc.new_params()
try:
buf_ptr = enc.alloc(len(bytes_data))
enc.write_bytes(buf_ptr, bytes_data)
if is_hdr:
ok = enc.set_image_float_rgba(params, 0, buf_ptr, w, h, w * 16)
else:
ok = enc.set_image_rgba32(params, 0, buf_ptr, w, h, w * 4)
if not ok:
raise RuntimeError("WASM encoder: set_image failed (HDR or LDR)")
ok = enc.compress(params, fmt, quality, effort, flags, 0.0)
if not ok:
raise RuntimeError("WASM encoder: compress() failed")
size = enc.get_comp_data_size(params)
ofs = enc.get_comp_data_ofs(params)
blob = enc.read_bytes(ofs, size)
return blob
finally:
enc.delete_params(params)
if buf_ptr:
enc.free(buf_ptr)
# ------------------------------------------------------
# Image conversion
# ------------------------------------------------------
def _convert_input_to_rgba_bytes(self, image):
"""
Accept:
- Pillow Image (LDR) -> returns uint8 bytes
- NumPy uint8 LDR -> returns uint8 bytes
- NumPy float32 HDR -> returns float32 bytes
Returns (bytes, width, height, is_hdr)
"""
# Pillow image -> LDR
if isinstance(image, Image.Image):
image = image.convert("RGBA")
arr = np.array(image, dtype=np.uint8)
h, w = arr.shape[:2]
return arr.tobytes(), w, h, False
# NumPy array
elif isinstance(image, np.ndarray):
# HDR float32 image
if image.dtype == np.float32:
if image.ndim != 3 or image.shape[2] not in (3,4):
raise ValueError("HDR NumPy image must be HxWx3 or HxWx4 float32")
h, w, c = image.shape
# Expand RGB -> RGBA if needed
if c == 3:
alpha = np.ones((h, w, 1), dtype=np.float32)
arr = np.concatenate([image, alpha], axis=2)
else:
arr = image
return arr.tobytes(), w, h, True
# LDR uint8 image
if image.dtype == np.uint8:
if image.ndim != 3 or image.shape[2] not in (3,4):
raise ValueError("LDR NumPy image must be HxWx3 or HxWx4 uint8")
h, w, c = image.shape
if c == 3:
alpha = np.full((h, w, 1), 255, dtype=np.uint8)
arr = np.concatenate([image, alpha], axis=2)
else:
arr = image
return arr.tobytes(), w, h, False
raise ValueError("NumPy image must be uint8 (LDR) or float32 (HDR)")
else:
raise TypeError("compress() expects Pillow Image or NumPy array")

View File

@@ -0,0 +1,183 @@
# basisu_constants.py
# ============================================================
# .KTX2/.basis file types
# basist::basis_tex_format
# ============================================================
class BasisTexFormat:
# Original LDR formats
cETC1S = 0
cUASTC_LDR_4x4 = 1
# HDR
cUASTC_HDR_4x4 = 2
cASTC_HDR_6x6 = 3
cUASTC_HDR_6x6 = 4
# XUASTC supercompressed LDR formats
cXUASTC_LDR_4x4 = 5
cXUASTC_LDR_5x4 = 6
cXUASTC_LDR_5x5 = 7
cXUASTC_LDR_6x5 = 8
cXUASTC_LDR_6x6 = 9
cXUASTC_LDR_8x5 = 10
cXUASTC_LDR_8x6 = 11
cXUASTC_LDR_10x5 = 12
cXUASTC_LDR_10x6 = 13
cXUASTC_LDR_8x8 = 14
cXUASTC_LDR_10x8 = 15
cXUASTC_LDR_10x10= 16
cXUASTC_LDR_12x10= 17
cXUASTC_LDR_12x12= 18
# Standard ASTC LDR
cASTC_LDR_4x4 = 19
cASTC_LDR_5x4 = 20
cASTC_LDR_5x5 = 21
cASTC_LDR_6x5 = 22
cASTC_LDR_6x6 = 23
cASTC_LDR_8x5 = 24
cASTC_LDR_8x6 = 25
cASTC_LDR_10x5 = 26
cASTC_LDR_10x6 = 27
cASTC_LDR_8x8 = 28
cASTC_LDR_10x8 = 29
cASTC_LDR_10x10= 30
cASTC_LDR_12x10= 31
cASTC_LDR_12x12= 32
# ============================================================
# Unified quality level: 1-100 (higher=better quality, 100 disables some codec options)
# ============================================================
class BasisQuality:
MIN = 1
MAX = 100
# ============================================================
# Unified effort level: 0-10 (0=fastest, 10=very slow, higher=slower but higher potential quality/more features utilized)
# ============================================================
class BasisEffort:
MIN = 0
MAX = 10
SUPER_FAST = 0
FAST = 2
NORMAL = 5
DEFAULT = 2
SLOW = 8
VERY_SLOW = 10
# ============================================================
# C-style API flags
# ============================================================
class BasisFlags:
NONE = 0
USE_OPENCL = 1 << 8
THREADED = 1 << 9
DEBUG_OUTPUT = 1 << 10
KTX2_OUTPUT = 1 << 11
KTX2_UASTC_ZSTD = 1 << 12
SRGB = 1 << 13
GEN_MIPS_CLAMP = 1 << 14
GEN_MIPS_WRAP = 1 << 15
Y_FLIP = 1 << 16
PRINT_STATS = 1 << 18
PRINT_STATUS = 1 << 19
DEBUG_IMAGES = 1 << 20
REC2020 = 1 << 21
VALIDATE_OUTPUT = 1 << 22
XUASTC_LDR_FULL_ARITH = 0
XUASTC_LDR_HYBRID = 1 << 23
XUASTC_LDR_FULL_ZSTD = 2 << 23
XUASTC_LDR_SYNTAX_SHIFT = 23
XUASTC_LDR_SYNTAX_MASK = 3
TEXTURE_TYPE_2D = 0 << 25
TEXTURE_TYPE_2D_ARRAY = 1 << 25
TEXTURE_TYPE_CUBEMAP_ARRAY = 2 << 25
TEXTURE_TYPE_VIDEO_FRAMES = 3 << 25
TEXTURE_TYPE_SHIFT = 25
TEXTURE_TYPE_MASK = 3
VERBOSE = PRINT_STATS | PRINT_STATUS
MIPMAP_CLAMP = GEN_MIPS_CLAMP
MIPMAP_WRAP = GEN_MIPS_WRAP
# ============================================================
# Transcoder Texture Formats (GPU block formats)
# basist::transcoder_texture_format
# ============================================================
class TranscoderTextureFormat:
TF_ETC1_RGB = 0
TF_ETC2_RGBA = 1
TF_BC1_RGB = 2
TF_BC3_RGBA = 3
TF_BC4_R = 4
TF_BC5_RG = 5
TF_BC7_RGBA = 6
TF_PVRTC1_4_RGB = 8
TF_PVRTC1_4_RGBA = 9
TF_ASTC_LDR_4X4_RGBA = 10
TF_ATC_RGB = 11
TF_ATC_RGBA = 12
# Uncompressed
TF_RGBA32 = 13
TF_RGB565 = 14
TF_BGR565 = 15
TF_RGBA4444 = 16
TF_FXT1_RGB = 17
TF_PVRTC2_4_RGB = 18
TF_PVRTC2_4_RGBA = 19
TF_ETC2_EAC_R11 = 20
TF_ETC2_EAC_RG11 = 21
TF_BC6H = 22
TF_ASTC_HDR_4X4_RGBA = 23
TF_RGB_HALF = 24
TF_RGBA_HALF = 25
TF_RGB_9E5 = 26
TF_ASTC_HDR_6X6_RGBA = 27
TF_ASTC_LDR_5X4_RGBA = 28
TF_ASTC_LDR_5X5_RGBA = 29
TF_ASTC_LDR_6X5_RGBA = 30
TF_ASTC_LDR_6X6_RGBA = 31
TF_ASTC_LDR_8X5_RGBA = 32
TF_ASTC_LDR_8X6_RGBA = 33
TF_ASTC_LDR_10X5_RGBA = 34
TF_ASTC_LDR_10X6_RGBA = 35
TF_ASTC_LDR_8X8_RGBA = 36
TF_ASTC_LDR_10X8_RGBA = 37
TF_ASTC_LDR_10X10_RGBA= 38
TF_ASTC_LDR_12X10_RGBA= 39
TF_ASTC_LDR_12X12_RGBA= 40
TOTAL = 41
# ============================================================
# Transcoder Decode Flags
# ============================================================
class TranscodeDecodeFlags:
PVRTC_DECODE_TO_NEXT_POW2 = 2
TRANSCODE_ALPHA_TO_OPAQUE = 4
BC1_FORBID_THREE_COLOR_BLOCKS = 8
OUTPUT_HAS_ALPHA_INDICES = 16
HIGH_QUALITY = 32
NO_ETC1S_CHROMA_FILTERING = 64
NO_DEBLOCK_FILTERING = 128
STRONGER_DEBLOCK_FILTERING = 256
FORCE_DEBLOCK_FILTERING = 512
XUASTC_LDR_DISABLE_FAST_BC7_TRANSCODING = 1024

View File

@@ -0,0 +1,735 @@
# 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

View File

@@ -0,0 +1 @@
# Purposely empty

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,126 @@
# basisu_py/wasm/wasm_encoder.py
import wasmtime
import ctypes
from ..constants import BasisTexFormat, BasisQuality, BasisEffort, BasisFlags
class BasisuWasmEncoder:
def __init__(self, wasm_path):
self.wasm_path = wasm_path
self.engine = None
self.store = None
self.memory = None
self.exports = None
# ------------------------------------------------------
# Initialize WASM + WASI
# ------------------------------------------------------
def _init_engine(self):
self.engine = wasmtime.Engine()
self.store = wasmtime.Store(self.engine)
wasi = wasmtime.WasiConfig()
wasi.argv = ["basisu-wasm"]
wasi.inherit_stdout()
wasi.inherit_stderr()
self.store.set_wasi(wasi)
def load(self):
self._init_engine()
module = wasmtime.Module.from_file(self.engine, self.wasm_path)
linker = wasmtime.Linker(self.engine)
linker.define_wasi()
instance = linker.instantiate(self.store, module)
self.exports = instance.exports(self.store)
self.memory = self.exports["memory"]
# Initialize if present
if "bu_init" in self.exports:
self.exports["bu_init"](self.store)
print("[WASM Encoder] Loaded:", self.wasm_path)
# ------------------------------------------------------
# Access raw linear memory buffer
# ------------------------------------------------------
def _buf(self):
raw_ptr = self.memory.data_ptr(self.store)
size = self.memory.data_len(self.store)
addr = ctypes.addressof(raw_ptr.contents)
return (ctypes.c_ubyte * size).from_address(addr)
# ------------------------------------------------------
# Version
# ------------------------------------------------------
def get_version(self):
return self.exports["bu_get_version"](self.store)
# ------------------------------------------------------
# Memory alloc/free
# ------------------------------------------------------
def alloc(self, size):
return self.exports["bu_alloc"](self.store, size)
def free(self, ptr):
self.exports["bu_free"](self.store, ptr)
# ------------------------------------------------------
# Params
# ------------------------------------------------------
def new_params(self):
return self.exports["bu_new_comp_params"](self.store)
def delete_params(self, params):
return self.exports["bu_delete_comp_params"](self.store, params)
# ------------------------------------------------------
# Image input
# ------------------------------------------------------
def set_image_rgba32(self, params, index, ptr, w, h, pitch):
return self.exports["bu_comp_params_set_image_rgba32"](
self.store, params, index, ptr, w, h, pitch
)
def set_image_float_rgba(self, params, index, ptr, w, h, pitch):
return self.exports["bu_comp_params_set_image_float_rgba"](
self.store, params, index, ptr, w, h, pitch
)
# ------------------------------------------------------
# Compression
# ------------------------------------------------------
def compress(self, params, fmt, quality, effort, flags, rdo):
return bool(self.exports["bu_compress_texture"](
self.store, params, fmt, quality, effort, flags, rdo
))
# ------------------------------------------------------
# Output blob
# ------------------------------------------------------
def get_comp_data_size(self, params):
return self.exports["bu_comp_params_get_comp_data_size"](self.store, params)
def get_comp_data_ofs(self, params):
return self.exports["bu_comp_params_get_comp_data_ofs"](self.store, params)
# ------------------------------------------------------
# Raw memory I/O
# ------------------------------------------------------
def write_bytes(self, ptr, data):
buf = self._buf()
buf[ptr:ptr + len(data)] = data
def read_bytes(self, ptr, size):
buf = self._buf()
return bytes(buf[ptr:ptr + size])
# NEW unified names:
def write_memory(self, ptr, data):
self.write_bytes(ptr, data)
def read_memory(self, ptr, size):
return self.read_bytes(ptr, size)

View File

@@ -0,0 +1,326 @@
# basisu_py/wasm/wasm_transcoder.py
import wasmtime
import ctypes
class BasisuWasmTranscoder:
"""
Lowest-level WASM transcoder wrapper.
Direct mapping to basisu_wasm_transcoder_api.h/.cpp
NOTE:
- This layer does NOT interpret formats or block sizes.
- It only wraps the raw C API (bt_* and basis_* exports).
- Higher-level logic (TranscoderCore, Transcoder) will build on top.
"""
def __init__(self, wasm_path: str):
self.wasm_path = wasm_path
self.engine = None
self.store = None
self.memory = None
self.exports = None
# ------------------------------------------------------
# Internal: initialize WASM + WASI
# ------------------------------------------------------
def _init_engine(self):
self.engine = wasmtime.Engine()
self.store = wasmtime.Store(self.engine)
wasi = wasmtime.WasiConfig()
wasi.argv = ["basisu-transcoder"]
wasi.inherit_stdout()
wasi.inherit_stderr()
self.store.set_wasi(wasi)
def load(self):
self._init_engine()
module = wasmtime.Module.from_file(self.engine, self.wasm_path)
linker = wasmtime.Linker(self.engine)
linker.define_wasi()
instance = linker.instantiate(self.store, module)
self.exports = instance.exports(self.store)
self.memory = self.exports["memory"]
# Mandatory transcoder init
if "bt_init" in self.exports:
self.exports["bt_init"](self.store)
print("[WASM Transcoder] Loaded:", self.wasm_path)
# ------------------------------------------------------
# Linear memory access helpers
# ------------------------------------------------------
def _buf(self):
raw_ptr = self.memory.data_ptr(self.store)
size = self.memory.data_len(self.store)
addr = ctypes.addressof(raw_ptr.contents)
return (ctypes.c_ubyte * size).from_address(addr)
def write_bytes(self, ptr: int, data: bytes):
buf = self._buf()
buf[ptr:ptr + len(data)] = data
def read_bytes(self, ptr: int, num: int) -> bytes:
buf = self._buf()
return bytes(buf[ptr:ptr + num])
# NEW unified names:
def write_memory(self, ptr, data):
self.write_bytes(ptr, data)
def read_memory(self, ptr, size):
return self.read_bytes(ptr, size)
# ------------------------------------------------------
# Memory alloc/free
# ------------------------------------------------------
def alloc(self, size: int) -> int:
return self.exports["bt_alloc"](self.store, size)
def free(self, ptr: int):
return self.exports["bt_free"](self.store, ptr)
# ------------------------------------------------------
# High-level functions: version, init, debug
# ------------------------------------------------------
def get_version(self) -> int:
return self.exports["bt_get_version"](self.store)
def enable_debug_printf(self, flag: bool = True):
return self.exports["bt_enable_debug_printf"](self.store, 1 if flag else 0)
# ------------------------------------------------------
# basis_tex_format helpers
# ------------------------------------------------------
def basis_tex_format_is_xuastc_ldr(self, basis_tex_fmt_u32: int) -> bool:
return bool(self.exports["bt_basis_tex_format_is_xuastc_ldr"](self.store, basis_tex_fmt_u32))
def basis_tex_format_is_astc_ldr(self, basis_tex_fmt_u32: int) -> bool:
return bool(self.exports["bt_basis_tex_format_is_astc_ldr"](self.store, basis_tex_fmt_u32))
def basis_tex_format_get_block_width(self, basis_tex_fmt_u32: int) -> int:
return self.exports["bt_basis_tex_format_get_block_width"](self.store, basis_tex_fmt_u32)
def basis_tex_format_get_block_height(self, basis_tex_fmt_u32: int) -> int:
return self.exports["bt_basis_tex_format_get_block_height"](self.store, basis_tex_fmt_u32)
def basis_tex_format_is_hdr(self, basis_tex_fmt_u32: int) -> bool:
return bool(self.exports["bt_basis_tex_format_is_hdr"](self.store, basis_tex_fmt_u32))
def basis_tex_format_is_ldr(self, basis_tex_fmt_u32: int) -> bool:
return bool(self.exports["bt_basis_tex_format_is_ldr"](self.store, basis_tex_fmt_u32))
# ------------------------------------------------------
# transcoder_texture_format helpers
# ------------------------------------------------------
def basis_get_bytes_per_block_or_pixel(self, tfmt_u32: int) -> int:
return self.exports["bt_basis_get_bytes_per_block_or_pixel"](self.store, tfmt_u32)
def basis_transcoder_format_has_alpha(self, tfmt_u32: int) -> bool:
return bool(self.exports["bt_basis_transcoder_format_has_alpha"](self.store, tfmt_u32))
def basis_transcoder_format_is_hdr(self, tfmt_u32: int) -> bool:
return bool(self.exports["bt_basis_transcoder_format_is_hdr"](self.store, tfmt_u32))
def basis_transcoder_format_is_ldr(self, tfmt_u32: int) -> bool:
return bool(self.exports["bt_basis_transcoder_format_is_ldr"](self.store, tfmt_u32))
def basis_transcoder_texture_format_is_astc(self, tfmt_u32: int) -> bool:
return bool(self.exports["bt_basis_transcoder_texture_format_is_astc"](self.store, tfmt_u32))
def basis_transcoder_format_is_uncompressed(self, tfmt_u32: int) -> bool:
return bool(self.exports["bt_basis_transcoder_format_is_uncompressed"](self.store, tfmt_u32))
def basis_get_uncompressed_bytes_per_pixel(self, tfmt_u32: int) -> int:
return self.exports["bt_basis_get_uncompressed_bytes_per_pixel"](self.store, tfmt_u32)
def basis_get_block_width(self, tfmt_u32: int) -> int:
return self.exports["bt_basis_get_block_width"](self.store, tfmt_u32)
def basis_get_block_height(self, tfmt_u32: int) -> int:
return self.exports["bt_basis_get_block_height"](self.store, tfmt_u32)
def basis_get_transcoder_texture_format_from_basis_tex_format(self, basis_tex_fmt_u32: int) -> int:
return self.exports["bt_basis_get_transcoder_texture_format_from_basis_tex_format"](self.store, basis_tex_fmt_u32)
def basis_is_format_supported(self, tfmt_u32: int, basis_tex_fmt_u32: int) -> bool:
return bool(self.exports["bt_basis_is_format_supported"](self.store, tfmt_u32, basis_tex_fmt_u32))
def basis_compute_transcoded_image_size_in_bytes(self, tfmt_u32: int, orig_width: int, orig_height: int) -> int:
return self.exports["bt_basis_compute_transcoded_image_size_in_bytes"](
self.store, tfmt_u32, orig_width, orig_height
)
# ------------------------------------------------------
# KTX2 handle management
# ------------------------------------------------------
def ktx2_open(self, data_ptr: int, data_len: int) -> int:
return self.exports["bt_ktx2_open"](self.store, data_ptr, data_len)
def ktx2_close(self, handle: int):
return self.exports["bt_ktx2_close"](self.store, handle)
# ------------------------------------------------------
# Basic KTX2 metadata
# ------------------------------------------------------
def ktx2_get_width(self, handle: int) -> int:
return self.exports["bt_ktx2_get_width"](self.store, handle)
def ktx2_get_height(self, handle: int) -> int:
return self.exports["bt_ktx2_get_height"](self.store, handle)
def ktx2_get_levels(self, handle: int) -> int:
return self.exports["bt_ktx2_get_levels"](self.store, handle)
def ktx2_get_faces(self, handle: int) -> int:
return self.exports["bt_ktx2_get_faces"](self.store, handle)
def ktx2_get_layers(self, handle: int) -> int:
return self.exports["bt_ktx2_get_layers"](self.store, handle)
def ktx2_get_basis_tex_format(self, handle: int) -> int:
return self.exports["bt_ktx2_get_basis_tex_format"](self.store, handle)
# KTX2 format checks
def ktx2_is_etc1s(self, handle: int) -> bool:
return bool(self.exports["bt_ktx2_is_etc1s"](self.store, handle))
def ktx2_is_uastc_ldr_4x4(self, handle: int) -> bool:
return bool(self.exports["bt_ktx2_is_uastc_ldr_4x4"](self.store, handle))
def ktx2_is_hdr(self, handle: int) -> bool:
return bool(self.exports["bt_ktx2_is_hdr"](self.store, handle))
def ktx2_is_hdr_4x4(self, handle: int) -> bool:
return bool(self.exports["bt_ktx2_is_hdr_4x4"](self.store, handle))
def ktx2_is_hdr_6x6(self, handle: int) -> bool:
return bool(self.exports["bt_ktx2_is_hdr_6x6"](self.store, handle))
def ktx2_is_ldr(self, handle: int) -> bool:
return bool(self.exports["bt_ktx2_is_ldr"](self.store, handle))
def ktx2_is_astc_ldr(self, handle: int) -> bool:
return bool(self.exports["bt_ktx2_is_astc_ldr"](self.store, handle))
def ktx2_is_xuastc_ldr(self, handle: int) -> bool:
return bool(self.exports["bt_ktx2_is_xuastc_ldr"](self.store, handle))
def ktx2_get_block_width(self, handle: int) -> int:
return self.exports["bt_ktx2_get_block_width"](self.store, handle)
def ktx2_get_block_height(self, handle: int) -> int:
return self.exports["bt_ktx2_get_block_height"](self.store, handle)
def ktx2_has_alpha(self, handle: int) -> bool:
return bool(self.exports["bt_ktx2_has_alpha"](self.store, handle))
def ktx2_get_dfd_color_model(self, handle: int) -> int:
return self.exports["bt_ktx2_get_dfd_color_model"](self.store, handle)
def ktx2_get_dfd_color_primaries(self, handle: int) -> int:
return self.exports["bt_ktx2_get_dfd_color_primaries"](self.store, handle)
def ktx2_get_dfd_transfer_func(self, handle: int) -> int:
return self.exports["bt_ktx2_get_dfd_transfer_func"](self.store, handle)
def ktx2_is_srgb(self, handle: int) -> bool:
return bool(self.exports["bt_ktx2_is_srgb"](self.store, handle))
def ktx2_get_dfd_flags(self, handle: int) -> int:
return self.exports["bt_ktx2_get_dfd_flags"](self.store, handle)
def ktx2_get_dfd_total_samples(self, handle: int) -> int:
return self.exports["bt_ktx2_get_dfd_total_samples"](self.store, handle)
def ktx2_get_dfd_channel_id0(self, handle: int) -> int:
return self.exports["bt_ktx2_get_dfd_channel_id0"](self.store, handle)
def ktx2_get_dfd_channel_id1(self, handle: int) -> int:
return self.exports["bt_ktx2_get_dfd_channel_id1"](self.store, handle)
def ktx2_is_video(self, handle: int) -> bool:
return bool(self.exports["bt_ktx2_is_video"](self.store, handle))
def ktx2_get_ldr_hdr_upconversion_nit_multiplier(self, handle: int) -> float:
return self.exports["bt_ktx2_get_ldr_hdr_upconversion_nit_multiplier"](self.store, handle)
# ------------------------------------------------------
# Per-level metadata
# ------------------------------------------------------
def ktx2_get_level_orig_width(self, h, lvl, layer, face) -> int:
return self.exports["bt_ktx2_get_level_orig_width"](self.store, h, lvl, layer, face)
def ktx2_get_level_orig_height(self, h, lvl, layer, face) -> int:
return self.exports["bt_ktx2_get_level_orig_height"](self.store, h, lvl, layer, face)
def ktx2_get_level_actual_width(self, h, lvl, layer, face) -> int:
return self.exports["bt_ktx2_get_level_actual_width"](self.store, h, lvl, layer, face)
def ktx2_get_level_actual_height(self, h, lvl, layer, face) -> int:
return self.exports["bt_ktx2_get_level_actual_height"](self.store, h, lvl, layer, face)
def ktx2_get_level_num_blocks_x(self, h, lvl, layer, face) -> int:
return self.exports["bt_ktx2_get_level_num_blocks_x"](self.store, h, lvl, layer, face)
def ktx2_get_level_num_blocks_y(self, h, lvl, layer, face) -> int:
return self.exports["bt_ktx2_get_level_num_blocks_y"](self.store, h, lvl, layer, face)
def ktx2_get_level_total_blocks(self, h, lvl, layer, face) -> int:
return self.exports["bt_ktx2_get_level_total_blocks"](self.store, h, lvl, layer, face)
def ktx2_get_level_alpha_flag(self, h, lvl, layer, face) -> bool:
return bool(self.exports["bt_ktx2_get_level_alpha_flag"](self.store, h, lvl, layer, face))
def ktx2_get_level_iframe_flag(self, h, lvl, layer, face) -> bool:
return bool(self.exports["bt_ktx2_get_level_iframe_flag"](self.store, h, lvl, layer, face))
# ------------------------------------------------------
# Transcoding control
# ------------------------------------------------------
def ktx2_start_transcoding(self, handle: int) -> bool:
return bool(self.exports["bt_ktx2_start_transcoding"](self.store, handle))
def ktx2_create_transcode_state(self) -> int:
return self.exports["bt_ktx2_create_transcode_state"](self.store)
def ktx2_destroy_transcode_state(self, handle: int):
return self.exports["bt_ktx2_destroy_transcode_state"](self.store, handle)
# ------------------------------------------------------
# Actual transcoding call
# ------------------------------------------------------
def ktx2_transcode_image_level(
self,
ktx2_handle: int,
level_index: int,
layer_index: int,
face_index: int,
output_block_mem_ofs: int,
output_blocks_buf_size_in_blocks_or_pixels: int,
transcoder_texture_format_u32: int,
decode_flags: int,
output_row_pitch_in_blocks_or_pixels: int,
output_rows_in_pixels: int,
channel0: int,
channel1: int,
state_handle: int,
) -> bool:
return bool(self.exports["bt_ktx2_transcode_image_level"](
self.store,
ktx2_handle,
level_index, layer_index, face_index,
output_block_mem_ofs,
output_blocks_buf_size_in_blocks_or_pixels,
transcoder_texture_format_u32,
decode_flags,
output_row_pitch_in_blocks_or_pixels,
output_rows_in_pixels,
channel0, channel1,
state_handle
))

View File

@@ -0,0 +1,264 @@
// File: basisu_transcoder_pybind11.cpp
// pybind11 native bindings for the transcoder's pure C API basisu_wasm_transcoder_api.h
#include <pybind11/pybind11.h>
#include <stdint.h>
#include "../encoder/basisu_wasm_transcoder_api.h"
namespace py = pybind11;
// wasm_bool_t is uint32_t — convert to Python bool
static inline bool to_bool(wasm_bool_t v) { return v != 0; }
PYBIND11_MODULE(basisu_transcoder_python, m) {
m.doc() = "Native Basis Universal transcoder (pybind11 binding over basisu_wasm_transcoder_api)";
// ------------------------------------------------------------------------
// High-level functions
// ------------------------------------------------------------------------
m.def("get_version", &bt_get_version,
"Get BasisU transcoder version");
m.def("enable_debug_printf",
[](bool flag) { bt_enable_debug_printf(flag ? 1u : 0u); },
"Enable or disable debug printf output");
m.def("init", &bt_init,
"Initialize transcoder library");
m.def("alloc", &bt_alloc,
"Allocate a buffer, returns uint64 offset/pointer");
m.def("free", &bt_free,
"Free a buffer allocated by bt_alloc");
// ------------------------------------------------------------------------
// basis_tex_format helpers
// ------------------------------------------------------------------------
m.def("basis_tex_format_is_xuastc_ldr",
[](uint32_t fmt) { return to_bool(bt_basis_tex_format_is_xuastc_ldr(fmt)); });
m.def("basis_tex_format_is_astc_ldr",
[](uint32_t fmt) { return to_bool(bt_basis_tex_format_is_astc_ldr(fmt)); });
m.def("basis_tex_format_get_block_width",
&bt_basis_tex_format_get_block_width);
m.def("basis_tex_format_get_block_height",
&bt_basis_tex_format_get_block_height);
m.def("basis_tex_format_is_hdr",
[](uint32_t fmt) { return to_bool(bt_basis_tex_format_is_hdr(fmt)); });
m.def("basis_tex_format_is_ldr",
[](uint32_t fmt) { return to_bool(bt_basis_tex_format_is_ldr(fmt)); });
// ------------------------------------------------------------------------
// transcoder_texture_format helpers
// ------------------------------------------------------------------------
m.def("basis_get_bytes_per_block_or_pixel",
&bt_basis_get_bytes_per_block_or_pixel);
m.def("basis_transcoder_format_has_alpha",
[](uint32_t tfmt) { return to_bool(bt_basis_transcoder_format_has_alpha(tfmt)); });
m.def("basis_transcoder_format_is_hdr",
[](uint32_t tfmt) { return to_bool(bt_basis_transcoder_format_is_hdr(tfmt)); });
m.def("basis_transcoder_format_is_ldr",
[](uint32_t tfmt) { return to_bool(bt_basis_transcoder_format_is_ldr(tfmt)); });
m.def("basis_transcoder_texture_format_is_astc",
[](uint32_t tfmt) { return to_bool(bt_basis_transcoder_texture_format_is_astc(tfmt)); });
m.def("basis_transcoder_format_is_uncompressed",
[](uint32_t tfmt) { return to_bool(bt_basis_transcoder_format_is_uncompressed(tfmt)); });
m.def("basis_get_uncompressed_bytes_per_pixel",
&bt_basis_get_uncompressed_bytes_per_pixel);
m.def("basis_get_block_width",
&bt_basis_get_block_width);
m.def("basis_get_block_height",
&bt_basis_get_block_height);
m.def("basis_get_transcoder_texture_format_from_basis_tex_format",
&bt_basis_get_transcoder_texture_format_from_basis_tex_format);
m.def("basis_is_format_supported",
[](uint32_t tfmt, uint32_t basis_fmt) {
return to_bool(bt_basis_is_format_supported(tfmt, basis_fmt));
});
m.def("basis_compute_transcoded_image_size_in_bytes",
&bt_basis_compute_transcoded_image_size_in_bytes);
// ------------------------------------------------------------------------
// KTX2 open/close & basic info
// ------------------------------------------------------------------------
m.def("ktx2_open", &bt_ktx2_open,
"Open a KTX2 image from memory; returns handle");
m.def("ktx2_close", &bt_ktx2_close,
"Close a previously opened KTX2 handle");
m.def("ktx2_get_width", &bt_ktx2_get_width);
m.def("ktx2_get_height", &bt_ktx2_get_height);
m.def("ktx2_get_levels", &bt_ktx2_get_levels);
m.def("ktx2_get_faces", &bt_ktx2_get_faces);
m.def("ktx2_get_layers", &bt_ktx2_get_layers);
m.def("ktx2_get_basis_tex_format", &bt_ktx2_get_basis_tex_format);
m.def("ktx2_is_etc1s",
[](uint64_t h) { return to_bool(bt_ktx2_is_etc1s(h)); });
m.def("ktx2_is_uastc_ldr_4x4",
[](uint64_t h) { return to_bool(bt_ktx2_is_uastc_ldr_4x4(h)); });
m.def("ktx2_is_hdr",
[](uint64_t h) { return to_bool(bt_ktx2_is_hdr(h)); });
m.def("ktx2_is_hdr_4x4",
[](uint64_t h) { return to_bool(bt_ktx2_is_hdr_4x4(h)); });
m.def("ktx2_is_hdr_6x6",
[](uint64_t h) { return to_bool(bt_ktx2_is_hdr_6x6(h)); });
m.def("ktx2_is_ldr",
[](uint64_t h) { return to_bool(bt_ktx2_is_ldr(h)); });
m.def("ktx2_is_astc_ldr",
[](uint64_t h) { return to_bool(bt_ktx2_is_astc_ldr(h)); });
m.def("ktx2_is_xuastc_ldr",
[](uint64_t h) { return to_bool(bt_ktx2_is_xuastc_ldr(h)); });
m.def("ktx2_get_block_width", &bt_ktx2_get_block_width);
m.def("ktx2_get_block_height", &bt_ktx2_get_block_height);
m.def("ktx2_has_alpha",
[](uint64_t h) { return to_bool(bt_ktx2_has_alpha(h)); });
m.def("ktx2_get_dfd_color_model", &bt_ktx2_get_dfd_color_model);
m.def("ktx2_get_dfd_color_primaries", &bt_ktx2_get_dfd_color_primaries);
m.def("ktx2_get_dfd_transfer_func", &bt_ktx2_get_dfd_transfer_func);
m.def("ktx2_is_srgb",
[](uint64_t h) { return to_bool(bt_ktx2_is_srgb(h)); });
m.def("ktx2_get_dfd_flags", &bt_ktx2_get_dfd_flags);
m.def("ktx2_get_dfd_total_samples", &bt_ktx2_get_dfd_total_samples);
m.def("ktx2_get_dfd_channel_id0", &bt_ktx2_get_dfd_channel_id0);
m.def("ktx2_get_dfd_channel_id1", &bt_ktx2_get_dfd_channel_id1);
m.def("ktx2_is_video",
[](uint64_t h) { return to_bool(bt_ktx2_is_video(h)); });
m.def("ktx2_get_ldr_hdr_upconversion_nit_multiplier",
&bt_ktx2_get_ldr_hdr_upconversion_nit_multiplier);
// ------------------------------------------------------------------------
// KTX2 per-level info
// ------------------------------------------------------------------------
m.def("ktx2_get_level_orig_width",
&bt_ktx2_get_level_orig_width);
m.def("ktx2_get_level_orig_height",
&bt_ktx2_get_level_orig_height);
m.def("ktx2_get_level_actual_width",
&bt_ktx2_get_level_actual_width);
m.def("ktx2_get_level_actual_height",
&bt_ktx2_get_level_actual_height);
m.def("ktx2_get_level_num_blocks_x",
&bt_ktx2_get_level_num_blocks_x);
m.def("ktx2_get_level_num_blocks_y",
&bt_ktx2_get_level_num_blocks_y);
m.def("ktx2_get_level_total_blocks",
&bt_ktx2_get_level_total_blocks);
m.def("ktx2_get_level_alpha_flag",
[](uint64_t h, uint32_t level, uint32_t layer, uint32_t face) {
return to_bool(bt_ktx2_get_level_alpha_flag(h, level, layer, face));
});
m.def("ktx2_get_level_iframe_flag",
[](uint64_t h, uint32_t level, uint32_t layer, uint32_t face) {
return to_bool(bt_ktx2_get_level_iframe_flag(h, level, layer, face));
});
// ------------------------------------------------------------------------
// Transcoding state and operations
// ------------------------------------------------------------------------
m.def("ktx2_start_transcoding",
[](uint64_t h) { return to_bool(bt_ktx2_start_transcoding(h)); });
m.def("ktx2_create_transcode_state",
&bt_ktx2_create_transcode_state);
m.def("ktx2_destroy_transcode_state",
&bt_ktx2_destroy_transcode_state);
m.def("ktx2_transcode_image_level",
[](uint64_t ktx2_handle,
uint32_t level_index, uint32_t layer_index, uint32_t face_index,
uint64_t out_mem_ofs,
uint32_t out_blocks_or_pixels,
uint32_t transcoder_texture_format_u32,
uint32_t decode_flags,
uint32_t row_pitch_blocks_or_pixels,
uint32_t rows_in_pixels,
int channel0, int channel1,
uint64_t state_handle)
{
return to_bool(bt_ktx2_transcode_image_level(
ktx2_handle,
level_index, layer_index, face_index,
out_mem_ofs,
out_blocks_or_pixels,
transcoder_texture_format_u32,
decode_flags,
row_pitch_blocks_or_pixels,
rows_in_pixels,
channel0, channel1,
state_handle));
},
py::arg("ktx2_handle"),
py::arg("level_index"),
py::arg("layer_index"),
py::arg("face_index"),
py::arg("output_block_mem_ofs"),
py::arg("output_blocks_buf_size_in_blocks_or_pixels"),
py::arg("transcoder_texture_format_u32"),
py::arg("decode_flags"),
py::arg("output_row_pitch_in_blocks_or_pixels") = 0,
py::arg("output_rows_in_pixels") = 0,
py::arg("channel0") = -1,
py::arg("channel1") = -1,
py::arg("state_handle") = 0);
m.def("read_memory",
[](uint64_t ptr, uint32_t size) {
return py::bytes((const char*)ptr, size);
},
"Read `size` bytes starting at native memory address `ptr`");
m.def("write_memory",
[](uint64_t dest_ptr, py::buffer src) {
py::buffer_info info = src.request();
memcpy((void*)dest_ptr, info.ptr, info.size * info.itemsize);
},
"Write bytes/buffer-like object into native memory at address `ptr`");
}

332
python/dds_writer.py Normal file
View File

@@ -0,0 +1,332 @@
# dds_writer.py
#
# Minimal DDS writer that mirrors the C/C++ save_dds() implementation you provided.
# It writes a DX9-style DDS header, and optionally a DX10 extension header,
# followed by the raw compressed blocks.
#
# No mipmaps, no cubes, no 3D volumes exactly like the original C code.
import struct
import sys
from typing import Union
# ---------------------------------------------------------------------------
# FourCC helper (same as PIXEL_FMT_FOURCC macro)
# ---------------------------------------------------------------------------
def make_fourcc(a: str, b: str, c: str, d: str) -> int:
return (ord(a) |
(ord(b) << 8) |
(ord(c) << 16) |
(ord(d) << 24))
# ---------------------------------------------------------------------------
# DDS-related constants (only the ones we actually use)
# ---------------------------------------------------------------------------
# DDSD flags
DDSD_CAPS = 0x00000001
DDSD_HEIGHT = 0x00000002
DDSD_WIDTH = 0x00000004
DDSD_PIXELFORMAT= 0x00001000
DDSD_LINEARSIZE = 0x00080000
# DDPF flags
DDPF_FOURCC = 0x00000004
# DDSCAPS flags
DDSCAPS_TEXTURE = 0x00001000
# DXGI_FORMAT subset (values must match the C enum)
class DXGI_FORMAT:
UNKNOWN = 0
BC1_UNORM = 71
BC3_UNORM = 77
BC4_UNORM = 80
BC5_UNORM = 83
# You can add more as needed; for DX10 header we just write the integer value.
# DX10 resource dimension
class D3D10_RESOURCE_DIMENSION:
UNKNOWN = 0
BUFFER = 1
TEXTURE1D = 2
TEXTURE2D = 3
TEXTURE3D = 4
# ---------------------------------------------------------------------------
# DDS writer class
# ---------------------------------------------------------------------------
class DDSWriter:
"""
Python port of the C save_dds() function.
Usage:
writer = DDSWriter()
ok = writer.save_dds(
filename="out.dds",
width=width,
height=height,
blocks=bc_data, # bytes or bytearray
pixel_format_bpp=4, # e.g. 4 for BC1, 8 for BC3/4/5/etc.
dxgi_format=DXGI_FORMAT.BC1_UNORM,
srgb=False,
force_dx10_header=False,
)
"""
DDS_MAGIC = b"DDS " # same as fwrite("DDS ", 4, 1, pFile);
def save_dds(
self,
filename: str,
width: int,
height: int,
blocks: Union[bytes, bytearray, memoryview],
pixel_format_bpp: int,
dxgi_format: int,
srgb: bool = False,
force_dx10_header: bool = False,
) -> bool:
"""
Port of:
bool save_dds(const char* pFilename,
uint32_t width, uint32_t height,
const void* pBlocks,
uint32_t pixel_format_bpp,
DXGI_FORMAT dxgi_format,
bool srgb,
bool force_dx10_header);
The 'blocks' buffer is written as-is (up to computed linear size).
"""
# srgb is intentionally unused in the original C code (commented logic).
_ = srgb
# Open file like the C code
try:
f = open(filename, "wb")
except OSError:
print(f"Failed creating file {filename}!", file=sys.stderr)
return False
try:
# Write the "DDS " magic
f.write(self.DDS_MAGIC)
# -----------------------------------------------------------------
# Build DDSURFACEDESC2 equivalent
# -----------------------------------------------------------------
# We'll pack DDSURFACEDESC2 as 31 uint32's (124 bytes) in little-endian:
# struct DDSURFACEDESC2 {
# uint32 dwSize;
# uint32 dwFlags;
# uint32 dwHeight;
# uint32 dwWidth;
# uint32 lPitch_or_dwLinearSize;
# uint32 dwBackBufferCount;
# uint32 dwMipMapCount;
# uint32 dwAlphaBitDepth;
# uint32 dwUnused0;
# uint32 lpSurface;
# DDCOLORKEY unused0; (2 * uint32)
# DDCOLORKEY unused1; (2 * uint32)
# DDCOLORKEY unused2; (2 * uint32)
# DDCOLORKEY unused3; (2 * uint32)
# DDPIXELFORMAT ddpfPixelFormat; (8 * uint32)
# DDSCAPS2 ddsCaps; (4 * uint32)
# uint32 dwUnused1;
# };
dwSize = 124 # sizeof(DDSURFACEDESC2)
dwFlags = (
DDSD_WIDTH |
DDSD_HEIGHT |
DDSD_PIXELFORMAT |
DDSD_CAPS
)
dwWidth = int(width)
dwHeight = int(height)
# lPitch (actually LinearSize for compressed formats), same as:
# (((dwWidth + 3) & ~3) * ((dwHeight + 3) & ~3) * pixel_format_bpp) >> 3;
lPitch = (
((dwWidth + 3) & ~3)
* ((dwHeight + 3) & ~3)
* int(pixel_format_bpp)
) >> 3
dwFlags |= DDSD_LINEARSIZE
dwBackBufferCount = 0
dwMipMapCount = 0
dwAlphaBitDepth = 0
dwUnused0 = 0
lpSurface = 0
# DDCOLORKEY unused0..3, all zero
ddcolorkey_zero = [0, 0] * 4 # 4 DDCOLORKEY structs
# DDPIXELFORMAT
# struct DDPIXELFORMAT {
# uint32 dwSize;
# uint32 dwFlags;
# uint32 dwFourCC;
# uint32 dwRGBBitCount;
# uint32 dwRBitMask;
# uint32 dwGBitMask;
# uint32 dwBBitMask;
# uint32 dwRGBAlphaBitMask;
# };
ddpf_dwSize = 32
ddpf_dwFlags = DDPF_FOURCC
ddpf_dwFourCC = 0
ddpf_dwRGBBitCount = 0
ddpf_dwRBitMask = 0
ddpf_dwGBitMask = 0
ddpf_dwBBitMask = 0
ddpf_dwRGBAlphaBitMask = 0
# DDSCAPS2
# struct DDSCAPS2 {
# uint32 dwCaps;
# uint32 dwCaps2;
# uint32 dwCaps3;
# uint32 dwCaps4;
# };
ddsCaps_dwCaps = DDSCAPS_TEXTURE
ddsCaps_dwCaps2 = 0
ddsCaps_dwCaps3 = 0
ddsCaps_dwCaps4 = 0
dwUnused1 = 0
# Decide whether to use legacy FourCC (DXT1/DXT5/ATI1/ATI2) or DX10 header
use_legacy = (
not force_dx10_header and
dxgi_format in (
DXGI_FORMAT.BC1_UNORM,
DXGI_FORMAT.BC3_UNORM,
DXGI_FORMAT.BC4_UNORM,
DXGI_FORMAT.BC5_UNORM,
)
)
if use_legacy:
if dxgi_format == DXGI_FORMAT.BC1_UNORM:
ddpf_dwFourCC = make_fourcc('D', 'X', 'T', '1')
elif dxgi_format == DXGI_FORMAT.BC3_UNORM:
ddpf_dwFourCC = make_fourcc('D', 'X', 'T', '5')
elif dxgi_format == DXGI_FORMAT.BC4_UNORM:
ddpf_dwFourCC = make_fourcc('A', 'T', 'I', '1')
elif dxgi_format == DXGI_FORMAT.BC5_UNORM:
ddpf_dwFourCC = make_fourcc('A', 'T', 'I', '2')
else:
# Write DX10 header, FourCC = "DX10"
ddpf_dwFourCC = make_fourcc('D', 'X', '1', '0')
# Build the 31 uint32's for DDSURFACEDESC2
header_values = [
dwSize,
dwFlags,
dwHeight,
dwWidth,
lPitch,
dwBackBufferCount,
dwMipMapCount,
dwAlphaBitDepth,
dwUnused0,
lpSurface,
]
header_values.extend(ddcolorkey_zero) # 8 uint32's
ddpf_values = [
ddpf_dwSize,
ddpf_dwFlags,
ddpf_dwFourCC,
ddpf_dwRGBBitCount,
ddpf_dwRBitMask,
ddpf_dwGBitMask,
ddpf_dwBBitMask,
ddpf_dwRGBAlphaBitMask,
]
header_values.extend(ddpf_values) # 8 uint32's
ddsCaps_values = [
ddsCaps_dwCaps,
ddsCaps_dwCaps2,
ddsCaps_dwCaps3,
ddsCaps_dwCaps4,
]
header_values.extend(ddsCaps_values) # 4 uint32's
header_values.append(dwUnused1) # final uint32
if len(header_values) != 31:
raise RuntimeError("Internal error: DDSURFACEDESC2 must contain 31 uint32's")
# Pack and write DDSURFACEDESC2
dds_header = struct.pack("<31I", *header_values)
f.write(dds_header)
# If needed, write the DX10 header (DDS_HEADER_DXT10)
if not use_legacy:
# struct DDS_HEADER_DXT10 {
# DXGI_FORMAT dxgiFormat;
# D3D10_RESOURCE_DIMENSION resourceDimension;
# uint32 miscFlag;
# uint32 arraySize;
# uint32 miscFlags2;
# };
dxgiFormat = int(dxgi_format)
resourceDimension = D3D10_RESOURCE_DIMENSION.TEXTURE2D
miscFlag = 0
arraySize = 1
miscFlags2 = 0
dxt10_header = struct.pack(
"<5I",
dxgiFormat,
resourceDimension,
miscFlag,
arraySize,
miscFlags2,
)
f.write(dxt10_header)
# -----------------------------------------------------------------
# Write the actual texture data blocks (pBlocks)
# -----------------------------------------------------------------
# C code: fwrite(pBlocks, desc.lPitch, 1, pFile);
# i.e. write exactly lPitch bytes.
data = memoryview(blocks)
if len(data) < lPitch:
raise ValueError(
f"blocks buffer too small: need at least {lPitch} bytes, got {len(data)}"
)
f.write(data[:lPitch])
except Exception as e:
# Mimic the C-style error reporting as much as practical
print(f"Failed writing to DDS file {filename}: {e}", file=sys.stderr)
try:
f.close()
except Exception:
pass
return False
# Close file
try:
f.close()
except OSError:
print(f"Failed closing DDS file {filename}!", file=sys.stderr)
return False
return True

413
python/explode_ktx2_file.py Normal file
View File

@@ -0,0 +1,413 @@
#!/usr/bin/env python3
"""
explode_ktx2_file.py
FULL LDR/HDR KTX2 EXPLODER + FULL API INTROSPECTION + ASTC + BC7/BC6H OUTPUT
Usage:
python3 explode_ktx2_file.py input.ktx2
python3 explode_ktx2_file.py input.ktx2 --info-only
"""
# Python Dependencies (beyond basisu_py):
# numpy
# pillow
# imageio (v3+)
# wasmtime
#
# System Dependencies:
# OpenImageIO ("oiiotool") -- required for EXR output
#
# Install Python deps:
# pip install numpy pillow imageio wasmtime
#
# On Ubuntu:
# sudo apt install openimageio-tools
#
# On macOS (Homebrew):
# brew install openimageio
import sys
import os
import numpy as np
import subprocess
import tempfile
import imageio.v3 as iio
from PIL import Image
from basisu_py import Transcoder
from basisu_py.constants import TranscoderTextureFormat as TF
# Writers located in same directory as this script
from astc_writer import write_astc_file
from dds_writer import DDSWriter
# ============================================================================
# File-writing helpers
# ============================================================================
def save_exr(path, rgba32f):
"""
Save float32 RGBA as EXR if possible.
If oiiotool is not available, save TIFF instead (Windows-safe).
"""
import numpy as np
import imageio.v3 as iio
import subprocess, tempfile, os
# Write temp TIFF
with tempfile.NamedTemporaryFile(suffix=".tiff", delete=False) as tmp:
temp_path = tmp.name
iio.imwrite(temp_path, rgba32f.astype(np.float32))
# Try EXR via oiiotool
try:
subprocess.run(["oiiotool", temp_path, "-o", path], check=True)
os.remove(temp_path)
print(" Wrote EXR:", path)
return
except Exception:
# --- FALLBACK: save TIFF ---
fallback_path = path + ".tiff"
# Windows cannot overwrite files via rename(), so remove first
if os.path.exists(fallback_path):
os.remove(fallback_path)
# os.replace() always overwrites
os.replace(temp_path, fallback_path)
print(" [Fallback] Wrote TIFF instead:", fallback_path)
def save_png(path, rgba8):
img = Image.fromarray(rgba8, mode="RGBA")
img.save(path)
print(f" PNG saved: {path}")
# ============================================================================
# Pretty header
# ============================================================================
def print_header(title):
print("\n" + "=" * 90)
print(title)
print("=" * 90)
# ============================================================================
# Full top-level metadata dump (ALL API)
# ============================================================================
def dump_all_top_level(t, h):
print_header("TOP-LEVEL KTX2 METADATA FULL API")
print("Backend :", t.backend_name)
print("Version :", t.get_version())
print("Width :", t.get_width(h))
print("Height :", t.get_height(h))
print("Levels :", t.get_levels(h))
print("Faces :", t.get_faces(h))
layers = t.get_layers(h)
eff_layers = layers if layers > 0 else 1
print("Layers (raw) :", layers)
print("Layers (effective) :", eff_layers)
fmt = t.get_basis_tex_format(h)
print("\nBasisTexFormat :", fmt)
print("\nKTX2 Format Flags:")
print(" is_etc1s :", t.is_etc1s(h))
print(" is_uastc_ldr_4x4 :", t.is_uastc_ldr_4x4(h))
print(" is_xuastc_ldr :", t.is_xuastc_ldr(h))
print(" is_astc_ldr :", t.is_astc_ldr(h))
print(" is_hdr :", t.is_hdr(h))
print(" is_hdr_4x4 :", t.is_hdr_4x4(h))
print(" is_hdr_6x6 :", t.is_hdr_6x6(h))
print(" is_ldr :", t.is_ldr(h))
print(" is_srgb :", t.is_srgb(h))
print(" is_video :", t.is_video(h))
print(" has_alpha :", t.has_alpha(h))
print("\nBlock Info:")
print(" block_width :", t.get_block_width(h))
print(" block_height :", t.get_block_height(h))
print("\nDFD Info:")
print(" color_model :", t.get_dfd_color_model(h))
print(" color_primaries :", t.get_dfd_color_primaries(h))
print(" transfer_func :", t.get_dfd_transfer_func(h))
print(" flags :", t.get_dfd_flags(h))
print(" total_samples :", t.get_dfd_total_samples(h))
print(" channel_id0 :", t.get_dfd_channel_id0(h))
print(" channel_id1 :", t.get_dfd_channel_id1(h))
if t.is_hdr(h):
print(" hdr_nit_multiplier :", t.get_ldr_hdr_upconversion_nit_multiplier(h))
# ============================================================================
# BasisTexFormat helpers
# ============================================================================
def dump_basis_tex_format_helpers(t, h):
print_header("BasisTexFormat HELPERS (FULL)")
fmt = t.get_basis_tex_format(h)
print("basis_tex_format:", fmt)
print("is_xuastc_ldr :", t.basis_tex_format_is_xuastc_ldr(fmt))
print("is_astc_ldr :", t.basis_tex_format_is_astc_ldr(fmt))
print("block width :", t.basis_tex_format_get_block_width(fmt))
print("block height :", t.basis_tex_format_get_block_height(fmt))
print("is_hdr :", t.basis_tex_format_is_hdr(fmt))
print("is_ldr :", t.basis_tex_format_is_ldr(fmt))
# ============================================================================
# Level / Layer / Face metadata dump
# ============================================================================
def dump_per_level_info(t, h):
print_header("PER-LEVEL / PER-LAYER / PER-FACE METADATA")
levels = t.get_levels(h)
faces = t.get_faces(h)
layers = t.get_layers(h)
if layers == 0:
layers = 1
for level in range(levels):
for layer in range(layers):
for face in range(faces):
print(f"\nLevel={level}, Layer={layer}, Face={face}")
print(" orig_width :", t.get_level_orig_width(h, level, layer, face))
print(" orig_height :", t.get_level_orig_height(h, level, layer, face))
print(" actual_width :", t.get_level_actual_width(h, level, layer, face))
print(" actual_height:", t.get_level_actual_height(h, level, layer, face))
print(" blocks_x :", t.get_level_num_blocks_x(h, level, layer, face))
print(" blocks_y :", t.get_level_num_blocks_y(h, level, layer, face))
print(" total_blocks :", t.get_level_total_blocks(h, level, layer, face))
print(" alpha_flag :", t.get_level_alpha_flag(h, level, layer, face))
print(" iframe_flag :", t.get_level_iframe_flag(h, level, layer, face))
# ============================================================================
# ASTC Selection
# ============================================================================
def choose_astc_format(t, h):
fmt = t.get_basis_tex_format(h)
tfmt = t.basis_get_transcoder_texture_format_from_basis_tex_format(fmt)
bw = t.basis_get_block_width(tfmt)
bh = t.basis_get_block_height(tfmt)
print_header("ASTC SELECTION")
print("ASTC TF:", tfmt)
print(f"Block dims: {bw}x{bh}")
return tfmt, bw, bh
# ============================================================================
# BC Format Selection
# ============================================================================
def choose_bc_format(t, h):
if t.is_hdr(h):
print_header("HDR -> BC6H")
return TF.TF_BC6H, 8, 95 # DXGI_FORMAT_BC6H_UF16
else:
print_header("LDR -> BC7")
return TF.TF_BC7_RGBA, 8, 98 # DXGI_FORMAT_BC7_UNORM
# ============================================================================
# Full explode transcoding (using handle API + per-level dims)
# ============================================================================
def explode_transcode(t, h):
levels = t.get_levels(h)
faces = t.get_faces(h)
layers = t.get_layers(h)
if layers == 0:
layers = 1
astc_tfmt, astc_bw, astc_bh = choose_astc_format(t, h)
bc_tfmt, bc_bpp, bc_dxgi = choose_bc_format(t, h)
ddsw = DDSWriter()
print_header("BEGIN EXPLODE TRANSCODING (handle API)")
for level in range(levels):
for layer in range(layers):
for face in range(faces):
print(f"\n- Level={level} Layer={layer} Face={face}")
ow = t.get_level_orig_width(h, level, layer, face)
oh = t.get_level_orig_height(h, level, layer, face)
print(f" Level orig dims: {ow}x{oh}")
# ASTC
astc_blocks = t.transcode_tfmt_handle(
h, astc_tfmt,
level=level, layer=layer, face=face,
decode_flags=0, channel0=-1, channel1=-1
)
astc_name = f"astc_L{level}_Y{layer}_F{face}.astc"
write_astc_file(astc_name, astc_blocks, astc_bw, astc_bh, ow, oh)
print(" ASTC saved:", astc_name)
# BC6H / BC7
bc_blocks = t.transcode_tfmt_handle(
h, bc_tfmt,
level=level, layer=layer, face=face,
decode_flags=0, channel0=-1, channel1=-1
)
if t.is_hdr(h):
dds_name = f"bc6h_L{level}_Y{layer}_F{face}.dds"
else:
dds_name = f"bc7_L{level}_Y{layer}_F{face}.dds"
ddsw.save_dds(
dds_name,
width=ow, height=oh,
blocks=bc_blocks,
pixel_format_bpp=bc_bpp,
dxgi_format=bc_dxgi,
srgb=False,
force_dx10_header=True,
)
print(" DDS saved :", dds_name)
print_header("EXPLODE TRANSCODING COMPLETE")
# ============================================================================
# Decode each (Level, Layer, Face) to PNG or EXR
# ============================================================================
def explode_decode_images(t, h):
print_header("BEGIN EXPLODE IMAGE DECODE (PNG/EXR)")
levels = t.get_levels(h)
faces = t.get_faces(h)
layers = t.get_layers(h)
if layers == 0:
layers = 1
hdr = t.is_hdr(h)
for level in range(levels):
for layer in range(layers):
for face in range(faces):
print(f"\n- Decode Level={level} Layer={layer} Face={face}")
ow = t.get_level_orig_width(h, level, layer, face)
oh = t.get_level_orig_height(h, level, layer, face)
if hdr:
rgba32f = t.decode_rgba_hdr_handle(h, level, layer, face)
outname = f"exr_L{level}_Y{layer}_F{face}.exr"
save_exr(outname, rgba32f)
else:
rgba8 = t.decode_rgba_handle(h, level, layer, face)
outname = f"png_L{level}_Y{layer}_F{face}.png"
save_png(outname, rgba8)
print_header("IMAGE DECODE COMPLETE")
def dump_transcoder_texture_format_helpers(t):
print_header("TranscoderTextureFormat HELPERS (FULL)")
test_formats = [
# uncompressed
TF.TF_RGBA32, TF.TF_RGB565, TF.TF_BGR565,
TF.TF_RGBA4444, TF.TF_RGB_HALF, TF.TF_RGBA_HALF, TF.TF_RGB_9E5,
# basic compressed
TF.TF_ETC1_RGB, TF.TF_ETC2_RGBA,
TF.TF_BC1_RGB, TF.TF_BC3_RGBA,
TF.TF_BC4_R, TF.TF_BC5_RG,
TF.TF_BC7_RGBA, TF.TF_BC6H,
TF.TF_ETC2_EAC_R11, TF.TF_ETC2_EAC_RG11,
TF.TF_FXT1_RGB,
TF.TF_PVRTC1_4_RGB, TF.TF_PVRTC1_4_RGBA,
TF.TF_PVRTC2_4_RGB, TF.TF_PVRTC2_4_RGBA,
TF.TF_ATC_RGB, TF.TF_ATC_RGBA,
# HDR ASTC
TF.TF_ASTC_HDR_4X4_RGBA,
TF.TF_ASTC_HDR_6X6_RGBA,
# LDR ASTC
TF.TF_ASTC_LDR_4X4_RGBA,
TF.TF_ASTC_LDR_5X4_RGBA, TF.TF_ASTC_LDR_5X5_RGBA,
TF.TF_ASTC_LDR_6X5_RGBA, TF.TF_ASTC_LDR_6X6_RGBA,
TF.TF_ASTC_LDR_8X5_RGBA, TF.TF_ASTC_LDR_8X6_RGBA,
TF.TF_ASTC_LDR_10X5_RGBA, TF.TF_ASTC_LDR_10X6_RGBA,
TF.TF_ASTC_LDR_8X8_RGBA, TF.TF_ASTC_LDR_10X8_RGBA,
TF.TF_ASTC_LDR_10X10_RGBA, TF.TF_ASTC_LDR_12X10_RGBA,
TF.TF_ASTC_LDR_12X12_RGBA,
]
for tfmt in test_formats:
print(f"\nTF={tfmt}")
print(" has_alpha :", t.basis_transcoder_format_has_alpha(tfmt))
print(" is_hdr :", t.basis_transcoder_format_is_hdr(tfmt))
print(" is_ldr :", t.basis_transcoder_format_is_ldr(tfmt))
print(" is_astc :", t.basis_transcoder_texture_format_is_astc(tfmt))
print(" is_uncompressed :", t.basis_transcoder_format_is_uncompressed(tfmt))
print(" bytes/block :", t.basis_get_bytes_per_block_or_pixel(tfmt))
print(" block_width :", t.basis_get_block_width(tfmt))
print(" block_height :", t.basis_get_block_height(tfmt))
def main():
if len(sys.argv) < 2:
print("Usage: python explode_ktx2_file.py input.ktx2 [--info-only] [--print-tf]")
return 1
args = sys.argv[1:]
info_only = "--info-only" in args
print_tf = "--print-tf" in args or "--transcoder-formats" in args
# Determine input filename
input_file = None
for a in args:
if not a.startswith("--"):
input_file = a
break
if input_file is None:
print("Error: No input file provided.")
return 1
ktx_bytes = open(input_file, "rb").read()
t = Transcoder()
h = t.open(ktx_bytes)
t.start_transcoding(h)
# Full metadata
dump_all_top_level(t, h)
dump_basis_tex_format_helpers(t, h)
dump_per_level_info(t, h)
# Optional TF helpers
if print_tf:
dump_transcoder_texture_format_helpers(t)
if info_only:
print_header("INFO-ONLY MODE NO FILES WRITTEN")
t.close(h)
return 0
# Full output
explode_transcode(t, h)
explode_decode_images(t, h)
t.close(h)
print("Success")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1 @@
# __init__.py

View File

@@ -0,0 +1,127 @@
# basic_test.py
import sys
sys.path.append("basisu_py") # make sure Python can load the .so
import basisu_python as bu
from constants import *
import ctypes
import math
def generate_swirl_rgba8(width, height):
"""
Generate a smooth colorful swirl procedural RGBA8 test image.
Returns: a ctypes array of type (c_ubyte * (width * height * 4))
"""
pixel_count = width * height * 4
img = (ctypes.c_ubyte * pixel_count)()
for y in range(height):
for x in range(width):
i = (y * width + x) * 4
dx = x - width / 2
dy = y - height / 2
dist = math.hypot(dx, dy)
angle = math.atan2(dy, dx)
# Color swirl pattern
r = int((math.sin(dist * 0.15) * 0.5 + 0.5) * 255)
g = int((math.sin(angle * 3.0) * 0.5 + 0.5) * 255)
b = int((math.cos(dist * 0.10 + angle * 2.0) * 0.5 + 0.5) * 255)
img[i + 0] = r & 255
img[i + 1] = g & 255
img[i + 2] = b & 255
img[i + 3] = 255
return img
def generate_test_pattern_rgba8(width, height):
"""
Generate a simple deterministic RGBA8 test pattern:
R = x
G = y
B = x^y
A = 255
"""
import ctypes
pixel_count = width * height * 4
img = (ctypes.c_ubyte * pixel_count)()
for y in range(height):
for x in range(width):
i = (y * width + x) * 4
img[i + 0] = x & 0xFF
img[i + 1] = y & 0xFF
img[i + 2] = (x ^ y) & 0xFF
img[i + 3] = 255
return img
# ------------------------------------------------------------
# BasisU compression test (NATIVE C++)
# ------------------------------------------------------------
print("Native BasisU version:", bu.get_version())
bu.init()
# Create comp params
params = bu.new_params()
print("Params handle:", params)
# Create RGBA8 swirl (64 x 64)
W, H = 512, 512
pixel_count = W * H * 4
# Generate swirl image in PYTHON memory
img = generate_swirl_rgba8(W, H)
#img = generate_test_pattern_rgba8(W, H)
# Allocate memory inside NATIVE C++ heap
img_ptr = bu.alloc(pixel_count)
# Copy Python swirl image ? C++ heap buffer
ctypes.memmove(img_ptr, img, pixel_count)
# Set into BasisU
pitch = W * 4
ok = bu.set_image_rgba32(params, 0, img_ptr, W, H, pitch)
print("Set image:", ok)
# Compress (UASTC LDR 4x4 = 1)
ok = bu.compress(
params,
BasisTexFormat.cASTC_LDR_4x4, # basis_tex_format
BasisQuality.MAX, # quality
BasisEffort.DEFAULT, # effort
BasisFlags.KTX2_OUTPUT | BasisFlags.SRGB | BasisFlags.THREADED | BasisFlags.DEBUG_OUTPUT | BasisFlags.VERBOSE, # flags
0.0 # rdo
)
print("Compress:", ok)
# Retrieve compressed data
size = bu.get_comp_data_size(params)
ofs = bu.get_comp_data_ofs(params)
print("Output size =", size, "ptr =", ofs)
# Copy bytes out of native memory
byte_ptr = ctypes.cast(ofs, ctypes.POINTER(ctypes.c_ubyte))
blob = bytes(byte_ptr[i] for i in range(size))
print("First 16 bytes:", blob[:16])
# Save to KTX2
with open("out_native.ktx2", "wb") as f:
f.write(blob)
print("Saved out_native.ktx2")
# Cleanup
bu.delete_params(params)
bu.free(img_ptr)

View File

@@ -0,0 +1,481 @@
#!/usr/bin/env python3
# example_capi_python.py
#
# Simple Python port of example_capi.c using native C++ pybind11 bindings:
# - basisu_python (encoder)
# - basisu_transcoder_python (transcoder)
#
# Requires:
# basisu_py/basisu_python*.so
# basisu_py/basisu_transcoder_python*.so
# basisu_py/constants.py
import sys
import os
import math
import ctypes
# Make sure Python can see the native .so's and the shared constants
sys.path.append("basisu_py")
import basisu_python as bu
import basisu_transcoder_python as bt
from constants import BasisTexFormat, BasisFlags
from constants import TranscoderTextureFormat as TF
from constants import TranscodeDecodeFlags as DF
TRUE = 1
FALSE = 0
# ------------------------------------------------------------
# Utility: write raw bytes to a file
# ------------------------------------------------------------
def write_blob_to_file(filename: str, data: bytes) -> int:
print(f"write_blob_to_file: writing {len(data)} bytes to {filename!r}")
if not filename or data is None:
print(" ERROR: invalid filename or data")
return FALSE
try:
with open(filename, "wb") as f:
f.write(data)
print(" OK")
return TRUE
except OSError as e:
print(" ERROR:", e)
return FALSE
# ------------------------------------------------------------
# TGA writer (24/32bpp) - port of write_tga_image()
# ------------------------------------------------------------
def write_tga_image(filename: str, w: int, h: int, has_alpha: bool, pixels_rgba_ptr: int) -> int:
"""
filename: path to TGA file
w, h: image dimensions
has_alpha: True for 32bpp, False for 24bpp
pixels_rgba_ptr: C pointer (uint64) to RGBA or RGB data in native heap
"""
print(f"write_tga_image: {filename!r}, {w}x{h}, has_alpha={has_alpha}, ptr=0x{pixels_rgba_ptr:x}")
if not filename or pixels_rgba_ptr == 0 or w <= 0 or h <= 0:
print(" ERROR: invalid args")
return -1
bytes_per_pixel = 4 if has_alpha else 3
row_bytes = w * bytes_per_pixel
total_bytes = row_bytes * h
# Create a ctypes buffer that views the native memory
SrcArrayType = ctypes.c_ubyte * total_bytes
src = SrcArrayType.from_address(pixels_rgba_ptr)
try:
with open(filename, "wb") as f:
header = bytearray(18)
header[2] = 2 # uncompressed true-color
header[12] = w & 0xFF
header[13] = (w >> 8) & 0xFF
header[14] = h & 0xFF
header[15] = (h >> 8) & 0xFF
header[16] = 32 if has_alpha else 24
header[17] = 8 if has_alpha else 0 # bottom-left origin (with or without alpha)
f.write(header)
# temp row buffer for BGRA/BGR
row_buf = bytearray(row_bytes)
# TGA expects rows bottom-to-top
for y in range(h):
src_y = h - 1 - y
row_start = src_y * row_bytes
src_row = src[row_start:row_start + row_bytes]
if has_alpha:
# RGBA -> BGRA
for x in range(w):
si = x*4
di = x*4
row_buf[di + 0] = src_row[si + 2] # B
row_buf[di + 1] = src_row[si + 1] # G
row_buf[di + 2] = src_row[si + 0] # R
row_buf[di + 3] = src_row[si + 3] # A
else:
# RGB -> BGR
for x in range(w):
si = x*3
di = x*3
row_buf[di + 0] = src_row[si + 2] # B
row_buf[di + 1] = src_row[si + 1] # G
row_buf[di + 2] = src_row[si + 0] # R
f.write(row_buf)
print(" Wrote TGA:", filename)
return 0
except OSError as e:
print(" ERROR writing TGA:", e)
return -2
# ------------------------------------------------------------
# ASTC writer - port of write_astc_file()
# ------------------------------------------------------------
def write_astc_file(filename: str,
blocks_ptr: int,
block_width: int,
block_height: int,
dim_x: int,
dim_y: int) -> int:
print(f"write_astc_file: {filename!r}, block={block_width}x{block_height}, dim={dim_x}x{dim_y}, ptr=0x{blocks_ptr:x}")
if not filename or blocks_ptr == 0:
print(" ERROR: invalid filename or pointer")
return 0
assert dim_x > 0 and dim_y > 0
assert 4 <= block_width <= 12
assert 4 <= block_height <= 12
num_blocks_x = (dim_x + block_width - 1) // block_width
num_blocks_y = (dim_y + block_height - 1) // block_height
total_blocks = num_blocks_x * num_blocks_y
total_bytes = total_blocks * 16 # 16 bytes per ASTC block
print(f" num_blocks_x={num_blocks_x}, num_blocks_y={num_blocks_y}, total_blocks={total_blocks}, total_bytes={total_bytes}")
# View native memory
BlockArray = ctypes.c_ubyte * total_bytes
src = BlockArray.from_address(blocks_ptr)
try:
with open(filename, "wb") as f:
# Magic
f.write(bytes([0x13, 0xAB, 0xA1, 0x5C]))
# Block dimensions x,y,z (=1)
f.write(bytes([block_width & 0xFF, block_height & 0xFF, 1]))
# dim_x (24-bit LE)
f.write(bytes([dim_x & 0xFF, (dim_x >> 8) & 0xFF, (dim_x >> 16) & 0xFF]))
# dim_y (24-bit LE)
f.write(bytes([dim_y & 0xFF, (dim_y >> 8) & 0xFF, (dim_y >> 16) & 0xFF]))
# dim_z = 1 (24-bit LE)
f.write(bytes([1, 0, 0]))
# Block data
f.write(bytes(src))
print(" Wrote ASTC:", filename)
return 1
except OSError as e:
print(" ERROR writing ASTC:", e)
return 0
# ------------------------------------------------------------
# Procedural RGBA pattern (ported & fixed version)
# ------------------------------------------------------------
def create_pretty_rgba_pattern(w: int, h: int) -> bytes:
print(f"create_pretty_rgba_pattern: {w}x{h}")
if w <= 0 or h <= 0:
return None
out = bytearray(w * h * 4)
for y in range(h):
for x in range(w):
fx = x / float(w)
fy = y / float(h)
# Colorful plasma-type formula
v = math.sin(fx * 12.0 + fy * 4.0)
v += math.sin(fy * 9.0 - fx * 6.0)
v += math.sin((fx + fy) * 7.0)
v = v * 0.25 + 0.5 # scale 0..1
L = 1.5
r = int(round(255.0 * math.sin(v * 6.28) * L))
g = int(round(255.0 * (1.0 - v) * L))
b = int(round(255.0 * v * L))
if r < 0: r = 0
elif r > 255: r = 255
if g < 0: g = 0
elif g > 255: g = 255
if b < 0: b = 0
elif b > 255: b = 255
i = (y * w + x) * 4
out[i+0] = r
out[i+1] = g
out[i+2] = b
out[i+3] = 255
return bytes(out)
# ------------------------------------------------------------
# Transcode a KTX2 blob (ported from transcode_ktx2_file)
# ------------------------------------------------------------
def transcode_ktx2_file(ktx2_data: bytes) -> int:
if not ktx2_data:
print("transcode_ktx2_file: empty data")
return FALSE
size = len(ktx2_data)
print(f"transcode_ktx2_file: size={size} bytes")
if size > 0xFFFFFFFF:
print(" ERROR: size too large for 32-bit length")
return FALSE
# Allocate memory in transcoder heap and copy KTX2 data
ktx2_data_ofs = bt.alloc(size)
if not ktx2_data_ofs:
print(" ERROR: bt.alloc failed")
return FALSE
print(f" KTX2 data allocated at 0x{ktx2_data_ofs:x}")
ctypes.memmove(ktx2_data_ofs, ktx2_data, size)
# Open KTX2
ktx2_handle = bt.ktx2_open(ktx2_data_ofs, size)
if not ktx2_handle:
print(" ERROR: bt.ktx2_open failed")
bt.free(ktx2_data_ofs)
return FALSE
print(f" KTX2 handle = 0x{ktx2_handle:x}")
if not bt.ktx2_is_ldr(ktx2_handle):
print(" ERROR: This sample only handles LDR KTX2 files")
bt.ktx2_close(ktx2_handle)
bt.free(ktx2_data_ofs)
return FALSE
if not bt.ktx2_start_transcoding(ktx2_handle):
print(" ERROR: bt.ktx2_start_transcoding failed")
bt.ktx2_close(ktx2_handle)
bt.free(ktx2_data_ofs)
return FALSE
width = bt.ktx2_get_width(ktx2_handle)
height = bt.ktx2_get_height(ktx2_handle)
levels = bt.ktx2_get_levels(ktx2_handle)
faces = bt.ktx2_get_faces(ktx2_handle)
layers = bt.ktx2_get_layers(ktx2_handle)
basis_tex_format = bt.ktx2_get_basis_tex_format(ktx2_handle)
block_width = bt.ktx2_get_block_width(ktx2_handle)
block_height = bt.ktx2_get_block_height(ktx2_handle)
is_srgb = bt.ktx2_is_srgb(ktx2_handle)
print(f"KTX2 Dimensions: {width}x{height}, Levels={levels}, Faces={faces}, Layers={layers}")
print(f"basis_tex_format: {basis_tex_format}")
print(f"Block dimensions: {block_width}x{block_height}")
print(f"is sRGB: {is_srgb}")
if layers < 1:
layers = 1
assert width >= 1 and height >= 1
assert levels >= 1
assert faces in (1, 6)
# Optional: separate transcode state (thread-local)
trans_state = bt.ktx2_create_transcode_state()
print(f"trans_state handle = 0x{trans_state:x}")
for level_index in range(levels):
for layer_index in range(layers):
for face_index in range(faces):
print(f"- Level {level_index}, layer {layer_index}, face {face_index}")
ow = bt.ktx2_get_level_orig_width(ktx2_handle, level_index, layer_index, face_index)
oh = bt.ktx2_get_level_orig_height(ktx2_handle, level_index, layer_index, face_index)
aw = bt.ktx2_get_level_actual_width(ktx2_handle, level_index, layer_index, face_index)
ah = bt.ktx2_get_level_actual_height(ktx2_handle, level_index, layer_index, face_index)
nbx = bt.ktx2_get_level_num_blocks_x(ktx2_handle, level_index, layer_index, face_index)
nby = bt.ktx2_get_level_num_blocks_y(ktx2_handle, level_index, layer_index, face_index)
tblocks = bt.ktx2_get_level_total_blocks(ktx2_handle, level_index, layer_index, face_index)
alpha_flag = bt.ktx2_get_level_alpha_flag(ktx2_handle, level_index, layer_index, face_index)
iframe_flag = bt.ktx2_get_level_iframe_flag(ktx2_handle, level_index, layer_index, face_index)
print(f" Orig dimensions: {ow}x{oh}, actual: {aw}x{ah}")
print(f" Block dims: {nbx}x{nby}, total blocks: {tblocks}")
print(f" Alpha={alpha_flag}, I-frame={iframe_flag}")
# 1) Transcode to RGBA32 and write TGA
tga_name = f"transcoded_{level_index}_{layer_index}_{face_index}.tga"
trans_size_rgba = bt.basis_compute_transcoded_image_size_in_bytes(TF.TF_RGBA32, ow, oh)
assert trans_size_rgba > 0
rgba_ofs = bt.alloc(trans_size_rgba)
print(f" RGBA buf ofs=0x{rgba_ofs:x}, size={trans_size_rgba}")
decode_flags = 0
ok = bt.ktx2_transcode_image_level(
ktx2_handle,
level_index, layer_index, face_index,
rgba_ofs,
trans_size_rgba,
TF.TF_RGBA32,
decode_flags,
0, 0, -1, -1,
trans_state
)
print(" ktx2_transcode_image_level(RGBA32):", ok)
if not ok:
bt.free(rgba_ofs)
bt.ktx2_destroy_transcode_state(trans_state)
bt.ktx2_close(ktx2_handle)
bt.free(ktx2_data_ofs)
return FALSE
write_tga_image(tga_name, ow, oh, True, rgba_ofs)
bt.free(rgba_ofs)
# 2) Transcode to ASTC and write .astc file
astc_name = f"transcoded_{level_index}_{layer_index}_{face_index}.astc"
target_tf = bt.basis_get_transcoder_texture_format_from_basis_tex_format(basis_tex_format)
print(f" Target ASTC TF={target_tf}")
trans_size_astc = bt.basis_compute_transcoded_image_size_in_bytes(target_tf, ow, oh)
assert trans_size_astc > 0
astc_ofs = bt.alloc(trans_size_astc)
print(f" ASTC buf ofs=0x{astc_ofs:x}, size={trans_size_astc}")
ok = bt.ktx2_transcode_image_level(
ktx2_handle,
level_index, layer_index, face_index,
astc_ofs,
trans_size_astc,
target_tf,
0, 0, 0, -1, -1,
trans_state
)
print(" ktx2_transcode_image_level(ASTC):", ok)
if not ok:
bt.free(astc_ofs)
bt.ktx2_destroy_transcode_state(trans_state)
bt.ktx2_close(ktx2_handle)
bt.free(ktx2_data_ofs)
return FALSE
write_astc_file(astc_name, astc_ofs, block_width, block_height, ow, oh)
bt.free(astc_ofs)
bt.ktx2_destroy_transcode_state(trans_state)
bt.ktx2_close(ktx2_handle)
bt.free(ktx2_data_ofs)
print("transcode_ktx2_file: success")
return TRUE
# ------------------------------------------------------------
# main() equivalent
# ------------------------------------------------------------
def main():
print("example_capi_python:")
# Init encoder (which initializes transcoder)
print("Calling bu.init() ...")
bu.init()
print("Calling bt.init() ...")
bt.init()
# Optional debug control if bound
if hasattr(bu, "enable_debug_printf"):
print("Disabling debug printf from encoder")
bu.enable_debug_printf(False)
# Generate test image
W, H = 512, 512
src_image = create_pretty_rgba_pattern(W, H)
if src_image is None:
print("ERROR: create_pretty_rgba_pattern failed")
return 1
# Save test image for inspection
print("Writing test_image.tga ...")
# use Python-level TGA writer by allocating a temporary native buffer
tmp_ofs = bt.alloc(len(src_image))
ctypes.memmove(tmp_ofs, src_image, len(src_image))
write_tga_image("test_image.tga", W, H, True, tmp_ofs)
bt.free(tmp_ofs)
# Compress to KTX2
print("Creating comp_params ...")
comp_params = bu.new_params()
print(" comp_params handle:", comp_params)
img_ofs = bu.alloc(W * H * 4)
print(f"Allocated encoder image buffer at 0x{img_ofs:x}")
ctypes.memmove(img_ofs, src_image, W * H * 4)
print("Calling bu.comp_params_set_image_rgba32(...)")
ok = bu.set_image_rgba32(comp_params, 0, img_ofs, W, H, W * 4)
print(" set_image_rgba32:", ok)
if not ok:
print("ERROR: bu_comp_params_set_image_rgba32 failed")
return 1
bu.free(img_ofs)
print("Compressing to XUASTC LDR 8x5 KTX2 ...")
basis_tex_format = BasisTexFormat.cXUASTC_LDR_8x5
quality_level = 85
effort_level = 2
flags = (BasisFlags.KTX2_OUTPUT |
BasisFlags.SRGB |
BasisFlags.THREADED |
BasisFlags.GEN_MIPS_CLAMP |
BasisFlags.PRINT_STATS |
BasisFlags.PRINT_STATUS)
ok = bu.compress(comp_params,
tex_format=basis_tex_format,
quality=quality_level,
effort=effort_level,
flags=flags,
rdo_quality=0.0)
print(" bu.compress:", ok)
if not ok:
print("ERROR: bu_compress_texture failed")
return 1
comp_size = bu.get_comp_data_size(comp_params)
print("Compressed size:", comp_size)
if comp_size == 0:
print("ERROR: bu_comp_params_get_comp_data_size failed")
return 1
comp_ofs = bu.get_comp_data_ofs(comp_params)
print(f"Compressed data ptr=0x{comp_ofs:x}")
# Copy compressed data into Python bytes
CompArray = ctypes.c_ubyte * comp_size
comp_buf = CompArray.from_address(comp_ofs)
comp_bytes = bytes(comp_buf)
print("Writing test.ktx2 ...")
if not write_blob_to_file("test.ktx2", comp_bytes):
print("ERROR: write_blob_to_file failed")
return 1
# Transcode using the native transcoder API
print("Now transcoding test.ktx2 via C API ...")
if not transcode_ktx2_file(comp_bytes):
print("ERROR: transcode_ktx2_file failed")
return 1
bu.delete_params(comp_params)
print("Success")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,24 @@
# test_transcoder_basic.py
import sys
import os
# Make sure Python can find the .so file
sys.path.append("basisu_py") # Adjust if needed
try:
import basisu_transcoder_python as bt
except ImportError as e:
print("Failed to import basisu_transcoder_python:", e)
raise
print("Successfully loaded basisu_transcoder_python")
# Call bt_get_version() via the pybind11 binding
try:
version = bt.get_version()
print("Transcoder version:", version)
except Exception as e:
print("Error calling bt_get_version:", e)
raise
print("Basic transcoder test complete.")

View File

@@ -0,0 +1 @@
# __init__.py

View File

@@ -0,0 +1,58 @@
import wasmtime
import ctypes
# --- Engine ---
engine = wasmtime.Engine()
# --- Store ---
store = wasmtime.Store(engine)
# --- WASI config ---
wasi = wasmtime.WasiConfig()
wasi.argv = ["basisu_module_st"]
wasi.inherit_stdout() # <-- tell WASI to use the host stdout
wasi.inherit_stderr()
store.set_wasi(wasi)
# --- Load module ---
module = wasmtime.Module.from_file(engine, "basisu_py/wasm/basisu_module_st.wasm")
# --- Linker + WASI ---
linker = wasmtime.Linker(engine)
linker.define_wasi()
# --- Instantiate ---
instance = linker.instantiate(store, module)
print("Single-threaded WASM instantiated OK")
# --- Exports ---
exports = instance.exports(store)
get_version = exports["bu_get_version"]
alloc = exports["bu_alloc"]
free = exports["bu_free"]
memory = exports["memory"]
# --- Version ---
version = get_version(store)
print("Version =", version)
# --- Alloc ---
ptr = alloc(store, 64)
print("Allocated ptr =", ptr)
# --- Access WASM memory properly ---
data_len = memory.data_len(store)
raw_ptr = memory.data_ptr(store) # ctypes pointer
addr = ctypes.addressof(raw_ptr.contents) # convert to integer pointer
# Create a byte array view into WASM memory
buf = (ctypes.c_ubyte * data_len).from_address(addr)
# Write TEST at allocated ptr
buf[ptr : ptr + 4] = b"TEST"
print("Wrote TEST into WASM memory.")
# --- Free ---
free(store, ptr)
print("Memory free OK.")

View File

@@ -0,0 +1,148 @@
# basisu_wasm.py
import wasmtime
import ctypes
import sys
sys.path.append("basisu_py") # our shared .py files
from constants import *
class BasisuWasm:
def __init__(self, path):
self.path = path
self.engine = None
self.store = None
self.memory = None
self.exports = None
# -----------------------------------------------
# Internal helper: build WASI + Wasmtime engine
# -----------------------------------------------
def _init_engine(self):
self.engine = wasmtime.Engine()
self.store = wasmtime.Store(self.engine)
wasi = wasmtime.WasiConfig()
wasi.argv = ["basisu"]
wasi.inherit_stdout()
wasi.inherit_stderr()
self.store.set_wasi(wasi)
return wasi
# -----------------------------------------------
# Create linker and instantiate WASM module
# -----------------------------------------------
def load(self):
self._init_engine()
module = wasmtime.Module.from_file(self.engine, self.path)
linker = wasmtime.Linker(self.engine)
linker.define_wasi()
instance = linker.instantiate(self.store, module)
self.exports = instance.exports(self.store)
self.memory = self.exports["memory"]
if "bu_init" in self.exports:
self.exports["bu_init"](self.store)
print("WASM loaded:", self.path)
# -----------------------------------------------
# Read/write WASM linear memory via ctypes
# -----------------------------------------------
def _wasm_buf(self):
raw_ptr = self.memory.data_ptr(self.store)
length = self.memory.data_len(self.store)
addr = ctypes.addressof(raw_ptr.contents)
return (ctypes.c_ubyte * length).from_address(addr)
# -----------------------------------------------
# Exported API accessors
# -----------------------------------------------
def init(self):
return self.exports["bu_init"](self.store)
def version(self):
return self.exports["bu_get_version"](self.store)
def alloc(self, size):
return self.exports["bu_alloc"](self.store, size)
def free(self, ptr):
return self.exports["bu_free"](self.store, ptr)
def new_params(self):
return self.exports["bu_new_comp_params"](self.store)
def delete_params(self, ptr):
return self.exports["bu_delete_comp_params"](self.store, ptr)
def set_image_rgba32(self, params, image_index, img_ptr, w, h, pitch):
return self.exports["bu_comp_params_set_image_rgba32"](
self.store, params, image_index, img_ptr, w, h, pitch
)
def set_image_float_rgba(self, params, image_index, img_ptr, w, h, pitch):
return self.exports["bu_comp_params_set_image_float_rgba"](
self.store, params, image_index, img_ptr, w, h, pitch
)
# Normally quality_level controls the quality.
# If quality_level==-1, then rdo_quality (a low-level parameter) directly
# controls each codec's quality setting. Normally set to 0.
def compress_texture_lowlevel(self, params,
tex_format,
quality_level,
effort_level,
flags_and_quality,
rdo_quality):
return self.exports["bu_compress_texture"](
self.store,
params,
tex_format,
quality_level,
effort_level,
flags_and_quality,
rdo_quality
)
def compress(self, params,
tex_format=BasisTexFormat.cUASTC_LDR_4x4,
quality=BasisQuality.MAX,
effort=BasisEffort.DEFAULT,
flags=BasisFlags.NONE,
rdo_quality=0.0):
return bool(self.compress_texture_lowlevel(
params,
tex_format,
quality,
effort,
flags,
rdo_quality
))
def get_comp_data_ofs(self, params):
return self.exports["bu_comp_params_get_comp_data_ofs"](self.store, params)
def get_comp_data_size(self, params):
return self.exports["bu_comp_params_get_comp_data_size"](self.store, params)
# -----------------------------------------------
# Copy bytes into WASM memory
# -----------------------------------------------
def write_bytes(self, wasm_ptr, data: bytes):
buf = self._wasm_buf()
buf[wasm_ptr:wasm_ptr+len(data)] = data
# -----------------------------------------------
# Read bytes from WASM memory
# -----------------------------------------------
def read_bytes(self, wasm_ptr, size):
buf = self._wasm_buf()
return bytes(buf[wasm_ptr:wasm_ptr+size])

View File

@@ -0,0 +1,63 @@
# compress_test.py
from .basisu_wasm import *
# === Load WASM ===
codec = BasisuWasm("basisu_py/wasm/basisu_module_st.wasm")
codec.load()
print("Version =", codec.version())
# === Build test image ===
W, H = 256, 256
BYTES_PER_PIXEL = 4
pitch = W * BYTES_PER_PIXEL
img = bytearray(W * H * 4)
for y in range(H):
for x in range(W):
i = (y * W + x) * 4
img[i + 0] = x & 0xFF # R
img[i + 1] = y & 0xFF # G
img[i + 2] = (x ^ y) & 0xFF # B
img[i + 3] = 255 # A
# === Upload image to WASM memory ===
img_ptr = codec.alloc(len(img))
codec.write_bytes(img_ptr, img)
# === Create comp_params ===
params = codec.new_params()
# === Set image into comp_params ===
ok = codec.set_image_rgba32(params, 0, img_ptr, W, H, pitch)
print("Set image:", ok)
# === Compress ===
ok = codec.compress(
params,
tex_format=BasisTexFormat.cUASTC_LDR_4x4,
quality=100,
effort=BasisEffort.DEFAULT,
flags=BasisFlags.KTX2_OUTPUT | BasisFlags.SRGB,
rdo_quality=0.0
)
print("Compress result:", ok)
# === Retrieve compressed blob ===
ofs = codec.get_comp_data_ofs(params)
size = codec.get_comp_data_size(params)
print("Output size =", size)
comp_data = codec.read_bytes(ofs, size)
print("First 16 bytes:", comp_data[:16])
# === Save to KTX2 ===
with open("test.ktx2", "wb") as f:
f.write(comp_data)
print("File written: test.ktx2")
# === Cleanup ===
codec.delete_params(params)
codec.free(img_ptr)

View File

@@ -0,0 +1,76 @@
# compress_test_float.py
from .basisu_wasm import BasisuWasm, BasisTexFormat, BasisEffort, BasisFlags, BasisQuality
import struct # for packing floats
# === Load WASM ===
codec = BasisuWasm("basisu_py/wasm/basisu_module_st.wasm")
codec.load()
print("Version =", codec.version())
# === Build a 256x256 FLOAT RGBA image ===
W, H = 256, 256
BYTES_PER_PIXEL = 16 # float32 * 4
pitch = W * BYTES_PER_PIXEL
# Float image stored as bytearray of packed floats
img = bytearray(W * H * BYTES_PER_PIXEL)
for y in range(H):
for x in range(W):
# Create some float HDR gradient pattern
r = float(x) / W # 0.0 ? 1.0
g = float(y) / H # 0.0 ? 1.0
b = float(x ^ y) / 255.0 # quirky pattern
a = 1.0
i = (y * W + x) * 4
# pack into img bytearray
struct.pack_into("ffff", img, i*4, r, g, b, a)
print("Created FLOAT RGBA image.")
# === Upload to WASM memory ===
img_ptr = codec.alloc(len(img))
codec.write_bytes(img_ptr, img)
print("Copied float image into WASM heap at", img_ptr)
# === Create params ===
params = codec.new_params()
# === Set FLOAT RGBA image ===
ok = codec.set_image_float_rgba(params, 0, img_ptr, W, H, pitch)
print("Set float RGBA:", ok)
# === Compress using HDR UASTC 4x4 ===
ok = codec.compress(
params,
tex_format=BasisTexFormat.cUASTC_HDR_4x4,
quality=BasisQuality.MAX,
effort=BasisEffort.DEFAULT,
flags=BasisFlags.KTX2_OUTPUT | BasisFlags.REC2020, # optional: HDR color space
rdo_quality=0.0
)
print("Compression result:", ok)
# === Retrieve compressed HDR KTX2 ===
ofs = codec.get_comp_data_ofs(params)
size = codec.get_comp_data_size(params)
print("Output size =", size)
data = codec.read_bytes(ofs, size)
print("First 16 bytes:", data[:16])
# === Save to test_hdr.ktx2 ===
with open("test_hdr.ktx2", "wb") as f:
f.write(data)
print("Wrote test_hdr.ktx2")
# === Cleanup ===
codec.delete_params(params)
codec.free(img_ptr)

44
python/pyproject.toml Normal file
View File

@@ -0,0 +1,44 @@
[build-system]
requires = ["setuptools>=65", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "basisu-py"
version = "0.2.0"
description = "Python bindings for Basis Universal encoder/transcoder v2.x with native + WASM backend"
authors = [
{ name = "Binomial LLC", email = "stephanie@binomial.info" }
]
license = { text = "Apache 2.0" }
readme = "README.md"
requires-python = ">=3.8"
dependencies = [
"numpy",
"Pillow",
"imageio>=2.22",
"wasmtime",
]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: C++",
"Operating System :: OS Independent",
"License :: OSI Approved :: Apache Software License",
]
[tool.setuptools]
include-package-data = true
[tool.setuptools.packages.find]
include = ["basisu_py*"]
[tool.setuptools.package-data]
basisu_py = [
"*.so",
"*.pyd",
"*.py",
"wasm/*.wasm",
"wasm/*.py",
"README.md",
]

1
python/tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
# python/tests/__init__.py

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python3
import numpy as np
from PIL import Image
from basisu_py.codec import Encoder, EncoderBackend
from basisu_py.constants import BasisTexFormat
print("========== BACKEND LOADING TEST ==========\n")
# --------------------------------------------------------------
# 1. Test native backend (if available)
# --------------------------------------------------------------
print("Testing native backend...")
try:
enc_native = Encoder(backend=EncoderBackend.NATIVE)
print(" [OK] Native backend loaded")
except Exception as e:
print(" [FAIL] Native backend failed to load:", e)
enc_native = None
# If native loaded, test very basic functionality
if enc_native:
try:
version = enc_native._native.get_version()
print(f" Native get_version() ? {version}")
ptr = enc_native._native.alloc(16)
print(f" Native alloc() returned ptr = {ptr}")
enc_native._native.free(ptr)
print(f" Native free() OK")
print(" [OK] Native basic operations working.\n")
except Exception as e:
print(" [FAIL] Native operations error:", e)
else:
print(" Skipping native basic operations.\n")
# --------------------------------------------------------------
# 2. Test WASM backend
# --------------------------------------------------------------
print("\nTesting WASM backend...")
try:
enc_wasm = Encoder(backend=EncoderBackend.WASM)
print(" [OK] WASM backend loaded")
except Exception as e:
print(" [FAIL] WASM backend failed to load:", e)
enc_wasm = None
# If WASM loaded, test basic methods
if enc_wasm and enc_wasm._wasm is not None:
try:
version = enc_wasm._wasm.get_version()
print(f" WASM get_version() ? {version}")
ptr = enc_wasm._wasm.alloc(16)
print(f" WASM alloc() returned ptr = {ptr}")
enc_wasm._wasm.free(ptr)
print(f" WASM free() OK")
print(" [OK] WASM basic operations working.\n")
except Exception as e:
print(" [FAIL] WASM operations error:", e)
else:
print(" Skipping WASM basic operations.\n")
print("\n========== DONE ==========\n")

View File

@@ -0,0 +1,7 @@
from basisu_py import Encoder
enc = Encoder() # AUTO mode
print("Encoder backend:", enc.backend)
print("Native loaded:", enc._native is not None)
print("WASM loaded:", enc._wasm is not None)
print("Version:", enc._native.get_version() if enc._native else enc._wasm.get_version())

View File

@@ -0,0 +1,19 @@
from basisu_py import Transcoder
from PIL import Image
import numpy as np
# Load input file
with open("test.ktx2", "rb") as f:
data = f.read()
# Decode (AUTO backend)
t = Transcoder()
rgba = t.decode_rgba(data) # returns HxWx4 uint8 NumPy array
print("Decoded:", rgba.shape, rgba.dtype)
# Convert to Pillow Image and save
img = Image.fromarray(rgba, mode="RGBA")
img.save("decoded.png")
print("Wrote decoded.png")

View File

@@ -0,0 +1,10 @@
from basisu_py import Transcoder
with open("test.ktx2", "rb") as f:
data = f.read()
t = Transcoder() # AUTO backend
img = t.decode_rgba(data)
print("Decoded shape:", img.shape)
print("dtype:", img.dtype)

View File

@@ -0,0 +1,6 @@
from basisu_py import Transcoder
from basisu_py.transcoder import TranscoderBackend
t = Transcoder(backend=TranscoderBackend.WASM)
print("Backend:", t.backend_name)
t.decode_rgba(open("test.ktx2","rb").read())

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env python3
import numpy as np
from PIL import Image
from math import sin, cos, atan2, hypot
from basisu_py.codec import Encoder, EncoderBackend
from basisu_py.constants import BasisTexFormat, BasisQuality, BasisEffort, BasisFlags
# --------------------------------------------------------------
# Procedural swirl pattern (RGBA8)
# --------------------------------------------------------------
def make_swirl_image(w=256, h=256):
arr = np.zeros((h, w, 4), dtype=np.uint8)
cx = w / 2.0
cy = h / 2.0
for y in range(h):
for x in range(w):
dx = x - cx
dy = y - cy
dist = hypot(dx, dy)
angle = atan2(dy, dx)
r = int((sin(dist * 0.15) * 0.5 + 0.5) * 255)
g = int((sin(angle * 3.0) * 0.5 + 0.5) * 255)
b = int((cos(dist * 0.10 + angle * 2.0) * 0.5 + 0.5) * 255)
arr[y, x] = (r, g, b, 255)
return arr
# --------------------------------------------------------------
# Test encode using a given backend
# --------------------------------------------------------------
def compress_swirl(backend, outfile):
print(f"\n========== Testing {backend} backend ==========")
# Build procedural image
swirl = make_swirl_image(256, 256)
print("Generated swirl image:", swirl.shape)
# Create encoder
enc = Encoder(backend=backend)
# Compress
blob = enc.compress(
swirl,
format=BasisTexFormat.cUASTC_LDR_4x4,
quality=BasisQuality.MAX,
effort=BasisEffort.DEFAULT,
flags=BasisFlags.KTX2_OUTPUT | BasisFlags.SRGB
)
print(f"Compressed blob size: {len(blob)} bytes")
# Save output
with open(outfile, "wb") as f:
f.write(blob)
print(f"Wrote: {outfile}")
print("==============================================")
# --------------------------------------------------------------
# Main
# --------------------------------------------------------------
if __name__ == "__main__":
# Test native backend
try:
compress_swirl(EncoderBackend.NATIVE, "swirl_native.ktx2")
except Exception as e:
print("Native backend ERROR:", e)
# Test WASM backend
try:
compress_swirl(EncoderBackend.WASM, "swirl_wasm.ktx2")
except Exception as e:
print("WASM backend ERROR:", e)

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env python3
import numpy as np
from math import sin, cos, atan2, hypot
from basisu_py.codec import Encoder, EncoderBackend
from basisu_py.constants import BasisTexFormat, BasisQuality, BasisEffort, BasisFlags
# --------------------------------------------------------------
# Procedural HDR swirl pattern (float32 RGBA)
# --------------------------------------------------------------
def make_hdr_swirl_image(w=256, h=256):
arr = np.zeros((h, w, 4), dtype=np.float32)
cx = w / 2.0
cy = h / 2.0
for y in range(h):
for x in range(w):
dx = x - cx
dy = y - cy
dist = hypot(dx, dy)
angle = atan2(dy, dx)
r = (sin(dist * 0.15) * 0.5 + 0.5)
g = (sin(angle * 3.0) * 0.5 + 0.5)
b = (cos(dist * 0.10 + angle * 2.0) * 0.5 + 0.5)
arr[y, x] = (r, g, b, 1.0) # full alpha
return arr
# --------------------------------------------------------------
# Test encode using a given backend
# --------------------------------------------------------------
def compress_hdr_swirl(backend, outfile):
print(f"\n========== Testing HDR {backend} backend ==========")
hdr = make_hdr_swirl_image(256, 256)
print("Generated HDR swirl image:", hdr.shape, hdr.dtype)
enc = Encoder(backend=backend)
blob = enc.compress(
hdr,
format=-1, # auto-select HDR (UASTC_HDR_4x4)
quality=BasisQuality.MAX,
effort=BasisEffort.DEFAULT,
flags=BasisFlags.KTX2_OUTPUT | BasisFlags.SRGB
)
print(f"Compressed blob size: {len(blob)} bytes")
with open(outfile, "wb") as f:
f.write(blob)
print(f"Wrote: {outfile}")
print("==============================================")
# --------------------------------------------------------------
# Main
# --------------------------------------------------------------
if __name__ == "__main__":
# Native backend
try:
compress_hdr_swirl(EncoderBackend.NATIVE, "hdr_swirl_native.ktx2")
except Exception as e:
print("Native HDR backend ERROR:", e)
# WASM backend
try:
compress_hdr_swirl(EncoderBackend.WASM, "hdr_swirl_wasm.ktx2")
except Exception as e:
print("WASM HDR backend ERROR:", e)

View File

@@ -0,0 +1,18 @@
from basisu_py import Transcoder
from astc_writer import write_astc_file
# Load a .ktx2
data = open("input.ktx2", "rb").read()
t = Transcoder()
# Transcode to ASTC
h = t.open(data)
bw = t.get_block_width(h) # or basis_get_block_width(astc_tfmt)
bh = t.get_block_height(h)
tfmt = t.basis_get_transcoder_texture_format_from_basis_tex_format(
t.get_basis_tex_format(h)
)
blocks = t.transcode_tfmt(data, tfmt)
write_astc_file("output.astc", blocks, bw, bh, t.get_width(h), t.get_height(h))
t.close(h)

View File

@@ -0,0 +1,72 @@
#!/usr/bin/env python3
import sys
from basisu_py.transcoder import Transcoder, TranscoderBackend
from basisu_py.constants import BasisTexFormat
print("========== TESTING TRANSCODER BACKENDS ==========\n")
# Load some test data (ensure test.ktx2 exists)
try:
test_data = open("test.ktx2", "rb").read()
print("[INFO] Loaded test.ktx2")
except FileNotFoundError:
print("[ERROR] test.ktx2 not found. Create one first via encoder tests.")
sys.exit(1)
# -------------------------------------------------------------------
# 1. Test NATIVE backend
# -------------------------------------------------------------------
print("\n--- Testing NATIVE transcoder backend ---")
try:
t_native = Transcoder(TranscoderBackend.NATIVE)
print(" [OK] Native backend loaded")
version = t_native.get_version()
print(f" Native get_version() = {version}")
# Open KTX2
raw = t_native.open(test_data)
print(" [OK] Opened KTX2 (native)")
# Query some basic properties
print(" Width :", t_native.get_width(raw))
print(" Height:", t_native.get_height(raw))
print(" Levels:", t_native.get_levels(raw))
# Cleanup
t_native.close(raw)
print(" [OK] Native transcoder basic operations working.")
except Exception as e:
print(" [FAIL] Native transcoder error:", e)
# -------------------------------------------------------------------
# 2. Test WASM backend
# -------------------------------------------------------------------
print("\n--- Testing WASM transcoder backend ---")
try:
t_wasm = Transcoder(TranscoderBackend.WASM)
print(" [OK] WASM backend loaded")
version = t_wasm.get_version()
print(f" WASM get_version() = {version}")
raw = t_wasm.open(test_data)
print(" [OK] Opened KTX2 (wasm)")
print(" Width :", t_wasm.get_width(raw))
print(" Height:", t_wasm.get_height(raw))
print(" Levels:", t_wasm.get_levels(raw))
t_wasm.close(raw)
print(" [OK] WASM transcoder basic operations working.")
except Exception as e:
print(" [FAIL] WASM transcoder error:", e)
print("\n========== DONE ==========")

View File

@@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""
Full end-to-end transcoder test with automatic fallback.
- Generates a swirl image
- Compresses it using native OR WASM (AUTO mode)
- Writes test.ktx2
- Decodes it using whichever backends are available:
* AUTO (native if present, otherwise WASM)
* Native (if available)
* WASM (if available)
- Produces PNG outputs for all successful backends
"""
import numpy as np
from math import sin, cos, atan2, hypot
from PIL import Image
import sys
from basisu_py.codec import Encoder, EncoderBackend
from basisu_py.transcoder import Transcoder, TranscoderBackend
from basisu_py.constants import (
BasisTexFormat,
BasisQuality,
BasisEffort,
BasisFlags,
)
# -------------------------------------------------------------------
# Create an RGBA swirl test image
# -------------------------------------------------------------------
def make_swirl(w=256, h=256):
arr = np.zeros((h, w, 4), dtype=np.uint8)
cx, cy = w / 2.0, h / 2.0
for y in range(h):
for x in range(w):
dx, dy = x - cx, y - cy
dist = hypot(dx, dy)
angle = atan2(dy, dx)
r = int((sin(dist * 0.15) * 0.5 + 0.5) * 255)
g = int((sin(angle * 3.0) * 0.5 + 0.5) * 255)
b = int((cos(dist * 0.10 + angle * 2.0) * 0.5 + 0.5) * 255)
arr[y, x] = (r, g, b, 255)
return arr
# -------------------------------------------------------------------
# Try loading transcoder with a backend, return (success, transcoder)
# -------------------------------------------------------------------
def try_transcoder(backend):
try:
t = Transcoder(backend)
print(f"[OK] Loaded transcoder backend '{backend}' ({t.backend_name})")
return True, t
except Exception as e:
print(f"[SKIP] Backend '{backend}' unavailable:", e)
return False, None
# -------------------------------------------------------------------
# Try loading encoder with a backend, return blob or None
# -------------------------------------------------------------------
def try_encoder(backend, img):
try:
enc = Encoder(backend)
print(f"[OK] Loaded encoder backend '{backend}' ({enc.backend_name})")
except Exception as e:
print(f"[SKIP] Encoder backend '{backend}' unavailable:", e)
return None
try:
print(f"[Test] Compressing swirl -> KTX2 using {enc.backend_name}...")
blob = enc.compress(
img,
format=-1,
quality=BasisQuality.MAX,
effort=BasisEffort.DEFAULT,
flags=BasisFlags.KTX2_OUTPUT | BasisFlags.SRGB
)
return blob
except Exception as e:
print(f"[FAIL] Compression failed on backend '{backend}':", e)
return None
# -------------------------------------------------------------------
# Decode blob with a given transcoder
# -------------------------------------------------------------------
def decode_with_backend(name, t, blob):
try:
rgba = t.decode_rgba(blob)
outname = f"decoded_{name}.png"
Image.fromarray(rgba, mode="RGBA").save(outname)
print(f" --> {name}: decoded successfully, wrote {outname}")
except Exception as e:
print(f" [FAIL] decode_rgba on backend '{name}':", e)
# -------------------------------------------------------------------
# Main test
# -------------------------------------------------------------------
if __name__ == "__main__":
print("========== BasisU End-to-End Compression & Transcoding Test ==========")
# -------------------------------------------------------
# Generate swirl test
# -------------------------------------------------------
img = make_swirl(256, 256)
print("[Test] Generated swirl:", img.shape)
# -------------------------------------------------------
# Try AUTO encoder (native if available, else WASM)
# -------------------------------------------------------
blob = try_encoder(EncoderBackend.AUTO, img)
if blob is None:
print("[FAIL] Could not encode using AUTO backend; aborting.")
sys.exit(1)
# Save test.ktx2
with open("test.ktx2", "wb") as f:
f.write(blob)
print("[Test] Wrote: test.ktx2")
# -------------------------------------------------------
# Test transcoding using AUTO
# -------------------------------------------------------
print("\n[Test] Decoding via AUTO backend...")
ok_auto, t_auto = try_transcoder(TranscoderBackend.AUTO)
if ok_auto:
decode_with_backend("auto", t_auto, blob)
# -------------------------------------------------------
# Test NATIVE explicitly (if available)
# -------------------------------------------------------
print("\n[Test] Decoding via NATIVE backend...")
ok_native, t_native = try_transcoder(TranscoderBackend.NATIVE)
if ok_native:
decode_with_backend("native", t_native, blob)
# -------------------------------------------------------
# Test WASM explicitly (if available)
# -------------------------------------------------------
print("\n[Test] Decoding via WASM backend...")
ok_wasm, t_wasm = try_transcoder(TranscoderBackend.WASM)
if ok_wasm:
decode_with_backend("wasm", t_wasm, blob)
print("\n========== DONE ==========")

View File

@@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""
HDR End-to-End Compression & Transcoding Test
Works on all platforms:
- native if available
- WASM fallback otherwise
"""
import numpy as np
from math import sin, cos, atan2, hypot
from PIL import Image
import subprocess
import tempfile
import os
import imageio.v3 as iio
from basisu_py.codec import Encoder, EncoderBackend
from basisu_py.transcoder import Transcoder, TranscoderBackend
from basisu_py.constants import (
BasisTexFormat,
BasisQuality,
BasisEffort,
BasisFlags
)
# -------------------------------------------------------------------
# Save EXR using TIFF temp + oiiotool (as required)
# -------------------------------------------------------------------
def save_exr(path, rgba32f):
"""
Save float32 RGBA as EXR if possible.
If oiiotool is not available, save TIFF instead (Windows-safe).
"""
import numpy as np
import imageio.v3 as iio
import subprocess, tempfile, os
# Write temp TIFF
with tempfile.NamedTemporaryFile(suffix=".tiff", delete=False) as tmp:
temp_path = tmp.name
iio.imwrite(temp_path, rgba32f.astype(np.float32))
# Try EXR via oiiotool
try:
subprocess.run(["oiiotool", temp_path, "-o", path], check=True)
os.remove(temp_path)
print(" Wrote EXR:", path)
return
except Exception:
# --- FALLBACK: save TIFF ---
fallback_path = path + ".tiff"
# Windows cannot overwrite files via rename(), so remove first
if os.path.exists(fallback_path):
os.remove(fallback_path)
# os.replace() always overwrites
os.replace(temp_path, fallback_path)
print(" [Fallback] Wrote TIFF instead:", fallback_path)
# -------------------------------------------------------------------
# Generate HDR swirl image (float32)
# -------------------------------------------------------------------
def make_swirl_hdr(w=256, h=256):
arr = np.zeros((h, w, 4), dtype=np.float32)
cx, cy = w / 2.0, h / 2.0
for y in range(h):
for x in range(w):
dx, dy = x - cx, y - cy
dist = hypot(dx, dy)
angle = atan2(dy, dx)
# HDR values range up to about 4.0
r = (sin(dist * 0.08) * 0.5 + 0.5) * 4.0
g = (sin(angle * 2.0) * 0.5 + 0.5) * 4.0
b = (cos(dist * 0.06 + angle * 1.5) * 0.5 + 0.5) * 4.0
arr[y, x] = (r, g, b, 1.0)
return arr
# -------------------------------------------------------------------
# Try loading a transcoder backend
# -------------------------------------------------------------------
def try_transcoder(name, backend):
try:
t = Transcoder(backend)
print(f"[OK] Loaded transcoder backend '{name}' ({t.backend_name})")
return t
except Exception as e:
print(f"[SKIP] Backend '{name}' unavailable:", e)
return None
# -------------------------------------------------------------------
# MAIN
# -------------------------------------------------------------------
if __name__ == "__main__":
print("========== HDR End-to-End Compression & Transcoding Test ==========")
# -------------------------------------------------------
# Create HDR test image
# -------------------------------------------------------
img_hdr = make_swirl_hdr(256, 256)
print("[HDR] swirl:", img_hdr.shape, img_hdr.dtype)
# -------------------------------------------------------
# ENCODE using AUTO backend (native ? or WASM)
# -------------------------------------------------------
try:
enc = Encoder(EncoderBackend.AUTO)
print(f"[HDR] Encoder backend = {enc.backend_name}")
except Exception as e:
print("[FATAL] Could not create encoder:", e)
exit(1)
try:
print("[HDR] Compressing HDR swirl -> test_hdr.ktx2...")
ktx2_blob = enc.compress(
img_hdr,
format=-1, # auto-select HDR format
quality=BasisQuality.MAX,
effort=BasisEffort.DEFAULT,
flags=BasisFlags.KTX2_OUTPUT
)
print(" KTX2 size:", len(ktx2_blob))
open("test_hdr.ktx2", "wb").write(ktx2_blob)
print(" Wrote test_hdr.ktx2")
except Exception as e:
print("[FATAL] Encoding failed:", e)
exit(1)
# -------------------------------------------------------
# DECODE using AUTO (native ? or WASM)
# -------------------------------------------------------
t_auto = try_transcoder("AUTO", TranscoderBackend.AUTO)
if t_auto:
try:
hdr = t_auto.decode_rgba_hdr(ktx2_blob)
print(" AUTO decoded:", hdr.shape, hdr.dtype)
save_exr("decoded_auto_hdr.exr", hdr)
except Exception as e:
print(" [FAIL] AUTO decode failed:", e)
# -------------------------------------------------------
# DECODE using NATIVE if available
# -------------------------------------------------------
t_native = try_transcoder("NATIVE", TranscoderBackend.NATIVE)
if t_native:
try:
hdr_n = t_native.decode_rgba_hdr(ktx2_blob)
print(" Native decoded:", hdr_n.shape, hdr_n.dtype)
save_exr("decoded_native_hdr.exr", hdr_n)
except Exception as e:
print(" [FAIL] Native decode failed:", e)
# -------------------------------------------------------
# DECODE using WASM if available
# -------------------------------------------------------
t_wasm = try_transcoder("WASM", TranscoderBackend.WASM)
if t_wasm:
try:
hdr_w = t_wasm.decode_rgba_hdr(ktx2_blob)
print(" WASM decoded:", hdr_w.shape, hdr_w.dtype)
save_exr("decoded_wasm_hdr.exr", hdr_w)
except Exception as e:
print(" [FAIL] WASM decode failed:", e)
print("\n========== DONE ==========")

View File

@@ -0,0 +1,190 @@
#!/usr/bin/env python3
import sys
import numpy as np
from basisu_py.transcoder import Transcoder, TranscoderBackend
from basisu_py.constants import BasisTexFormat, TranscoderTextureFormat
print("========== TESTING TRANSCODER HELPERS & METADATA ==========\n")
# ----------------------------------------------------------------------------
# Load test KTX2 file
# ----------------------------------------------------------------------------
try:
ktx2_bytes = open("test.ktx2", "rb").read()
print("[INFO] Loaded test.ktx2")
except FileNotFoundError:
print("[ERROR] test.ktx2 not found. Run encoder tests first.")
sys.exit(1)
# ----------------------------------------------------------------------------
# Utility: run helper tests on a given backend
# ----------------------------------------------------------------------------
def test_backend(name, backend):
print(f"\n=== Testing {name} backend ===")
try:
t = Transcoder(backend)
except Exception as e:
print(f"[FAIL] Could not initialize {name} backend:", e)
return
print(f"[OK] {name} backend loaded")
# Version
try:
ver = t.get_version()
print(f" version = {ver}")
except Exception as e:
print(" [FAIL] get_version() error:", e)
return
# enable_debug_printf
try:
t.enable_debug_printf(True)
except Exception as e:
print(" [FAIL] enable_debug_printf() failed")
return
# Open KTX2
try:
raw = t.open(ktx2_bytes)
print(" [OK] open() success")
except Exception as e:
print(" [FAIL] open() failed:", e)
return
# ----------------------------------------------------------------------
# KTX2 top-level metadata
# ----------------------------------------------------------------------
try:
w = t.get_width(raw)
h = t.get_height(raw)
lv = t.get_levels(raw)
fc = t.get_faces(raw)
la = t.get_layers(raw)
fmt = t.get_basis_tex_format(raw)
print(f" Width = {w}")
print(f" Height = {h}")
print(f" Levels = {lv}")
print(f" Faces = {fc}")
print(f" Layers = {la}")
print(f" basis_tex_format = {fmt}")
print(f" has_alpha = {t.has_alpha(raw)}")
print(f" is_hdr = {t.is_hdr(raw)}")
print(f" is_ldr = {t.is_ldr(raw)}")
print(f" is_srgb = {t.is_srgb(raw)}")
print(f" is_etc1s = {t.is_etc1s(raw)}")
print(f" is_uastc_ldr_4x4 = {t.is_uastc_ldr_4x4(raw)}")
print(f" is_xuastc_ldr = {t.is_xuastc_ldr(raw)}")
print(f" is_astc_ldr = {t.is_astc_ldr(raw)}")
print(f" block dims = {t.get_block_width(raw)} x {t.get_block_height(raw)}")
except Exception as e:
print(" [FAIL] get_* metadata error:", e)
t.close(raw)
return
# ----------------------------------------------------------------------
# Per-level metadata for each mipmap
# ----------------------------------------------------------------------
print("\n -- Level Metadata --")
for level in range(lv):
try:
ow = t.get_level_orig_width(raw, level)
oh = t.get_level_orig_height(raw, level)
nbx = t.get_level_num_blocks_x(raw, level)
nby = t.get_level_num_blocks_y(raw, level)
tb = t.get_level_total_blocks(raw, level)
af = t.get_level_alpha_flag(raw, level)
ff = t.get_level_iframe_flag(raw, level)
print(f" Level {level}: orig={ow}x{oh}, blocks={nbx}x{nby}, total={tb}, alpha={af}, iframe={ff}")
except Exception as e:
print(f" [FAIL] Level {level} metadata error:", e)
# ----------------------------------------------------------------------
# Test ALL basis_tex_format helpers on the file's format
# ----------------------------------------------------------------------
print("\n -- basis_tex_format helpers --")
try:
print(f" is_xuastc_ldr = {t.basis_tex_format_is_xuastc_ldr(fmt)}")
print(f" is_astc_ldr = {t.basis_tex_format_is_astc_ldr(fmt)}")
print(f" block W/H = {t.basis_tex_format_get_block_width(fmt)} x "
f"{t.basis_tex_format_get_block_height(fmt)}")
print(f" is_hdr = {t.basis_tex_format_is_hdr(fmt)}")
print(f" is_ldr = {t.basis_tex_format_is_ldr(fmt)}")
except Exception as e:
print(" [FAIL] basis_tex_format_* error:", e)
# ----------------------------------------------------------------------
# Test transcoder_texture_format helpers using a few common formats
# ----------------------------------------------------------------------
print("\n -- transcoder_texture_format helpers --")
test_formats = [
TranscoderTextureFormat.TF_RGBA32,
TranscoderTextureFormat.TF_RGBA_HALF,
TranscoderTextureFormat.TF_BC7_RGBA,
TranscoderTextureFormat.TF_ETC1_RGB,
]
for tfmt in test_formats:
try:
print(f" Format {tfmt}: hdr={t.basis_transcoder_format_is_hdr(tfmt)}, "
f"ldr={t.basis_transcoder_format_is_ldr(tfmt)}, "
f"has_alpha={t.basis_transcoder_format_has_alpha(tfmt)}, "
f"uncompressed={t.basis_transcoder_format_is_uncompressed(tfmt)}, "
f"bytes/pixel or block={t.basis_get_bytes_per_block_or_pixel(tfmt)}")
except Exception as e:
print(" [FAIL] transcoder_texture_format_* error:", e)
# ----------------------------------------------------------------------
# Compute transcode buffer sizes
# ----------------------------------------------------------------------
print("\n -- compute_transcoded_image_size_in_bytes --")
try:
for tfmt in test_formats:
sz = t.basis_compute_transcoded_image_size_in_bytes(tfmt, w, h)
print(f" Format {tfmt}: size = {sz}")
except Exception as e:
print(" [FAIL] size computation error:", e)
# ----------------------------------------------------------------------
# Decode RGBA (LDR)
# ----------------------------------------------------------------------
print("\n -- decode_rgba --")
try:
img_rgba = t.decode_rgba(ktx2_bytes)
print(f" decode_rgba: shape={img_rgba.shape}, dtype={img_rgba.dtype}")
except Exception as e:
print(" [FAIL] decode_rgba error:", e)
# ----------------------------------------------------------------------
# Decode HDR if applicable
# ----------------------------------------------------------------------
if t.is_hdr(raw):
print("\n -- decode_rgba_hdr --")
try:
img_hdr = t.decode_rgba_hdr(ktx2_bytes)
print(f" decode_rgba_hdr: shape={img_hdr.shape}, dtype={img_hdr.dtype}")
except Exception as e:
print(" [FAIL] decode_rgba_hdr error:", e)
else:
print(" Texture is LDR; skipping decode_rgba_hdr().")
# Cleanup
t.close(raw)
print(f"\n=== {name} backend OK ===\n")
# ----------------------------------------------------------------------------
# Run tests for both backends
# ----------------------------------------------------------------------------
test_backend("NATIVE", TranscoderBackend.NATIVE)
test_backend("WASM", TranscoderBackend.WASM)
print("\n========== DONE ==========\n")