-
-
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 will try to load the right python version somehow
I think another reason. I checked now on version 3.12.4 (https://www.python.org), there is no error.
What are you running the script through? Cmd? Python from cmd? Shell?
What are you running the script through?
cmd. PowerShell works too.
Got it, now I just have to figure out how to switch my py version from 3.11 to 3.12 in cmd. Thanks for answering all my questions, I really appreciate this, and the script!
I figured it out!
I am a bit confused on what I should be doing on color quantizer, as when I select 256 from the dropdown menu, and press, ok, it switches to 146. Also, you mention that the script wont work if I dont reduce it to 256, I am assuming you are referring to your python script, but that doesnt seem to have any issues, maybe im understanding it wrong.
what I should be doing on color quantizer, as when I select 256 from the dropdown menu, and press, ok, it switches to 146
"Color quantizer" is needed if you want to convert the modified PNG back to a .dxt file and the PNG will have more than 256 colors (my script will display the message "Too many colors! Max: 256."). You have fewer than 256 colors, so the program is not needed in this case.
Ok, I have no need to recompress the textures. I have figured this all out, and now I need to find out how to make a script to automaticly split at certain points. Any ideas where to start/where to ask?
I need to find out how to make a script to automaticly split at certain points
Extracting .dxt files from .wii is easy. Here is the script, name it wii_to_dxt.py:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
[Wii] Coraline
.wii -> .dxt
Usage:
# ./*.wii -> ./*.dxt
script.py
# filename.wii -> filename_*.dxt
script.py filename.wii
"""
__version__ = "1.0"
__author__ = "infval"
import sys
from pathlib import Path
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 = data[ind + 0xA], data[ind + 0xB]
size = (1 << w) * (1 << h) + 0x480
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)
To extract all .wii in the script folder, run the script:
wii_to_dxt.py
To unpack a single .wii, pass the file path as a command line argument:
wii_to_dxt.py global.wii
...YOU MADLAD, thank you so much!
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.
@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
I downloaded the script, but I do not think I am using python 3. I will try to load the right python version somehow.