Skip to content

Instantly share code, notes, and snippets.

@huytrinhm
Created December 18, 2024 03:41
Show Gist options
  • Select an option

  • Save huytrinhm/fe37a6fa01f97275d74245920c36d858 to your computer and use it in GitHub Desktop.

Select an option

Save huytrinhm/fe37a6fa01f97275d74245920c36d858 to your computer and use it in GitHub Desktop.
Convert Halfbrick Jetpack Joyride texture files (*.tex) to png
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)
@huytrinhm

huytrinhm commented Dec 18, 2024

Copy link
Copy Markdown
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment