-
-
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) |
@romhacking I have some questions. What made you create this script? Any motive? Are you also interested in datamining this game?
@Rypie109 The script dxt_to_png.py was made for the Russian translation of the game Coraline, that's all. I mostly do small hacks for NES games.
@romhacking That is very interesting! I am assuming this was a fan translation?
@Rypie109 Of course it's an unofficial translation. I only made this script.
Hi, it's me again. Is there a way to make the dxt_to_png script work with a game that has the exact same form
at, yet has a slightly different formatting? Here is a file I copied out, same header same everthing, and when converted it looks like this. Heres a sample dxt file, just rename it from .svg to .dxt
The image I attached is not properly converted, open it in a new tab or save it to see
Hi, it's me again. Is there a way to make the dxt_to_png script work with a game that has the exact same form . The palette is strange. First 0x000-0x1FF palette bytes - 256 colors of 2 bytes in RGB565 Big-Endian format, then 0x200-0x3FF palette bytes - 128 colors of 4 bytes in RGBA8888 Big-Endian format, which coincide with color indices 128-255 of the first palette, most likely garbage (the developers wrote the palette in RGBA8888 format, then wrote over it in RGB565 format). I changed the script for this file, the color channels may be rearranged.
Change in dxt_to_png.py script from line 57:
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
New code:
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))
Also change from line 148:
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)
New code:
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
Wow thank you so dang much. Appreciate it.
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!
You have been monumental in this project, and I made sure to tell people that this stuff was by you. The last thing for me to do to technically finish this 235 day project is to get the rigs, but I do not expect you to do that at all, as I don't want to waste your time, and you have already done so much for me already.