Created
December 18, 2024 03:41
-
-
Save huytrinhm/fe37a6fa01f97275d74245920c36d858 to your computer and use it in GitHub Desktop.
Convert Halfbrick Jetpack Joyride texture files (*.tex) to png
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
| import os | |
| import glob | |
| import struct | |
| from PIL import Image | |
| def check_file_header(file_path): | |
| headers = [ | |
| b'\x54\x45\x58\x01\x00\x00\x00\x01\x04\x02\x04\x03\x04\x04\x04\x01\x00\x00\x00\x00', | |
| b'\x54\x45\x58\x01\x06\x00\x00\x00\x08\x02\x08\x03\x08\x04\x08\x01\x00\x00\x00\x00', | |
| b'\x54\x45\x58\x01\x00\x00\x00\x03\x08\x01\x08\x04\x08\x03\x08\x02\x00\x00\x00\x00', | |
| b'\x54\x45\x58\x01\x00\x00\x00\x00\x08\x02\x08\x03\x08\x04\x00\x00\x00\x00\x00\x00', | |
| b'\x54\x45\x58\x01\x00\x00\x00\x01\x05\x02\x05\x03\x05\x04\x01\x01\x00\x00\x00\x00' | |
| ] | |
| with open(file_path, 'rb') as file: | |
| file_data = file.read(len(headers[0])) | |
| for i, header in enumerate(headers): | |
| if file_data == header: | |
| return i # Return the index of the matching header | |
| return -1 # Return -1 if no header matches | |
| def extract_image_to_png_4_bits(file_path, output_path, image_offset=0x34): | |
| """Extract raw image data and save as PNG.""" | |
| with open(file_path, 'rb') as file: | |
| # Read dimensions (assumes width and height are stored as 16-bit shorts at a fixed offset) | |
| file.seek(0x14) | |
| width, height = struct.unpack('<HH', file.read(4)) | |
| # Read raw image data starting at the image_offset | |
| file.seek(image_offset) | |
| data = file.read(width * height * 2) # 16 bits (2 bytes) per pixel | |
| # Convert raw data to an image (ARGB format) | |
| image = Image.new('RGBA', (width, height)) | |
| pixels = image.load() | |
| for y in range(height): | |
| for x in range(width): | |
| pixel_offset = (y * width + x) * 2 | |
| if pixel_offset + 1 >= len(data): | |
| break | |
| pixel = struct.unpack('<H', data[pixel_offset:pixel_offset + 2])[0] | |
| red = (pixel & 0xF000) >> 12 | |
| green = (pixel & 0x0F00) >> 8 | |
| blue = (pixel & 0x00F0) >> 4 | |
| alpha = (pixel & 0x000F) | |
| # Scale 4-bit color channels (0–15) to 8-bit (0–255) | |
| pixels[x, y] = ( | |
| red * 17, # Scale 0–15 to 0–255 | |
| green * 17, | |
| blue * 17, | |
| alpha * 17 | |
| ) | |
| # Ensure the output directory exists | |
| os.makedirs(os.path.dirname(output_path), exist_ok=True) | |
| # Save the image as PNG | |
| image.save(output_path) | |
| print(f"[INFO] Saved {output_path}") | |
| def extract_image_to_png_8_bits(file_path, output_path, image_offset=0x34): | |
| """Extract raw image data and save as PNG.""" | |
| with open(file_path, 'rb') as file: | |
| # Read dimensions (assumes width and height are stored as 16-bit shorts at a fixed offset) | |
| file.seek(0x14) | |
| width, height = struct.unpack('<HH', file.read(4)) | |
| # Read raw image data starting at the image_offset | |
| file.seek(image_offset) | |
| data = file.read(width * height * 4) # 32 bits (4 bytes) per pixel (8 bits per channel) | |
| # Convert raw data to an image (RGBA format) | |
| image = Image.new('RGBA', (width, height)) | |
| pixels = image.load() | |
| for y in range(height): | |
| for x in range(width): | |
| pixel_offset = (y * width + x) * 4 # 4 bytes per pixel (RGBA) | |
| if pixel_offset + 3 >= len(data): | |
| break | |
| # Unpack the 4 bytes (RGBA) | |
| pixel = struct.unpack('<4B', data[pixel_offset:pixel_offset + 4]) | |
| red, green, blue, alpha = pixel | |
| # Assign directly (no need to scale, since each channel is already 8-bit) | |
| pixels[x, y] = (red, green, blue, alpha) | |
| # Ensure the output directory exists | |
| os.makedirs(os.path.dirname(output_path), exist_ok=True) | |
| # Save the image as PNG | |
| image.save(output_path) | |
| print(f"[INFO] Saved {output_path}") | |
| def parse_directory_and_convert(directory, output_base_dir): | |
| pattern = os.path.join(directory, '**', '*.tex') | |
| tex_files = glob.glob(pattern, recursive=True) | |
| for file_path in tex_files: | |
| header_index = check_file_header(file_path) | |
| relative_path = os.path.relpath(file_path, directory) | |
| output_path = os.path.join(output_base_dir, relative_path + '.png') | |
| if header_index == 0: | |
| extract_image_to_png_4_bits(file_path, output_path) | |
| elif header_index == 2: | |
| extract_image_to_png_8_bits(file_path, output_path) | |
| if __name__ == "__main__": | |
| input_directory = './textures' | |
| output_directory = './textures_png' | |
| parse_directory_and_convert(input_directory, output_directory) |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Currently only support 2 types of texture (header type 0 and header type 2). As of Jetpack Joyride apk version 1.93.2, this works fine for most important textures except level backgrounds (header type 1). However, we can use the apk version 1.48.1 to get access to those background textures, since back then they were still type 2 textures.