Last active
August 25, 2022 21:58
-
-
Save infval/b23380076d77341ccc379a62ef9f23b3 to your computer and use it in GitHub Desktop.
[PS2] Silent Hill 2 PIC.IMG extractor (unpacker/packer)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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