Skip to content

Instantly share code, notes, and snippets.

@infval
Last active August 25, 2022 21:58
Show Gist options
  • Save infval/b23380076d77341ccc379a62ef9f23b3 to your computer and use it in GitHub Desktop.
Save infval/b23380076d77341ccc379a62ef9f23b3 to your computer and use it in GitHub Desktop.
[PS2] Silent Hill 2 PIC.IMG extractor (unpacker/packer)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import math
import argparse
from collections import OrderedDict
from PIL import Image, ImageDraw
PSMT8 = 0x08
PSMCT32 = 0x18
HEADER_SIZE = 192
HEADER_PAL_SIZE = 48
b = None
def UnSwizzle8(Buf, Width, Height, Start = 0):
# https://forum.xentax.com/viewtopic.php?t=2640#p110753
Swizzled = Buf[Start:Start+Width*Height]
for y in range(Height):
for x in range(Width):
block_location = (y & (~0xf)) * Width + (x & (~0xf)) * 2
swap_selector = (((y + 2) >> 2) & 0x1) * 4
posY = (((y & (~3)) >> 1) + (y & 1)) & 0x7
column_location = posY * Width * 2 + ((x + swap_selector) & 0x7) * 4
byte_num = ((y >> 1) & 1) + ((x >> 2) & 2);
Buf[Start + x + (y * Width)] = Swizzled[block_location + column_location + byte_num]
def Swizzle8(Buf, Width, Height, Start = 0):
UnSwizzled = Buf[Start:Start+Width*Height]
for y in range(Height):
for x in range(Width):
block_location = (y & (~0xf)) * Width + (x & (~0xf)) * 2
swap_selector = (((y + 2) >> 2) & 0x1) * 4
posY = (((y & (~3)) >> 1) + (y & 1)) & 0x7
column_location = posY * Width * 2 + ((x + swap_selector) & 0x7) * 4
byte_num = ((y >> 1) & 1) + ((x >> 2) & 2);
Buf[Start + block_location + column_location + byte_num] = UnSwizzled[x + (y * Width)]
def process_pixels(func, pixels, w, h):
pixels2 = [pixels[i % w, i / w] for i in range(w * h)]
func(pixels2, w, h)
i = 0
for y in range(h):
for x in range(w):
pixels[x, y] = pixels2[i]
i += 1
def swap_palette(pal):
temp_pal = [None] * 256
for i in range(0, 256, 32):
temp_pal[i+ 0:i+ 0+8] = pal[i+ 0:i+ 0+8]
temp_pal[i+ 8:i+ 8+8] = pal[i+16:i+16+8]
temp_pal[i+16:i+16+8] = pal[i+ 8:i+ 8+8]
temp_pal[i+24:i+24+8] = pal[i+24:i+24+8]
return temp_pal
class Texture:
def __init__(self, offset, format, w, h):
self.offset = offset
self.format = format
self.w = w
self.h = h
@staticmethod
def scale_alpha(a):
return int(min(255.0 * (a / 128.0), 0xFF))
@staticmethod
def unscale_alpha(a):
return int(min(math.ceil(128.0 * (a / 255.0)), 0x80))
def to_png(self, path):
im = Image.new("RGBA", (self.w, self.h))
pixels = im.load()
png_path = os.path.join(path, "{}.png".format(self.offset))
if self.format == PSMT8:
pal_start = self.offset + HEADER_SIZE + self.w * self.h + HEADER_PAL_SIZE
pal = []
for j in range(16):
for i in range(16):
index = pal_start+j*256+i*4
pal.append((b[index+0], b[index+1], b[index+2], Texture.scale_alpha(b[index+3])))
pal = swap_palette(pal)
x = 0
y = 0
start = (self.offset + HEADER_SIZE)
for i in range(start, start + self.w * self.h):
c = b[i]
pixels[x, y] = pal[c]
x += 1
if x == self.w:
x = 0
y += 1
process_pixels(UnSwizzle8, pixels, self.w, self.h)
im.save(png_path, "PNG")
print("Unpacked 8:", png_path)
elif self.format == PSMCT32:
x = 0
y = 0
start = (self.offset + HEADER_SIZE) // 4
for i in range(start, start + self.w * self.h):
int32_str = b[i*4:i*4+4]
c0, c1, c2, c3 = int32_str
pixels[x, y] = (c0, c1, c2, Texture.scale_alpha(c3))
x += 1
if x == self.w:
x = 0
y += 1
im.save(png_path, "PNG")
print("Unpacked 24:", png_path)
def from_png(self, path):
png_path = os.path.join(path, "{}.png".format(self.offset))
if not os.path.isfile(png_path):
return
im = Image.open(png_path).convert("RGBA")
pixels = im.load()
if self.format == PSMT8:
pal = OrderedDict()
pal_i = 0
i = (self.offset + HEADER_SIZE)
for y in range(self.h):
for x in range(self.w):
c = list(pixels[x, y])
c[3] = Texture.unscale_alpha(c[3])
c = tuple(c)
if c not in pal:
if len(pal) >= 256:
print("Too many colors! Max: 256", png_path)
sys.exit(1)
else:
pal[c] = pal_i
pal_i += 1
b[i] = pal[c]
i += 1
Swizzle8(b, self.w, self.h, self.offset + HEADER_SIZE)
pal = list(pal.keys())
pal += [(0, 0, 0, 0)] * (256 - len(pal)) # Unused colors
pal = swap_palette(pal)
pal_start = self.offset + HEADER_SIZE + self.w * self.h + HEADER_PAL_SIZE
pal_i = 0
for j in range(16):
for i in range(16):
index = pal_start+j*256+i*4
b[index:index+4] = pal[pal_i]
pal_i += 1
print("Packed 8:", png_path)
elif self.format == PSMCT32:
i = (self.offset + HEADER_SIZE) // 4
for y in range(self.h):
for x in range(self.w):
c = list(pixels[x, y])
c[3] = Texture.unscale_alpha(c[3])
b[i*4:i*4+4] = c
i += 1
print("Packed 24:", png_path)
texs = []
def find_texs(filename):
global b, texs
texs = []
print("File:", filename)
print(" Offset Format Width Height")
with open(filename, "rb") as f:
b = f.read()
ind = b.find(b"\xA7\xA7\xA7\xA7\x00\x00\x00\x00")
while ind != -1:
i = ind - 0xC
texs.append(Texture(i, b[i + 76], 1<<b[i + 92], 1<<b[i + 93]))
print("0x{:08X} {:6} {:6} {:6}".format(
texs[-1].offset, texs[-1].format, texs[-1].w, texs[-1].h))
ind = b.find(b"\xA7\xA7\xA7\xA7\x00\x00\x00\x00", ind + 1)
print("Found:", len(texs))
def unpack(filename):
global b
find_texs(filename)
extracted_path = filename + "_extracted"
os.makedirs(extracted_path, exist_ok=True)
for tex in texs:
tex.to_png(extracted_path)
b = None
def pack(filename):
global b
find_texs(filename)
b = bytearray(b)
extracted_path = filename + "_extracted"
for tex in texs:
tex.from_png(extracted_path)
with open(filename + "_new.MGF", "wb") as f:
f.write(b)
b = None
parser = argparse.ArgumentParser(description='Pack/unpack PIC.IMG for [PS2] Silent Hill 2')
parser.add_argument('-u', '--unpack', action='store_true',
help='unpack to {MGF_FILE}_extracted folder')
parser.add_argument('-p', '--pack', action='store_true',
help='pack from {MGF_FILE}_extracted folder to {MGF_FILE}_new.MGF')
parser.add_argument('mgf', metavar='PIC.MGF', type=str,
help='MGF file')
args = parser.parse_args()
if args.unpack or (not args.unpack and not args.pack):
unpack(args.mgf)
elif args.pack:
pack(args.mgf)
input("Press any key...")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment