500 lines
12 KiB
Python
500 lines
12 KiB
Python
"""
|
|
|
|
MIT License
|
|
|
|
Copyright (c) 2020 Benjamin Collins (kion @ dashgl.com)
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|
|
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import png
|
|
import math
|
|
import struct
|
|
|
|
from NinjaTexture import NinjaTexture
|
|
|
|
class PowerVR: # {
|
|
|
|
# Cache Positions
|
|
LOOKUP_TABLE = {}
|
|
|
|
# Color Formats
|
|
ARGB_1555 = 0x00
|
|
RGB_565 = 0x01
|
|
ARGB_4444 = 0x02
|
|
YUV_422 = 0x03
|
|
BUMP = 0x04
|
|
RGB_555 = 0x05
|
|
ARGB_8888 = 0x06
|
|
YUV_420 = 0x06
|
|
|
|
# Data Formats
|
|
TWIDDLED = 0x01
|
|
TWIDDLED_MM = 0x02
|
|
VQ = 0x03
|
|
VQ_MM = 0x04
|
|
PALETTIZE4 = 0x05
|
|
PALETTIZE4_MM = 0x06
|
|
PALETTIZE8 = 0x07
|
|
PALETTIZE8_MM = 0x08
|
|
RECTANGLE = 0x09
|
|
STRIDE = 0x0B
|
|
TWIDDLED_RECTANGLE = 0x0D
|
|
ABGR = 0x0E
|
|
ABGR_MM = 0x0F
|
|
SMALLVQ = 0x10
|
|
SMALLVQ_MM = 0x11
|
|
TWIDDLED_MM_ALIAS = 0x12
|
|
|
|
def __init__(self, filename): # {
|
|
|
|
# File Information
|
|
|
|
self.debug = False
|
|
base = os.path.basename(filename)
|
|
self.name = os.path.splitext(base)[0]
|
|
print(self.name)
|
|
self.path = 'input/' + filename
|
|
|
|
# Textures
|
|
self.tex_list = []
|
|
self.pal_list = []
|
|
|
|
return None
|
|
|
|
def parse(self):
|
|
|
|
self.file = open(self.path, 'rb')
|
|
self.length = os.path.getsize(self.path)
|
|
|
|
while self.file.tell() < self.length:
|
|
|
|
bytes = self.file.read(4)
|
|
|
|
if bytes == b'PVMH':
|
|
self.readPvm()
|
|
break
|
|
elif bytes == b'PVRT':
|
|
print("-- READ PVRT--")
|
|
bytes = self.file.read(4)
|
|
p = struct.unpack('I', bytes)
|
|
pvrt_len = p[0]
|
|
tex = NinjaTexture()
|
|
tex.setIndex(0)
|
|
tex.setName(self.name)
|
|
raw = self.readPvr()
|
|
tex.setRaw(raw)
|
|
tex.setWidth(self.width)
|
|
tex.setHeight(self.height)
|
|
self.tex_list.append(tex)
|
|
break
|
|
|
|
for tex in self.tex_list:
|
|
if not tex.raw:
|
|
continue
|
|
imgName = "output/" + tex.name + '.png'
|
|
w = png.Writer(tex.width, tex.height, greyscale=False, alpha=True,compression=9)
|
|
f = open(imgName, 'wb')
|
|
w.write(f, tex.raw)
|
|
f.close()
|
|
|
|
f = open(imgName, 'rb')
|
|
data = f.read()
|
|
f.close()
|
|
tex.setData(data)
|
|
os.remove(imgName)
|
|
|
|
self.file.close()
|
|
return self.tex_list
|
|
|
|
def readPvm(self):
|
|
|
|
if self.debug:
|
|
print("--- READING PVM FIL ---")
|
|
|
|
bytes = self.file.read(4)
|
|
p = struct.unpack('I', bytes)
|
|
pvmh_len = p[0]
|
|
save_pos = self.file.tell()
|
|
|
|
bytes = self.file.read(4)
|
|
p = struct.unpack('HH', bytes)
|
|
flags = p[0]
|
|
tex_count = p[1]
|
|
|
|
for i in range(tex_count):
|
|
bytes = self.file.read(2)
|
|
p = struct.unpack('H', bytes)
|
|
tex = NinjaTexture()
|
|
index = p[0]
|
|
tex.setIndex(p[0])
|
|
|
|
if flags & 0x08:
|
|
bytes = self.file.read(0x1c)
|
|
name = bytes.decode()
|
|
name = name.replace('\x00', '')
|
|
tex.setName(name)
|
|
if self.debug:
|
|
print("[%02d] %s" % (index, name))
|
|
if flags & 0x04:
|
|
bytes = self.file.read(2)
|
|
p = struct.unpack('H', bytes)
|
|
format = p[0] >> 8
|
|
if flags & 0x02:
|
|
bytes = self.file.read(2)
|
|
p = struct.unpack('H', bytes)
|
|
size = p[0]
|
|
lo_bytes = size & 0x0f
|
|
hi_bytes = (size >> 4) & 0x0f
|
|
width = (1 << (lo_bytes + 2))
|
|
height = (1 << (hi_bytes + 2))
|
|
if flags & 0x01:
|
|
bytes = self.file.read(4)
|
|
p = struct.unpack('I', bytes)
|
|
tex.setGlobalIndex(p[0])
|
|
self.tex_list.append(tex)
|
|
|
|
self.file.seek(save_pos + pvmh_len, 0)
|
|
tex_len = len(self.tex_list)
|
|
for tex in self.tex_list:
|
|
|
|
if self.debug:
|
|
print("PVR Reading %d of %d" % (tex.index, tex_len))
|
|
print("Position: 0x%08x" % self.file.tell())
|
|
|
|
while self.file.tell() < self.length:
|
|
bytes = self.file.read(4)
|
|
if bytes != b'PVRT':
|
|
continue
|
|
break
|
|
|
|
if self.debug:
|
|
print("Found PVRT @ 0x%08x" % (self.file.tell() - 4))
|
|
|
|
bytes = self.file.read(4)
|
|
p = struct.unpack('I', bytes)
|
|
save_pos = self.file.tell()
|
|
pvrt_len = p[0]
|
|
raw = self.readPvr()
|
|
tex.setRaw(raw)
|
|
tex.setWidth(self.width)
|
|
tex.setHeight(self.height)
|
|
self.file.seek(pvrt_len + save_pos, 0)
|
|
|
|
return None
|
|
|
|
def readPvr(self):
|
|
# Read Header
|
|
bytes = self.file.read(8)
|
|
p = struct.unpack('BBHHH', bytes)
|
|
|
|
self.color_format = p[0]
|
|
self.data_format = p[1]
|
|
self.width = p[3]
|
|
self.height = p[4]
|
|
|
|
format_list = [
|
|
"",
|
|
"TWIDDLED",
|
|
"TWIDDLED_MM",
|
|
"VQ",
|
|
"VQ_MM",
|
|
"PALETTIZE4",
|
|
"PALETTIZE4_MM",
|
|
"PALETTIZE8",
|
|
"PALETTIZE8_MM",
|
|
"RECTANGLE",
|
|
"",
|
|
"STRIDE",
|
|
"",
|
|
"TWIDDLED_RECTANGLE",
|
|
"ABGR",
|
|
"ABGR_MM",
|
|
"SMALLVQ",
|
|
"SMALLVQ_MM",
|
|
"TWIDDLED_MM_ALIAS"
|
|
]
|
|
|
|
if self.debug:
|
|
print("Data Format: %s" % format_list[self.data_format])
|
|
print("Width: %d" % self.width)
|
|
print("Height: %d" % self.height)
|
|
|
|
self.isTwiddled = False
|
|
self.isMipmap = False
|
|
self.isCompressed = False
|
|
self.codebook_size = 256
|
|
self.bitmap = [[0 for x in range(self.width * 4)] for y in range(self.height)]
|
|
|
|
twiddled = (
|
|
PowerVR.TWIDDLED,
|
|
PowerVR.TWIDDLED_MM,
|
|
PowerVR.TWIDDLED_RECTANGLE,
|
|
PowerVR.TWIDDLED_MM_ALIAS
|
|
)
|
|
|
|
mipmap = (
|
|
PowerVR.TWIDDLED_MM,
|
|
PowerVR.PALETTIZE4_MM,
|
|
PowerVR.PALETTIZE8_MM,
|
|
PowerVR.ABGR_MM,
|
|
PowerVR.VQ_MM,
|
|
PowerVR.SMALLVQ_MM
|
|
)
|
|
|
|
compressed = (
|
|
PowerVR.VQ,
|
|
PowerVR.VQ_MM,
|
|
PowerVR.SMALLVQ,
|
|
PowerVR.SMALLVQ_MM
|
|
)
|
|
|
|
palette = (
|
|
PowerVR.PALETTIZE4,
|
|
PowerVR.PALETTIZE4_MM,
|
|
PowerVR.PALETTIZE8,
|
|
PowerVR.PALETTIZE8_MM
|
|
)
|
|
|
|
not_supported = (
|
|
PowerVR.STRIDE,
|
|
PowerVR.ABGR,
|
|
PowerVR.ABGR_MM
|
|
)
|
|
|
|
if self.data_format in compressed:
|
|
self.isCompressed = True
|
|
|
|
if self.data_format in twiddled:
|
|
self.isTwiddled = True
|
|
|
|
if self.data_format in mipmap:
|
|
self.isMipmap = True
|
|
|
|
if self.data_format in palette:
|
|
print("NEED TO DEFINE A PALETTE!!!")
|
|
sys.exit()
|
|
|
|
if self.data_format in not_supported:
|
|
print("THIS DATA FORMAT IS NOT SUPPORTED")
|
|
sys.exit()
|
|
|
|
if self.data_format == PowerVR.SMALLVQ:
|
|
if self.width <= 16:
|
|
self.codebook_size = 16
|
|
elif self.width == 32:
|
|
self.codebook_size = 32
|
|
elif self.width == 64:
|
|
self.codebook_size = 128
|
|
else:
|
|
self.codebook_size = 256
|
|
elif self.data_format == PowerVR.SMALLVQ_MM:
|
|
if self.width <= 16:
|
|
self.codebook_size = 16
|
|
elif self.width == 32:
|
|
self.codebook_size = 64
|
|
else:
|
|
self.codebook_size = 256
|
|
|
|
if self.isCompressed:
|
|
self.codebook = []
|
|
for i in range(self.codebook_size):
|
|
bytes = self.file.read(8)
|
|
color = struct.unpack('HHHH', bytes)
|
|
entry = []
|
|
for p in range(4):
|
|
if self.color_format == 0:
|
|
rgba = self.ARGB_1555(color[p])
|
|
elif self.color_format == 1:
|
|
rgba = self.RGB_565(color[p])
|
|
elif self.color_format == 2:
|
|
rgba = self.ARGB_4444(color[p])
|
|
entry.append(rgba)
|
|
self.codebook.append(entry)
|
|
|
|
if self.isMipmap:
|
|
seek_ofs = self.get_mipmap_size()
|
|
self.file.read(seek_ofs)
|
|
|
|
self.startOfs = self.file.tell()
|
|
|
|
if self.data_format == PowerVR.TWIDDLED_RECTANGLE:
|
|
size = 0
|
|
dir = ''
|
|
count = 0
|
|
if self.width > self.height:
|
|
size = self.height
|
|
dir = 'h'
|
|
count = int(self.width / self.height)
|
|
else:
|
|
size = self.width
|
|
dir = 'w'
|
|
count = int(self.height / self.width)
|
|
|
|
if self.debug:
|
|
print("Size: %d" % size)
|
|
|
|
for n in range(count):
|
|
|
|
if self.debug:
|
|
print("File Position: 0x%08x" % self.file.tell())
|
|
print("Reading square %d of %d" % (n, count))
|
|
|
|
self.startOfs = self.file.tell()
|
|
for y in range(size):
|
|
for x in range(size):
|
|
|
|
i = self.untwiddle(x, y)
|
|
self.file.seek(self.startOfs + (i * 2), 0)
|
|
b = self.file.read(2)
|
|
color = struct.unpack('H', b)[0]
|
|
if self.color_format == 0:
|
|
rgba = self.ARGB_1555(color)
|
|
elif self.color_format == 1:
|
|
rgba = self.RGB_565(color)
|
|
elif self.color_format == 2:
|
|
rgba = self.ARGB_4444(color)
|
|
|
|
if self.width > self.height:
|
|
self.bitmap[y][(n*size*4) + x*4 + 0] = rgba[0]
|
|
self.bitmap[y][(n*size*4) + x*4 + 1] = rgba[1]
|
|
self.bitmap[y][(n*size*4) + x*4 + 2] = rgba[2]
|
|
self.bitmap[y][(n*size*4) + x*4 + 3] = rgba[3]
|
|
else:
|
|
self.bitmap[y + (n*size)][x*4 + 0] = rgba[0]
|
|
self.bitmap[y + (n*size)][x*4 + 1] = rgba[1]
|
|
self.bitmap[y + (n*size)][x*4 + 2] = rgba[2]
|
|
self.bitmap[y + (n*size)][x*4 + 3] = rgba[3]
|
|
return self.bitmap
|
|
|
|
if self.isTwiddled:
|
|
for y in range(self.height):
|
|
for x in range(self.width):
|
|
i = self.untwiddle(x, y)
|
|
self.file.seek(self.startOfs + (i * 2), 0)
|
|
b = self.file.read(2)
|
|
color = struct.unpack('H', b)[0]
|
|
if self.color_format == 0:
|
|
rgba = self.ARGB_1555(color)
|
|
elif self.color_format == 1:
|
|
rgba = self.RGB_565(color)
|
|
elif self.color_format == 2:
|
|
rgba = self.ARGB_4444(color)
|
|
self.bitmap[y][x*4 + 0] = rgba[0]
|
|
self.bitmap[y][x*4 + 1] = rgba[1]
|
|
self.bitmap[y][x*4 + 2] = rgba[2]
|
|
self.bitmap[y][x*4 + 3] = rgba[3]
|
|
return self.bitmap
|
|
elif self.isCompressed:
|
|
for y in range(int(self.height / 2)):
|
|
for x in range(int(self.width / 2)):
|
|
i = self.untwiddle(x, y)
|
|
self.file.seek(self.startOfs + i , 0)
|
|
b = self.file.read(1)
|
|
index = struct.unpack('B', b)[0]
|
|
rgba = self.codebook[index]
|
|
# Thanks to VincentNL for order fix
|
|
bitmap[y*2 + 0][x*8 + 0] = rgba[0][0]
|
|
bitmap[y*2 + 0][x*8 + 1] = rgba[0][1]
|
|
bitmap[y*2 + 0][x*8 + 2] = rgba[0][2]
|
|
bitmap[y*2 + 0][x*8 + 3] = rgba[0][3]
|
|
bitmap[y*2 + 0][x*8 + 4] = rgba[2][0]
|
|
bitmap[y*2 + 0][x*8 + 5] = rgba[2][1]
|
|
bitmap[y*2 + 0][x*8 + 6] = rgba[2][2]
|
|
bitmap[y*2 + 0][x*8 + 7] = rgba[2][3]
|
|
bitmap[y*2 + 1][x*8 + 0] = rgba[1][0]
|
|
bitmap[y*2 + 1][x*8 + 1] = rgba[1][1]
|
|
bitmap[y*2 + 1][x*8 + 2] = rgba[1][2]
|
|
bitmap[y*2 + 1][x*8 + 3] = rgba[1][3]
|
|
bitmap[y*2 + 1][x*8 + 4] = rgba[3][0]
|
|
bitmap[y*2 + 1][x*8 + 5] = rgba[3][1]
|
|
bitmap[y*2 + 1][x*8 + 6] = rgba[3][2]
|
|
bitmap[y*2 + 1][x*8 + 7] = rgba[3][3]
|
|
return self.bitmap
|
|
|
|
return None
|
|
|
|
def get_mipmap_size(self):
|
|
mipCount = 0
|
|
seek_ofs = 0
|
|
width = self.width
|
|
|
|
while width :
|
|
mipCount = mipCount + 1
|
|
width = int(width / 2)
|
|
|
|
while mipCount:
|
|
mipWidth = (self.width >> (mipCount - 1))
|
|
mipHeight = (self.height >> (mipCount - 1))
|
|
mipSize = mipWidth * mipHeight
|
|
|
|
mipCount = mipCount - 1
|
|
|
|
if mipCount > 0:
|
|
if self.isCompressed:
|
|
seek_ofs = seek_ofs + int(mipSize / 4)
|
|
else:
|
|
seek_ofs = seek_ofs + (2 * mipSize)
|
|
else:
|
|
if self.isCompressed:
|
|
seek_ofs = seek_ofs + 1
|
|
else:
|
|
seek_ofs = seek_ofs + 2
|
|
return seek_ofs
|
|
|
|
def untwiddle(self, x, y):
|
|
key = "{0}_{1}".format(x,y)
|
|
if key in PowerVR.LOOKUP_TABLE:
|
|
return PowerVR.LOOKUP_TABLE[key]
|
|
|
|
def UntwiddleValue(val):
|
|
untwiddled = 0
|
|
for i in range(10):
|
|
shift = int(math.pow(2, i))
|
|
if val & shift :
|
|
untwiddled = untwiddled | (shift << i)
|
|
return untwiddled
|
|
|
|
pos = UntwiddleValue(y) | UntwiddleValue(x) << 1
|
|
PowerVR.LOOKUP_TABLE[key] = pos
|
|
return pos
|
|
|
|
def ARGB_1555 (self, v):
|
|
a = 0xFF if (v & (1<<15)) else 0
|
|
r = (v >> (10-3)) & 0xf8
|
|
g = (v >> (5-3)) & 0xf8
|
|
b = (v << 3) & 0xf8
|
|
return (r,g,b,a)
|
|
|
|
def ARGB_4444 (self, v):
|
|
a = (v >> (12-4)) & 0xf0
|
|
r = (v >> (8-4)) & 0xf0
|
|
g = (v >> (4-4)) & 0xf0
|
|
b = (v << 4) & 0xf0
|
|
return (r,g,b,a)
|
|
|
|
def RGB_565(self, v):
|
|
a = 0xff
|
|
r = (v >> (11-3)) & (0x1f<<3)
|
|
g = (v >> (5-2)) & (0x3f<<2)
|
|
b = (v << 3) & (0x1f<<3)
|
|
return (r,g,b,a)
|