mirror of
https://github.com/BinomialLLC/basis_universal.git
synced 2026-06-08 00:23:52 +00:00
414 lines
14 KiB
Python
414 lines
14 KiB
Python
#!/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())
|
|
|
|
|