-
-
Save infval/3d12781f57e891d2905212f5aaebc6c5 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3 | |
# -*- coding: utf-8 -*- | |
""" | |
[Wii] Coraline | |
.dxt <-> PNG | |
Requirements: | |
pip install -U pillow | |
Usage: | |
# ./*.dxt -> ./*.dxt.png | |
script.py | |
# filename.dxt -> filename.dxt.png | |
script.py filename.dxt | |
# filename.dxt.png + filename.dxt (header) -> filename.dxt | |
script.py filename.dxt.png | |
Useful Tool: | |
Color quantizer - http://x128.ho.ua/color-quantizer.html | |
""" | |
__version__ = "1.1" | |
__author__ = "infval" | |
import sys | |
from pathlib import Path | |
from struct import unpack | |
from collections import OrderedDict | |
from PIL import Image | |
HEADER_SIZE = 0x80 | |
class Texture: | |
""" | |
[Wii] Color Index 8-bits (C8) + Palette RGBA8 | |
""" | |
def __init__(self, path, offset, w, h, pal_offset): | |
self.path = path | |
self.offset = offset | |
self.w = w | |
self.h = h | |
self.pal_offset = pal_offset | |
def to_png(self, png_path): | |
im = Image.new("RGBA", (self.w, self.h)) | |
pixels = im.load() | |
with open(self.path, "rb") as f: | |
f.seek(self.offset) | |
b = f.read(self.w * self.h) | |
f.seek(self.pal_offset) | |
b_pal = f.read(256 * 4) | |
pal = [] | |
for i in range(256): | |
index = i * 2 | |
pal.append((b_pal[index+1], # R | |
b_pal[index+0], # G | |
b_pal[index+1+0x200], # B | |
b_pal[index+0+0x200])) # A | |
tw = 8 | |
th = 4 | |
bytes_in_tile = tw * th | |
bytes_in_row = self.w * th | |
x = 0 | |
y = 0 | |
for i in range(self.w * self.h): | |
index = i // bytes_in_row * bytes_in_row | |
index += x // tw * bytes_in_tile | |
index += (y % th) * tw + x % tw | |
c = b[index] | |
pixels[x, y] = pal[c] | |
x += 1 | |
if x == self.w: | |
x = 0 | |
y += 1 | |
im.save(png_path, "PNG") | |
print("Unpacked: {} -> {}".format(self.path, png_path)) | |
def from_png(self, bin_path, base_path=None): | |
im = Image.open(self.path).convert("RGBA") | |
pixels = im.load() | |
self.w, self.h = im.size | |
if not base_path: | |
# Raw | |
self.offset = 0 | |
self.pal_offset = self.w * self.h | |
b = bytearray(self.w * self.h + 256 * 4) | |
else: | |
with open(base_path, "rb") as f: | |
b = bytearray(f.read()) | |
pal = OrderedDict() | |
pal_i = 0 | |
b_img = bytearray(self.w * self.h) | |
tw = 8 | |
th = 4 | |
bytes_in_tile = tw * th | |
bytes_in_row = self.w * th | |
i = 0 | |
for y in range(self.h): | |
for x in range(self.w): | |
c = pixels[x, y] | |
if c not in pal: | |
if len(pal) >= 256: | |
print("Too many colors! Max: 256.", self.path) | |
sys.exit(1) | |
else: | |
pal[c] = pal_i | |
pal_i += 1 | |
index = i // bytes_in_row * bytes_in_row | |
index += x // tw * bytes_in_tile | |
index += (y % th) * tw + x % tw | |
b_img[index] = pal[c] | |
i += 1 | |
b[self.offset: self.offset + self.w * self.h] = b_img | |
pal = list(pal.keys()) | |
pal += [(0, 0, 0, 0)] * (256 - len(pal)) # Unused colors | |
for i in range(256): | |
index1 = self.pal_offset + i * 2 | |
index2 = self.pal_offset + i * 2 + 0x200 | |
cR, cG, cB, cA = pal[i] | |
b[index2+0] = cA | |
b[index2+1] = cB | |
b[index1+0] = cG | |
b[index1+1] = cR | |
with open(bin_path, "wb") as f: | |
f.write(b) | |
print("Packed: {} -> {}".format(self.path, bin_path)) | |
def dxt_to_png(path): | |
with open(path, "rb") as f: | |
f.seek(0x0A) | |
w, h = unpack("<BB", f.read(2)) | |
w = 1 << w | |
h = 1 << h | |
png_path = path.with_name(path.name + ".png") | |
Texture(path, HEADER_SIZE, w, h, HEADER_SIZE + w * h).to_png(png_path) | |
def png_to_dxt(path): | |
bin_path = path.with_suffix('') | |
with open(bin_path, "rb") as f: | |
f.seek(0x0A) | |
w, h = unpack("<BB", f.read(2)) | |
w = 1 << w | |
h = 1 << h | |
Texture(path, HEADER_SIZE, None, None, HEADER_SIZE + w * h).from_png(bin_path, bin_path) | |
if __name__ == '__main__': | |
if len(sys.argv) == 1: | |
path = Path(".") | |
for p in path.glob("*.dxt"): | |
dxt_to_png(p) | |
else: | |
p = Path(sys.argv[1]) | |
if p.suffix == ".png": | |
png_to_dxt(p) | |
else: | |
dxt_to_png(p) |
I am getting an error.
C:\ddrive\romfs4cnptexl\DATA\files\l_ben1\Tex>dxt_to_png.py
Palette offset: $2BA0
Width, Height: 128, 64. Position: $100. Size: $2000.
Traceback (most recent call last):
File "C:\ddrive\romfs4cnptexl\DATA\files\l_ben1\Tex\dxt_to_png.py", line 185, in <module>
dxt_to_png(p)
File "C:\ddrive\romfs4cnptexl\DATA\files\l_ben1\Tex\dxt_to_png.py", line 169, in dxt_to_png
Texture(path, offset, w_cur, h_cur, pal_offset).to_png(png_path)
File "C:\ddrive\romfs4cnptexl\DATA\files\l_ben1\Tex\dxt_to_png.py", line 59, in to_png
c = (b_pal[i * 2] << 8) | b_pal[i * 2 + 1]
~~~~~^^^^^^^
IndexError: index out of range
Heres the script edited to the new code.
import sys
from pathlib import Path
from struct import unpack
from collections import OrderedDict
from PIL import Image
HEADER_SIZE = 0x80
class Texture:
"""
[Wii] Color Index 8-bits (C8) + Palette RGBA8
"""
def __init__(self, path, offset, w, h, pal_offset):
self.path = path
self.offset = offset
self.w = w
self.h = h
self.pal_offset = pal_offset
def to_png(self, png_path):
im = Image.new("RGBA", (self.w, self.h))
pixels = im.load()
with open(self.path, "rb") as f:
f.seek(self.offset)
b = f.read(self.w * self.h)
f.seek(self.pal_offset)
b_pal = f.read(256 * 4)
pal = []
for i in range(256):
c = (b_pal[i * 2] << 8) | b_pal[i * 2 + 1]
c_r = (c & 0xF800) >> 8
c_g = (c & 0x07E0) >> 3
c_b = (c & 0x001F) << 3
pal.append((c_r, c_g, c_b, 0xFF))
tw = 8
th = 4
bytes_in_tile = tw * th
bytes_in_row = self.w * th
x = 0
y = 0
for i in range(self.w * self.h):
index = i // bytes_in_row * bytes_in_row
index += x // tw * bytes_in_tile
index += (y % th) * tw + x % tw
c = b[index]
pixels[x, y] = pal[c]
x += 1
if x == self.w:
x = 0
y += 1
im.save(png_path, "PNG")
print("Unpacked: {} -> {}".format(self.path, png_path))
def from_png(self, bin_path, base_path=None):
im = Image.open(self.path).convert("RGBA")
pixels = im.load()
self.w, self.h = im.size
if not base_path:
# Raw
self.offset = 0
self.pal_offset = self.w * self.h
b = bytearray(self.w * self.h + 256 * 4)
else:
with open(base_path, "rb") as f:
b = bytearray(f.read())
pal = OrderedDict()
pal_i = 0
b_img = bytearray(self.w * self.h)
tw = 8
th = 4
bytes_in_tile = tw * th
bytes_in_row = self.w * th
i = 0
for y in range(self.h):
for x in range(self.w):
c = pixels[x, y]
if c not in pal:
if len(pal) >= 256:
print("Too many colors! Max: 256.", self.path)
sys.exit(1)
else:
pal[c] = pal_i
pal_i += 1
index = i // bytes_in_row * bytes_in_row
index += x // tw * bytes_in_tile
index += (y % th) * tw + x % tw
b_img[index] = pal[c]
i += 1
b[self.offset: self.offset + self.w * self.h] = b_img
pal = list(pal.keys())
pal += [(0, 0, 0, 0)] * (256 - len(pal)) # Unused colors
for i in range(256):
index1 = self.pal_offset + i * 2
index2 = self.pal_offset + i * 2 + 0x200
cR, cG, cB, cA = pal[i]
b[index2+0] = cA
b[index2+1] = cB
b[index1+0] = cG
b[index1+1] = cR
with open(bin_path, "wb") as f:
f.write(b)
print("Packed: {} -> {}".format(self.path, bin_path))
def dxt_to_png(path):
with open(path, "rb") as f:
f.seek(0x0A)
w, h = unpack("<BB", f.read(2))
w = 1 << w
h = 1 << h
f.seek(0x1F)
mips_count = f.read(1)[0]
if mips_count == 0:
png_path = path.with_name(path.name + ".png")
Texture(path, HEADER_SIZE, w, h, HEADER_SIZE + w * h).to_png(png_path)
else:
header_size = 0x100
pal_offset = header_size + sum((w // (1 << i) * h // (1 << i) for i in range(mips_count)))
print(f"Palette offset: ${pal_offset:X}")
offset = header_size
for i in range(mips_count):
w_cur = w // (1 << i)
h_cur = h // (1 << i)
png_path = path.with_name(path.name + f"_{i}.png")
print(f"Width, Height: {w_cur}, {h_cur}. Position: ${offset:X}. Size: ${w_cur*h_cur:X}.")
Texture(path, offset, w_cur, h_cur, pal_offset).to_png(png_path)
offset += w_cur * h_cur
def png_to_dxt(path):
bin_path = path.with_suffix('')
with open(bin_path, "rb") as f:
f.seek(0x0A)
w, h = unpack("<BB", f.read(2))
w = 1 << w
h = 1 << h
Texture(path, HEADER_SIZE, None, None, HEADER_SIZE + w * h).from_png(bin_path, bin_path)
if __name__ == '__main__':
if len(sys.argv) == 1:
path = Path(".")
for p in path.glob("*.dxt"):
dxt_to_png(p)
else:
p = Path(sys.argv[1])
if p.suffix == ".png":
png_to_dxt(p)
else:
dxt_to_png(p)
Removed unnecessary sections so github doesnt take any of it as markdown
I am getting an error
You changed the script correctly, but I don't have this file to fix (you didn't even write what game you use and the path to the file in the resources). It is only clear that the file is shorter than expected. Try changing header_size = 0x100
to header_size = 0x80
.
Yeah probably should have. Game is cartoon network punchtime explosion. Here is the extracted DXT using the wii_to_dxt.py script. Header size change did not work. Heres a link to a mega folder with multiple dxt files https://mega.nz/folder/mjBFAIQS#Ep75_7ydW3twTkbOIsEz4Q
@Rypie109 I didn't realize that my wii_to_dxt.py script was cutting off texture data. Updated wii_to_dxt.py script:
import sys
from pathlib import Path
PALETTE_SIZE = 0x400 # 0x200
def wii_to_dxt(path):
print(f"# {path}")
data = path.read_bytes()
ind = -1
while True:
ind = data.find(bytes.fromhex("30 81 80 00"), ind + 1)
if ind == -1:
break
w, h, mips_count = 1 << data[ind + 0xA], 1 << data[ind + 0xB], data[ind + 0x1F]
header_size = 0x100
if mips_count == 0:
mips_count = 1
header_size = 0x80
size = header_size + PALETTE_SIZE + sum((w // (1 << i) * h // (1 << i) for i in range(mips_count)))
p = path.with_name(path.stem + f"_{ind:06X}.dxt")
p.write_bytes(data[ind : ind + size])
print(f"> {p}")
if __name__ == '__main__':
if len(sys.argv) == 1:
path = Path(".")
for p in path.glob("*.wii"):
wii_to_dxt(p)
else:
p = Path(sys.argv[1])
wii_to_dxt(p)
Updated dxt_to_png.py script, which now supports RGBA8888, RGB565, gray (I didn't figure it out) palette formats at the same time:
import sys
from pathlib import Path
from struct import unpack
from collections import OrderedDict
from PIL import Image
HEADER_SIZE = 0x80
class Texture:
"""
[Wii] Color Index 8-bits (C8) + Palette RGBA8
"""
def __init__(self, path, offset, w, h, pal_offset, pal_format=0):
self.path = path
self.offset = offset
self.w = w
self.h = h
self.pal_offset = pal_offset
self.pal_format = pal_format
def to_png(self, png_path):
im = Image.new("RGBA", (self.w, self.h))
pixels = im.load()
with open(self.path, "rb") as f:
f.seek(self.offset)
b = f.read(self.w * self.h)
f.seek(self.pal_offset)
b_pal = f.read(256 * 4)
pal = []
for i in range(256):
if self.pal_format == 0:
index = i * 2
pal.append((b_pal[index+1], # R
b_pal[index+0], # G
b_pal[index+1+0x200], # B
b_pal[index+0+0x200])) # A
elif self.pal_format == 1:
c = (b_pal[i * 2] << 8) | b_pal[i * 2 + 1]
c_r = (c & 0xF800) >> 8
c_g = (c & 0x07E0) >> 3
c_b = (c & 0x001F) << 3
pal.append((c_r, c_g, c_b, 0xFF))
else:
pal.append((i, i, i, 0xFF))
tw = 8
th = 4
bytes_in_tile = tw * th
bytes_in_row = self.w * th
x = 0
y = 0
for i in range(self.w * self.h):
index = i // bytes_in_row * bytes_in_row
index += x // tw * bytes_in_tile
index += (y % th) * tw + x % tw
c = b[index]
pixels[x, y] = pal[c]
x += 1
if x == self.w:
x = 0
y += 1
im.save(png_path, "PNG")
print("Unpacked: {} -> {}".format(self.path, png_path))
def from_png(self, bin_path, base_path=None):
im = Image.open(self.path).convert("RGBA")
pixels = im.load()
self.w, self.h = im.size
if not base_path:
# Raw
self.offset = 0
self.pal_offset = self.w * self.h
b = bytearray(self.w * self.h + 256 * 4)
else:
with open(base_path, "rb") as f:
b = bytearray(f.read())
pal = OrderedDict()
pal_i = 0
b_img = bytearray(self.w * self.h)
tw = 8
th = 4
bytes_in_tile = tw * th
bytes_in_row = self.w * th
i = 0
for y in range(self.h):
for x in range(self.w):
c = pixels[x, y]
if c not in pal:
if len(pal) >= 256:
print("Too many colors! Max: 256.", self.path)
sys.exit(1)
else:
pal[c] = pal_i
pal_i += 1
index = i // bytes_in_row * bytes_in_row
index += x // tw * bytes_in_tile
index += (y % th) * tw + x % tw
b_img[index] = pal[c]
i += 1
b[self.offset: self.offset + self.w * self.h] = b_img
pal = list(pal.keys())
pal += [(0, 0, 0, 0)] * (256 - len(pal)) # Unused colors
for i in range(256):
index1 = self.pal_offset + i * 2
index2 = self.pal_offset + i * 2 + 0x200
cR, cG, cB, cA = pal[i]
b[index2+0] = cA
b[index2+1] = cB
b[index1+0] = cG
b[index1+1] = cR
with open(bin_path, "wb") as f:
f.write(b)
print("Packed: {} -> {}".format(self.path, bin_path))
def dxt_to_png(path):
with open(path, "rb") as f:
f.seek(0x0A)
w, h = unpack("<BB", f.read(2))
w = 1 << w
h = 1 << h
f.seek(0x1F)
mips_count = f.read(1)[0]
f.seek(0x04)
pal_format = (f.read(1)[0] >> 5) & 3
if mips_count == 0:
png_path = path.with_name(path.name + ".png")
Texture(path, HEADER_SIZE, w, h, HEADER_SIZE + w * h).to_png(png_path)
else:
header_size = 0x100
pal_offset = header_size + sum((w // (1 << i) * h // (1 << i) for i in range(mips_count)))
print(f"Palette offset: ${pal_offset:X}")
offset = header_size
for i in range(mips_count):
w_cur = w // (1 << i)
h_cur = h // (1 << i)
png_path = path.with_name(path.name + f"_{i}.png")
print(f"Width, Height: {w_cur}, {h_cur}. Position: ${offset:X}. Size: ${w_cur*h_cur:X}.")
Texture(path, offset, w_cur, h_cur, pal_offset, pal_format).to_png(png_path)
offset += w_cur * h_cur
def png_to_dxt(path):
bin_path = path.with_suffix('')
with open(bin_path, "rb") as f:
f.seek(0x0A)
w, h = unpack("<BB", f.read(2))
w = 1 << w
h = 1 << h
Texture(path, HEADER_SIZE, None, None, HEADER_SIZE + w * h).from_png(bin_path, bin_path)
if __name__ == '__main__':
if len(sys.argv) == 1:
path = Path(".")
for p in path.glob("*.dxt"):
dxt_to_png(p)
else:
p = Path(sys.argv[1])
if p.suffix == ".png":
png_to_dxt(p)
else:
dxt_to_png(p)
Thank you so much! It works. Curious though, you mentioned using the software to reduce the colors for repack, but how would I get 5 different .png files extracted from 1 .dxt file back into that .dxt file after modification? Does the script have support for that? Coraline extracts 1 single image from each .dxt, making it simpler
how would I get 5 different .png files extracted from 1 .dxt file back into that .dxt file after modification? Does the script have support for that?
The updated script does not support PNG -> "DXT". It is not difficult to take the main texture and create smaller versions of it, but I am not going to spend time on it.
Got it. That's fine, already helped me plenty on this as it is. I appreciate it!
Wow thank you so dang much. Appreciate it.