Skip to content

Instantly share code, notes, and snippets.

@infval
Last active June 22, 2021 03:23
Show Gist options
  • Save infval/3d12781f57e891d2905212f5aaebc6c5 to your computer and use it in GitHub Desktop.
Save infval/3d12781f57e891d2905212f5aaebc6c5 to your computer and use it in GitHub Desktop.
[Wii] Coraline | dxt Converter / Unpacker / Packer / Extractor | Magic: 30 81 80 00 / \x30\x81\x80\x00 / 30818000 / 308180
#!/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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment