PS2 modify texture text of "Evil Dead A Fistful of Boomstick"

fiorelaVEra

Forum Noob
Hello, I want to modify the text texture of this game
Evil Dead A Fistful of Boomstick
to add the characters in Spanish or example ñ

I know that a Russian group translated this game into Russian and modified the textures to change it to the Cyrillic alphabet, so there is a way to do it but I don't know how he did it
Does anyone know how to do it or can you recommend someone to ask me?

The texture files are in PS2 format
For example
CROWDTEX.PS2
CrowdTx1.PS2

thank you so much
 
there is a proof of russian translation
uXwoYFF.jpg
 
someone tell me can extract and inject using python with this comand

Code:
no_clut =  {
  "CLUT_MODE":ImageHill.NO_CLUT
  }


ED_FONT =  {
  "PXL_FILE":"G:\GFX\FONTS.PS2",
  "PXL_OFFSET":0x400,
  "WIDTH":256,
  "HEIGHT":256,
  "PXL_MODE":ImageHill.THIRTY_TWO_BIT_PS2_DIRECT
  }
ImageHill.convertImage(ED_FONT, no_clut, "ED_FONT.PNG", True)

Then you can inject the graphic with

Code:
ImageHill.injectImage(ED_FONT , no_clut, "ED_FONT.PNG")

this is
ImageHill.py
Code:
import sys   
import os
import math
from pathlib import Path
from PIL import Image, ImageOps
import numpy as np
import TIMresource

#CLUT MODES
NO_CLUT = -1
RGB_555 = 0
RGBA_32_PS2 = 1  #0-128 alpha
RGBA_32 = 2      #0-128 alpha no PS2 swizzle
RGBA_5551_PS1 = 3
BITMAP_RGBX = 4

    #PXL MODES
#Indexed
ONE_BIT = 0
TWO_BIT = 1
FOUR_BIT = 2
EIGHT_BIT = 3
#Direct color
FIFTEEN_BIT_DIRECT = 4
TWENTY_FOUR_BIT_DIRECT = 5
THIRTY_TWO_BIT_DIRECT = 6
SIXTEEN_BIT_PS1_DIRECT = 7
THIRTY_TWO_BIT_PS2_DIRECT = 8

#Flip Modes
NO_FLIP = -1
HORIZONTAL = 0
VERTICAL = 1
HORIZONTAL_AND_VERTICAL = 2

def readPXL(file, offset, width, height, mode, inset=-1):
    file.seek(offset)
    buffer = []
    if mode == EIGHT_BIT:
        #buffer = list(file.read(width*height))
        for y in range(height):
            buffer += list(file.read(width))
            
            if inset != -1:
                file.read(inset) #Skip inset bytes if horizontally stacked image
                
    elif mode == FOUR_BIT:
        for y in range(height):
            for x in range(width//2):
                next_byte = int.from_bytes(file.read(1), "little")
                buffer.append(next_byte&0b1111)
                buffer.append((next_byte&0b11110000)>>4)
            if inset != -1:
                file.read(inset) #Skip inset bytes if horizontally stacked image
    elif mode == SIXTEEN_BIT_PS1_DIRECT or mode == FIFTEEN_BIT_DIRECT:
        for y in range(height):
            for x in range(width):
                buffer.append(int.from_bytes(file.read(2), "little"))
            if inset != -1:
                file.read(inset) #Skip inset bytes if horizontally stacked image
                
    elif mode == TWENTY_FOUR_BIT_DIRECT:
        for y in range(height):
            for x in range(width):
                buffer.append(int.from_bytes(file.read(3), "little"))
            if inset != -1:
                file.read(inset) #Skip inset bytes if horizontally stacked image
    elif mode == THIRTY_TWO_BIT_PS2_DIRECT:
        for y in range(height):
            for x in range(width):
                buffer.append(int.from_bytes(file.read(4), "little"))
            if inset != -1:
                file.read(inset) #Skip inset bytes if horizontally stacked image
    return buffer

def getColorCount(mode):
    
    if mode == 0:
        count = 2
    elif mode == 1:
        count = 4
    elif mode == 2:
        count = 16
    elif mode == 3:
        count = 256
    else:
        count = 0
        '''
    match mode:
        case 0:#one bit
            count = 2
        case 1:#two bit
            count = 4
        case 2:#four bit
            count = 16
        case 3:#eight bit
            count = 256
        case _:#no clut or unidentified
            count = 0'''

    return count

def changeBase(num, old_base, new_base):
    fraction = num/old_base
    value = math.floor(fraction*new_base)
    return value

def readCLUT(file, offset, n_entries, mode):
    
    buffer = []
    
    if mode == BITMAP_RGBX:
        file.seek(offset)
        for x in range(n_entries):
            entry_value = int.from_bytes(file.read(4), "little")
            blue  = (entry_value & 0x000000FF)
            green = (entry_value & 0x0000FF00) >> 8
            red   = (entry_value & 0x00FF0000) >> 16
            alpha = 255
            buffer.append((red,green,blue,alpha))
    elif mode == RGBA_5551_PS1:
        file.seek(offset)
        for x in range(n_entries):
            entry_value = int.from_bytes(file.read(2), "little")
            red   = (entry_value & 0b11111) << 3
            #red = changeBase(red, 31, 255)
            green = (entry_value & 0b1111100000) >> 2
            #green = changeBase(green, 31, 255)
            blue  = (entry_value & 0b111110000000000) >> 7
            #blue = changeBase(blue, 31, 255)
            STP =   (entry_value & 0b1000000000000000) >> 15
            if red == green == blue == STP == 0:
                alpha = 0
            else:
                alpha = 255
            
            buffer.append((red,green,blue,alpha))
    elif mode == RGBA_32_PS2:
        file.seek(offset)
        if n_entries > 16:
            for x in range(n_entries//8):
                #Swizzle
                block_number = x % 4
                if block_number == 0:
                    pass
                elif block_number == 1:
                    file.seek(file.tell() + 4*8)
                elif block_number == 2:
                    file.seek(file.tell() - 4*8*2)
                elif block_number == 3:
                    file.seek(file.tell() + 4*8)
                    
                for y in range(8):
                    entry_value = int.from_bytes(file.read(4), "little")
                    red =    entry_value & 0xFF
                    green = (entry_value & 0xFF00)>>8
                    blue =  (entry_value & 0xFF0000)>>16
                    alpha = (entry_value & 0xFF000000)>>24
                    buffer.append((red, green, blue, min(255, alpha*2)))
        else:
            for x in range(n_entries):
                entry_value = int.from_bytes(file.read(4), "little")
                red =    entry_value & 0xFF
                green = (entry_value & 0xFF00)>>8
                blue =  (entry_value & 0xFF0000)>>16
                alpha = (entry_value & 0xFF000000)>>24
                buffer.append((red, green, blue, min(255, alpha*2)))
    elif mode == RGBA_32:
        file.seek(offset)
        for x in range(n_entries):
            entry_value = int.from_bytes(file.read(4), "little")
            red  = (entry_value & 0x000000FF)
            green = (entry_value & 0x0000FF00) >> 8
            blue   = (entry_value & 0x00FF0000) >> 16
            alpha = (entry_value & 0xFF000000)>>24
            buffer.append((red, green, blue, min(255, alpha*2)))
        
    elif mode == NO_CLUT:
        buffer = []
    return buffer


def convertDirectColor(pxl, width, height, color_mode, flip_mode = NO_FLIP):
    
    im = Image.new("RGBA", (width,height), (0, 0, 0, 0))
    
    if color_mode == SIXTEEN_BIT_PS1_DIRECT:
        for y in range(height):
            for x in range(width):
                val = pxl[width*y + x]
                red = val & 0b11111
                green = (val & 0b1111100000) >> 5
                blue = (val & 0b111110000000000) >> 10
                semitransparency_flag = (val & 0x8000)
                
                if red == green == blue == semitransparency_flag == 0:
                    pixel =  (red<<3, green<<3, blue<<3, 0)
                else:
                    pixel =  (red<<3, green<<3, blue<<3, 255)

                im.putpixel((x,y), pixel)
    elif color_mode == THIRTY_TWO_BIT_PS2_DIRECT or color_mode == THIRTY_TWO_BIT_DIRECT:
        for y in range(height):
            for x in range(width):
                val = pxl[width*y + x]
                red    = val & 0xFF
                green = (val & 0xFF00) >> 8
                blue =  (val & 0xFF0000) >> 16
                alpha = (val & 0xFF000000) >> 24
                
                if color_mode == THIRTY_TWO_BIT_DIRECT:
                    pixel = (red, green, blue, alpha)
                elif color_mode == TWENTY_FOUR_BIT_DIRECT:
                    pixel = (red, green, blue, 255)
                else:
                    pixel = (red, green, blue, min(alpha*2, 255))
                
                im.putpixel((x,y), pixel)
    
    
    return im

def getBMP(path, offset):
    file = open(path, 'rb')
    file.seek(0xA)
    pxl_offset = int.from_bytes(file.read(4), "little") + offset
    clut_offset = int.from_bytes(file.read(4), "little") + 0xE + offset

    width  = int.from_bytes(file.read(4), "little")
    height = int.from_bytes(file.read(4), "little")
    
    planes          = int.from_bytes(file.read(2), "little")
    pixel_bit_count = int.from_bytes(file.read(2), "little")
    compression = int.from_bytes(file.read(4), "little")
    
    file.seek(0x2E)
    color_count = int.from_bytes(file.read(4), "little")
    
    clut = {}
    if color_count == 0:
        clut["CLUT_MODE"] = NO_CLUT
    else:
        clut["CLUT_OFFSET"] = clut_offset
        clut["CLUT_FILE"] = path
        clut["CLUT_MODE"] = BITMAP_RGBX
        clut["N_COLORS"] = color_count
    pxl = {}
    pxl["PXL_FILE"] = path
    pxl["PXL_OFFSET"] = pxl_offset
    pxl["FLIP"] = VERTICAL
    
    pxl["HEIGHT"] = height
    pxl["WIDTH"] = width
    if pixel_bit_count == 8:
        pxl["PXL_MODE"] = EIGHT_BIT
    elif pixel_bit_count == 4:
        pxl["PXL_MODE"] = FOUR_BIT
    elif pixel_bit_count == 24:
        pxl["PXL_MODE"] = TWENTY_FOUR_BIT_DIRECT
    return pxl, clut

def getTIM(path, offset, STP_mode=TIMresource.STP_FIFTY_FIFTY):
    file = open(path, 'rb')
    file.read(offset)
    timObj = TIMresource.TIM(file)
    
    if timObj.CF==1:
        cluts = []
        total_colors = (timObj.CLUT.bnum - 0xC)//2
        if timObj.PMD == TIMresource.FOUR_BIT_CLUT:
            palette_colors =  16
        elif timObj.PMD == TIMresource.EIGHT_BIT_CLUT:
            palette_colors =  256
        
        n_cluts = total_colors//palette_colors
        
        for n in range(n_cluts):
            clut = {}
            clut["CLUT_OFFSET"] = timObj.CLUT_offset
            clut["CLUT_FILE"] = path
            clut["CLUT_MODE"] = RGBA_5551_PS1
            if timObj.PMD == TIMresource.FOUR_BIT_CLUT:
                clut["N_COLORS"] = 16
            elif timObj.PMD == TIMresource.EIGHT_BIT_CLUT:
                clut["N_COLORS"] = 256
            else:
                assert False, "Bad PMD mode: " + path
                
            cluts.append(clut)
    
    pxl = {}
    
    pxl["PXL_FILE"] = path
    pxl["PXL_OFFSET"] = timObj.PXL_offset
    pxl["HEIGHT"] = timObj.H
    if timObj.PMD == TIMresource.FOUR_BIT_CLUT:
        pxl["WIDTH"] = timObj.W*4
        pxl["PXL_MODE"] = FOUR_BIT
        
    elif  timObj.PMD == TIMresource.EIGHT_BIT_CLUT:
        pxl["WIDTH"] = timObj.W*2
        pxl["PXL_MODE"] = EIGHT_BIT
    elif timObj.PMD == TIMresource.SIXTEEN_BIT_CLUT:
        pxl["WIDTH"] = timObj.W
        pxl["PXL_MODE"] = SIXTEEN_BIT_PS1_DIRECT
        clut = {"CLUT_MODE":NO_CLUT}
        return pxl, [clut]
    
    return pxl, cluts

def extractTIM(path, offset, outfolder, STP_mode=TIMresource.STP_FIFTY_FIFTY):
    pxl, cluts = getTIM(path, offset)
    
    file_stem = Path(path).stem
    
    for clut_number in range(len(cluts)):
        out_path = os.path.join(outfolder, file_stem + "-offset-" + hex(offset) + "-CLUT-" + hex(clut_number) + ".PNG")
        convertImage(pxl, cluts[clut_number], out_path)
        
    return

def injectTIM(path, offset, PNG_path, clut_number = 0):
    pxl, cluts =  getTIM(path, offset)
    injectImage(pxl, cluts[clut_number], PNG_path)
    return

def convertImage(image_definition, clut_definition, output_path, show_output=False, STP_MODE=TIMresource.STP_OFF):
    image_file = open(image_definition["PXL_FILE"], "rb")
    
    if "PXL_INSET" in image_definition:
        inset = image_definition["PXL_INSET"]
    else:
        inset = -1
    pxl = readPXL(image_file, image_definition["PXL_OFFSET"], image_definition["WIDTH"],image_definition["HEIGHT"], image_definition["PXL_MODE"], inset)
    
    
    height = image_definition["HEIGHT"]
    width = image_definition["WIDTH"]
    
    if clut_definition["CLUT_MODE"] != NO_CLUT:
        #indexed color
        clut_file = open(clut_definition["CLUT_FILE"], "rb")
        n_clut_entries = getColorCount(image_definition["PXL_MODE"])
        clut = readCLUT(clut_file,clut_definition["CLUT_OFFSET"], n_clut_entries, clut_definition["CLUT_MODE"])
        
        im = Image.new("RGBA", (image_definition["WIDTH"],image_definition["HEIGHT"]), (0, 0, 0, 0))

        for y in range(height):
            for x in range(width):
                
                pixel =  clut[pxl[y*image_definition["WIDTH"] + x]]
                
                
                im.putpixel((x,y), pixel)
        if "FLIP" in image_definition:
            if image_definition["FLIP"] == HORIZONTAL:
                im = ImageOps.mirror(im)
            elif image_definition["FLIP"] == VERTICAL:
                im = ImageOps.flip(im)
            elif image_definition["FLIP"] == HORIZONTAL_AND_VERTICAL:
                im = ImageOps.mirror(im)
                im = ImageOps.flip(im)
        if show_output:
            im.show()
        im.save(output_path)
    else:
        #direct color
        im = convertDirectColor(pxl, image_definition["WIDTH"],image_definition["HEIGHT"], image_definition["PXL_MODE"], flip_mode = NO_FLIP)
        
        if "FLIP" in image_definition:
            if image_definition["FLIP"] == HORIZONTAL:
                im = ImageOps.mirror(im)
            elif image_definition["FLIP"] == VERTICAL:
                im = ImageOps.flip(im)
            elif image_definition["FLIP"] == HORIZONTAL_AND_VERTICAL:
                im = ImageOps.mirror(im)
                im = ImageOps.flip(im)
        if show_output:
            im.show()
        im.save(output_path)
    return im

def closest(color,colors, color_dict):
    if color in color_dict:
        return color_dict[color]
    
    if color[3] == 0:
        return getAlpha(colors)
    #colors = np.array(colors)
    color_array = np.array(color)
    
    distances = np.sqrt(np.sum((colors-color_array)**2,axis=1))
    index_of_smallest = int(np.where(distances==np.amin(distances))[0][0])
    #smallest_distance = colors[index_of_smallest]
    color_dict[color] = index_of_smallest
    return index_of_smallest

def getAlpha(palette):
    for color_num in range(len(palette)):
        if palette[color_num][3] == 0:
            return color_num

    #print("ERROR: ALPHA NOT FOUND!!!!")
    return 0

def injectImage(imagedef, clutdef, png_path, STP_mode=TIMresource.STP_OFF):
    print("Injecting PXL:", imagedef, "\nCLUT:", clutdef, "\nPNG:", png_path,"\n")
    #Open and read clut
    if clutdef["CLUT_MODE"] != NO_CLUT:
        clut_parent_path = clutdef["CLUT_FILE"]
        clut_parent_file = open(clut_parent_path, "rb")
        clut = readCLUT(clut_parent_file, clutdef["CLUT_OFFSET"], clutdef["N_COLORS"], clutdef["CLUT_MODE"])
        clut = np.array(clut)
        pass
    
    #Open pxl
    pxl_parent_path = imagedef["PXL_FILE"]
    pxl_parent_file = open(pxl_parent_path, "r+b")
    pxl_parent_file.seek(imagedef["PXL_OFFSET"])
    
    edited_im = Image.open(png_path).convert("RGBA")
    
    color_dict = {}
    
    if "FLIP" in imagedef:
        if imagedef["FLIP"] == HORIZONTAL:
            edited_im = ImageOps.mirror(edited_im)
        elif imagedef["FLIP"] == VERTICAL:
            edited_im = ImageOps.flip(edited_im)
        elif imagedef["FLIP"] == HORIZONTAL_AND_VERTICAL:
            edited_im = ImageOps.mirror(edited_im)
            edited_im = ImageOps.flip(edited_im)
    
    pxl_mode = imagedef["PXL_MODE"]
    if pxl_mode == ONE_BIT:
        #TODO
        pass
    elif pxl_mode == TWO_BIT:
        for y in range(imagedef["HEIGHT"]):
            for x in range(imagedef["WIDTH"]//4):
                x1 = x*4
                y1 = y
                edit_color1 = edited_im.getpixel((x1, y1))
                val1 = closest(edit_color1, clut, color_dict)
                
                x2 = x*4 + 1
                y2 = y
                edit_color2 = edited_im.getpixel((x2, y2))
                val2 = closest(edit_color2, clut, color_dict)
                
                x3 = x*4 + 2
                y3 = y
                edit_color3 = edited_im.getpixel((x3, y3))
                val3 = closest(edit_color3, clut, color_dict)
                
                x4 = x*4 + 3
                y4 = y
                edit_color4 = edited_im.getpixel((x4, y4))
                val4 = closest(edit_color4, clut, color_dict)
                
                new_byte = val1 | (val2 << 2) | (val3 << 4) | (val4 << 6)
                pxl_parent_file.write(new_byte.to_bytes(1, "little"))
            
            if "PXL_INSET" in imagedef:
                pxl_parent_file.read(imagedef["PXL_INSET"])
    elif pxl_mode == FOUR_BIT:
        for y in range(imagedef["HEIGHT"]):
            for x in range(imagedef["WIDTH"]//2):
                x1 = x*2
                y1 = y
                edit_color1 = edited_im.getpixel((x1, y1))
                val1 = closest(edit_color1, clut, color_dict)
                
                x2 = x*2 + 1
                y2 = y
                edit_color2 = edited_im.getpixel((x2, y2))
                val2 = closest(edit_color2, clut, color_dict)
                new_byte = val1 | (val2 << 4)
                pxl_parent_file.write(new_byte.to_bytes(1, "little"))
            
            if "PXL_INSET" in imagedef:
                pxl_parent_file.read(imagedef["PXL_INSET"])
    elif pxl_mode == EIGHT_BIT:
        for y in range(imagedef["HEIGHT"]):
            for x in range(imagedef["WIDTH"]):
                edit_color = edited_im.getpixel((x, y))
                val = closest(edit_color, clut, color_dict)
                pxl_parent_file.write(val.to_bytes(1, "little"))
                
            if "PXL_INSET" in imagedef:
                pxl_parent_file.read(imagedef["PXL_INSET"])
    elif pxl_mode == SIXTEEN_BIT_PS1_DIRECT:
        for y in range(imagedef["HEIGHT"]):
            for x in range(imagedef["WIDTH"]):
                color = edited_im.getpixel((x, y))
                red = color[0]>>3
                green = color[1] >> 3
                blue = color[2] >> 3
                alpha = color[3]
                if alpha == red == green == blue == 0:
                    stp = 0
                else:
                    stp = 1
                    
                val = red | (green <<5) | (blue << 10) | (stp << 15) #R,G,B
                pxl_parent_file.write(val.to_bytes(2, "little"))
                
            if "PXL_INSET" in imagedef:
                pxl_parent_file.read(imagedef["PXL_INSET"])
    elif pxl_mode == THIRTY_TWO_BIT_PS2_DIRECT:
        for y in range(imagedef["HEIGHT"]):
            for x in range(imagedef["WIDTH"]):
                color = edited_im.getpixel((x, y))
                #red = color[0]
                #green = color[1]
                #blue = color[2]
                alpha = (color[3] + 1) //2
                
                pxl_parent_file.write(color[0].to_bytes(1, "little"))
                pxl_parent_file.write(color[1].to_bytes(1, "little"))
                pxl_parent_file.write(color[2].to_bytes(1, "little"))
                pxl_parent_file.write(alpha.to_bytes(1, "little"))
                
            if "PXL_INSET" in imagedef:
                pxl_parent_file.read(imagedef["PXL_INSET"])
    elif pxl_mode == TWENTY_FOUR_BIT_DIRECT:
        for y in range(imagedef["HEIGHT"]):
            for x in range(imagedef["WIDTH"]):
                color = edited_im.getpixel((x, y))
                #red = color[0]
                #green = color[1]
                #blue = color[2]
                
                pxl_parent_file.write(color[0].to_bytes(1, "little"))
                pxl_parent_file.write(color[1].to_bytes(1, "little"))
                pxl_parent_file.write(color[2].to_bytes(1, "little"))
    return

and this is
TIMresources.py

Code:
#Contains a number of resources for using PS1 image files
from PIL import Image,ImageDraw
import os
import numpy as np
import copy

#User configurable values
OUTPUT_FOLDER = "OUTPUT"
LOG_FOLDER = "LOG"
#End of user configurable values

FOUR_BIT_CLUT    = 0
EIGHT_BIT_CLUT   = 1
SIXTEEN_BIT_CLUT = 2
TWENTYFOUR_BIT_CLUT = 3
TIM_ID = 0x10
FOUR_BIT_MASK_1  = 0b0000000000001111
FOUR_BIT_MASK_2  = 0b0000000011110000
FOUR_BIT_MASK_3  = 0b0000111100000000
FOUR_BIT_MASK_4  = 0b1111000000000000

EIGHT_BIT_MASK_1 = 0b0000000011111111
EIGHT_BIT_MASK_2 = 0b1111111100000000

#STP MODES back, fore
STP_OFF = 0
STP_FIFTY_FIFTY = 1
STP_HUNDRED_HUNDRED = 2
STP_HUNDRED_INVERT = 3
STP_HUNDRED_TWENTYFIVE = 4

#Reads little endian int from file and advances cursor
def readInt(file):
    integer = int.from_bytes(file.read(4), byteorder='little')
    return integer

#Reads little endian short from file and advances cursor
def readShort(file):
    readShort = int.from_bytes(file.read(2), byteorder='little')
    return readShort

#Reads little endian byte from file and advances cursor
def readByte(file):
    readByte = int.from_bytes(file.read(1), byteorder='little')
    return readByte

def readPXLEntries(file, PMODE, fileLength):
    PXL_Entries = []
    for x in range(fileLength // 2):
        pixels = readShort(file)
        if PMODE == FOUR_BIT_CLUT:
            PXL_Entries +=  [pixels & FOUR_BIT_MASK_1]
            PXL_Entries += [(pixels & FOUR_BIT_MASK_2) >> 4]
            PXL_Entries += [(pixels & FOUR_BIT_MASK_3) >> 8]
            PXL_Entries += [(pixels & FOUR_BIT_MASK_4) >> 12]

        elif PMODE == EIGHT_BIT_CLUT:
            PXL_Entries += [pixels & EIGHT_BIT_MASK_1]
            PXL_Entries += [(pixels & EIGHT_BIT_MASK_2) >> 8]

        elif PMODE == SIXTEEN_BIT_CLUT:
            PXL_Entries += [pixels]

        else:
            raise ValueError("UNRECOGNIZED PMODE")

    return PXL_Entries

def readCLTEntries(file, numEntries):
    CLT_Entries = []
    for x in range(numEntries):
        entry = readShort(file)

        red   =  entry & 0b0000000000011111
        green = (entry & 0b0000001111100000) >> 5
        blue  = (entry & 0b0111110000000000) >>10
        alpha = (entry & 0b1000000000000000) >> 15

        CLT_Entries += [[red, green, blue, alpha]]


    return CLT_Entries


class CLUT:
    def __init__(self, *args):

        if len(args) == 1:
            
            self.bnum = readInt(args[0])
            self.DX = readShort(args[0])
            self.DY = readShort(args[0])
            self.W = readShort(args[0])
            self.H = readShort(args[0])

            self.palette = readCLTEntries(args[0], (self.bnum - 0xC)//2)
        
        elif len(args) == 9:
            self.bnum = args[0]
            self.DX = args[1]
            self.DY = args[2]
            self.W = args[3]
            self.H = args[4]

            self.palette = args[5]
        else:
            raise ValueError("CLT constructor arguments must be 1 file or 8 parameters!")

#PXL Classes
class PXL:# file OR ID, version, PMD, bnum, DX, DY, W, H, PXLData
    def __init__(self, *args):
        if len(args) == 1:
            self.ID = readByte(args[0])
            self.version = readByte(args[0])
            args[0].read(2)
            self.PMD = readInt(args[0]) & 0b1

            self.bnum = readInt(args[0])
            self.DX = readShort(args[0])
            self.DY = readShort(args[0])
            self.W = readShort(args[0])
            self.H = readShort(args[0])
            self.PXLData = readPXLEntries(args[0], self.PMD, self.bnum - 0xC)
            self.TPN = (self.DX//64) + (self.DY//256)*16
        
        elif len(args) == 9:
            self.ID = args[0]
            self.version = args[1]
            self.PMD = args[2]
            self.bnum = args[3]
            self.DX = args[4]
            self.DY = args[5]
            self.W = args[6]
            self.H = args[7]
            self.PXLData = args[8]
            self.TPN = (self.DX//64) + (self.DY//256)*16
        
        else:
            raise ValueError("PXL constructor arguments must be 1 file or 9 parameters!")


class TIM:
    def __init__(self, *args):
        if len(args) == 1:
            self.ID = readByte(args[0])
            self.version = readByte(args[0])
            self.ID_upper = readShort(args[0])
            self.Flag = readInt(args[0])
            self.PMD = self.Flag & 0b111
            self.CF = (self.Flag & 0b1000) >>3
            if self.CF == 0b1:
                self.CLUT_offset = args[0].tell() + 0xC
                self.CLUT = CLUT(args[0])

            self.bnum = readInt(args[0])
            self.DX = readShort(args[0])
            self.DY = readShort(args[0])
            self.W = readShort(args[0])
            self.H = readShort(args[0])
            self.PXL_offset = args[0].tell()
            self.PXLData = readPXLEntries(args[0], self.PMD, self.bnum - 0xC)
            
            self.TPN = (self.DX//64) + (self.DY//256)*16
        
        elif len(args) == 10:
            self.ID = args[0]
            self.version = args[1]
            self.PMD = args[2]
            self.CF = args[3]
            self.bnum = args[4]
            self.DX = args[5]
            self.DY = args[6]
            self.W = args[7]
            self.H = args[8]
            self.PXLData = args[9]
            self.TPN = (self.DX//64) + (self.DY//256)*16

        elif len(args) == 11:
            self.ID = args[0]
            self.version = args[1]
            self.PMD = args[2]
            self.CF = args[3]
            self.bnum = args[4]
            self.DX = args[5]
            self.DY = args[6]
            self.W = args[7]
            self.H = args[8]
            self.PXLData = args[9]
            self.CLUT = args[10]
            self.TPN = (self.DX//64) + (self.DY//256)*16
        
        else:
            raise ValueError("TIM constructor arguments must be 1 file or 10 parameters(no clut) or 11 parameters(clut)!")
        
        

    def to_bin(self):
        buffer = b''
        buffer += self.ID.to_bytes(1,'little')
        buffer += self.version.to_bytes(1,'little')
        buffer += self.ID_upper.to_bytes(2,'little')
        buffer += self.Flag.to_bytes(4,'little')

        if self.CF != 0:
            buffer += self.CLUT.bnum.to_bytes(4,'little')
            buffer += self.CLUT.DX.to_bytes(2,'little')
            buffer += self.CLUT.DY.to_bytes(2,'little')
            buffer += self.CLUT.W.to_bytes(2,'little')
            buffer += self.CLUT.H.to_bytes(2,'little')
            for palette in self.CLUT.palette:
                pixel = palette[0] + (palette[1] << 5) + (palette[2] << 10) + (palette[3] << 15)
                buffer += pixel.to_bytes(2, 'little')
            
        buffer += self.bnum.to_bytes(4,'little')
        buffer += self.DX.to_bytes(2,'little')
        buffer += self.DY.to_bytes(2,'little')
        buffer += self.W.to_bytes(2,'little')
        buffer += self.H.to_bytes(2,'little')
        if self.PMD == FOUR_BIT_CLUT:
            entry_buffer = 0
            entry_counter = 0
            for entry in self.PXLData:
                if entry_counter % 2 != 0:
                    entry_buffer += entry << 4
                    buffer += entry_buffer.to_bytes(1,'little')
                    entry_buffer = 0
                else:
                    entry_buffer += entry
                entry_counter += 1
                pass
        elif self.PMD == EIGHT_BIT_CLUT:
            for entry in self.PXLData:
                buffer += entry.to_bytes(1,'little')
        elif self.PMD == SIXTEEN_BIT_CLUT:
            for entry in self.PXLData:
                buffer += entry.to_bytes(2,'little')
        else:
            raise ValueError("TIM PMODE UNRECOGNIZED")

        return buffer

    def get_PXL_start(self):
        pxl_start = 8 + 0xC
        if self.CF != 0:
            pxl_start += self.CLUT.bnum
        
        return pxl_start

def isTIM(filePath):
    testFile = open(filePath, 'rb')
    
    ext = int.from_bytes(testFile.read(4), byteorder="little")

    TIM_EXT = 0x00000010

    if ext == TIM_EXT:
        return True
    else:
        return False

#Convert a PXL array into PXL binary data
def PXL_data_to_bytes(PXL_data, PMODE):
    if PMODE == FOUR_BIT_CLUT:
        bits_per_entry = 4
    elif PMODE == EIGHT_BIT_CLUT:
        bits_per_entry = 8
    elif PMODE == SIXTEEN_BIT_CLUT:
        bits_per_entry = 16
        
    byte_buffer = b''
    pixels_added = 0
    

    while pixels_added < len(PXL_data):
        if PMODE == SIXTEEN_BIT_CLUT:
            byte_1 = (PXL_data[pixels_added] & 0b1111111100000000) >> 8
            pixels_added += 1
            byte_2 =  PXL_data[pixels_added] & 0b0000000011111111
            pixels_added += 1

            byte_buffer += (byte_1 + byte_2).to_bytes(2, "little")

        elif PMODE == EIGHT_BIT_CLUT:
            byte_buffer += PXL_data[pixels_added].to_bytes(1, "little")
            pixels_added += 1
        
        elif PMODE == FOUR_BIT_CLUT:
            nibble_1 = PXL_data[pixels_added]
            nibble_2 = PXL_data[pixels_added + 1] << 4

            byte_buffer += (nibble_1 & nibble_2).to_bytes(1, "little")
            pixels_added += 1


    return byte_buffer


def getDiffPixels(image1, image2):
    '''Returns an array of x,y coords of pixels that mismatch in image 1 and 2'''

    changedPixels = []
    if image1.height != image2.height or image1.width != image2.width:
        raise ValueError("Image dimensions for diff must match")

    for y in range(image1.height):
        for x in range(image1.width):
            if image1.getpixel((x,y))[3] == 0 and image2.getpixel((x,y))[3] == 0:
                continue
            elif not np.array_equal(image1.getpixel((x,y)),image2.getpixel((x,y))):
                changedPixels += [[x,y]]
    return changedPixels

def injectPNG(originalPNGPath, newPNGPath, PXL_data, CLUT_data):
    '''Returns a PXL object of a PNG image updated with '''
    newPNGImage = Image.open(newPNGPath).convert('RGBA')
    originalPNGImage = Image.open(originalPNGPath).convert('RGBA')

    changedPixels = getDiffPixels(newPNGImage, originalPNGImage)
    
    new_PXL = copy.deepcopy(PXL_data)
    
    for changedPixel in changedPixels:
        x = changedPixel[0]
        y = changedPixel[1]

        rawPixel = newPNGImage.getpixel((x,y))
        try:
            searchPixel = [rawPixel[0] >>3, rawPixel[1] >>3, rawPixel[2] >>3, 0]
            newPixel = CLUT_data.index(searchPixel)
        except ValueError:
            searchPixel = [rawPixel[0] >>3, rawPixel[1] >>3, rawPixel[2] >>3, 1]
            newPixel = CLUT_data.index(searchPixel)
        new_PXL[x + originalPNGImage.width*y] = newPixel
            

    for change in changedPixels:
        newPNGImage.putpixel((change[0],change[1]), (255,0,0))

    #newPNGImage.show()
    return new_PXL

def PNG_to_TIM(originalTIMPath, modifiedPNGPath):
    TIMfile = open(originalTIMPath, 'rb')

    TIM_obj = TIM(TIMfile)
    original_image = generatePNG(TIM_obj.PXLData, TIM_obj.CLUT.palette, TIM_obj.W, TIM_obj.H, 0, TIM_obj.PMD)
    original_image.save("temp.PNG")

    newPXL = injectPNG("temp.PNG", modifiedPNGPath, TIM_obj.PXLData, TIM_obj.CLUT.palette)

    TIM_obj.PXLData = newPXL

    newTIMbinary = TIM_obj.to_bin()

    return newTIMbinary

#Produces an Image from a given PXL and CLUT
def generatePNG(PXL_data, CLUT_data, width, height, CLUT_Number, PMODE):
    if not os.path.exists(OUTPUT_FOLDER):
        os.mkdir(OUTPUT_FOLDER)

    if PMODE == FOUR_BIT_CLUT:
        CLUT_ENTRIES = 0x10
        width = width * 4
    elif PMODE == EIGHT_BIT_CLUT:
        CLUT_ENTRIES = 0x100
        width = width * 2
    elif PMODE == SIXTEEN_BIT_CLUT:
        for pixelNumber in range(len(PXL_data)):
            value = PXL_data[pixelNumber]
            
            red   = value & 0b0000000000011111
            green = value & 0b0000001111100000
            blue  = value & 0b0111110000000000
            alpha = value & 0b1000000000000000
            
            if red == green == blue == alpha == 0:
                alpha = 0
            else:
                alpha = 255

            
            rowNumber    = pixelNumber//width
            columnNumber = pixelNumber % width
            arrayBuffer[rowNumber][columnNumber] = (red, green, blue, alpha)
            im = Image.fromarray(arrayBuffer, "RGBA")

            return im

    arrayBuffer = np.zeros((height, width, 4), dtype=np.uint8)

    
    CLUT_Offset = CLUT_Number * CLUT_ENTRIES


    for pixelNumber in range(len(PXL_data)):
        value = PXL_data[pixelNumber]

        CLUT_Entry = CLUT_Offset + value
        
        red   = CLUT_data[CLUT_Entry][0] << 3
        green = CLUT_data[CLUT_Entry][1] << 3
        blue  = CLUT_data[CLUT_Entry][2] << 3
        alpha = CLUT_data[CLUT_Entry][3] << 3
        
        if red == green == blue == alpha == 0:
            alpha = 0
        else:
            alpha = 255

        
        rowNumber    = pixelNumber//width
        columnNumber = pixelNumber % width
        arrayBuffer[rowNumber][columnNumber] = (red, green, blue, alpha)


    im = Image.fromarray(arrayBuffer, "RGBA")
    #im.show()

    return im

i have installed python with NUMPY and PIL installed
but dont know how run the command for extract
can anyone help me? thanks
 

Similar threads

Back
Top