Files
Nexus/2023/scripts/animation_tools/dwpicker/ingest/animschool/parser.py
2025-11-23 23:31:18 +08:00

276 lines
8.1 KiB
Python

"""
Module to parse and extract data from AnimSchool picker file.
This works for Animschool until 2021 release.
PKR file structure description:
-- header --
4 bytes (singed int): Picker Version.
4 bytes (singed int): Title number (x) of bytes length.
x bytes (hex text): Title.
-- PNG data --
...
--- buttons ---
4 bytes (singed int): Number of buttons
-- Button array --
for _ in range(number_of_buttons)
- 4 bytes (singed int): Button id as signed int.
- 4 bytes (singed int): Center position X.
- 4 bytes (singed int): Center position Y.
- 4 bytes (singed int):
Size for old AnimSchool versions (4 and older)
This is still there but unused in 2021 version.
- 4 bytes (singed int): Width.
- 4 bytes (singed int): Height.
- 4 bytes (bool): Button type.
True = Command button.
False = Selection button.
- 4 bytes (bool): Languages used for command button.
True = Python.
False = Mel.
- 4 bytes (hex __RRGGBB): Background color.
- 4 bytes (hex __RRGGBB): Text color.
- 4 bytes (singed int): Label number (x) of bytes length.
- x bytes (hexa text): Label.
- 4 bytes (singed int): Number (x) of targets.
This is automatically 1 for command button
for _ in range(number_of_targets):
- 4 bytes (singed int): Target name number (x) of bytes length.
- x bytes (hexa text): Target name.
The script export pkr data in 3 different objects:
PNG data:
This is a one to one of the png binari data encapsulated in the pkr
file.
Title:
As simple string
Buttons:
Translate the binari buttons as readable python dict!
{
"id": int,
"x": int,
"y": int,
"w": int,
"h": int,
"action": str: "select" | "command",
"lang": str: "mel" | "python",
"bgcolor": [r:int, g:int, b:int],
"txtcolor": [r:int, g:int, b:int],
"label": str,
"targets": List[str]
}
"""
from binascii import hexlify, unhexlify
import json
import os
PNG_HEADER = b'89504e470d0a1a0a'
PNG_FOOTER = b'ae426082'
def split_data(content, number_of_bytes=4):
if isinstance(number_of_bytes, bytes):
number_of_bytes = int(number_of_bytes, 16)
return content[:number_of_bytes * 2], content[number_of_bytes * 2:]
def bytes_to_string(stringdata):
return ''.join(
b.decode('cp1252')
for b in unhexlify(stringdata).split(b'\x00'))
def bytes_to_int(i):
if i[:4] == b'00' * 2:
return int(i, 16)
elif i[:4] == b'ff' * 2:
return -65535 + int(i[-4:], 16)
raise Exception('Count not interpret data as int')
def print_(data, max_bytes=64):
string = repr(data)[2:-1][:max_bytes * 2]
beautified = ''
for i in range(len(string)):
beautified += string[i].upper()
if i % 2:
beautified += ' '
if (i + 1) % 16 == 0 and i != 0:
beautified += '\n'
print(beautified)
def bytes_to_rgb(data):
data = int(data, 16)
b = data & 255
g = (data >> 8) & 255
r = (data >> 16) & 255
return r, g, b
def extract_string(data):
string_size, data = split_data(data)
string, data = split_data(data, string_size)
string = bytes_to_string(string)
return string, data
def extract_png_data(data):
png_len_size, data = split_data(data)
png_len_size = bytes_to_int(png_len_size)
if not png_len_size:
return None, data
png_len, data = split_data(data, png_len_size)
png_len = int(bytes_to_string(png_len)) # lol
if png_len == 0:
_, data = split_data(data, 4) # remove some leftover data
return None, data
_, data = split_data(data, 4)
png_end = int((data.find(PNG_FOOTER) + len(PNG_FOOTER)) / 2)
return split_data(data, png_end)
def extract_button_targets(data):
number_of_targets, data = split_data(data)
targets = []
number_of_targets = int(number_of_targets, 16)
for _ in range(number_of_targets):
target_name, data = extract_string(data)
targets.append(target_name)
return targets, data
def extract_button_data(data, version=5, verbose=True):
button_id, data = split_data(data)
button_id = bytes_to_int(button_id)
if verbose:
print('Button #{button_id}'.format(button_id=button_id))
x, data = split_data(data)
x = bytes_to_int(x)
y, data = split_data(data)
y = bytes_to_int(y)
old_height, data = split_data(data)
if version > 4:
width, data = split_data(data)
width = bytes_to_int(width)
height, data = split_data(data)
height = bytes_to_int(height)
else:
width, height = bytes_to_int(old_height), bytes_to_int(old_height)
action, data = split_data(data)
action = bytes_to_int(action)
assert action in [0, 1]
action = 'command' if action else 'select'
lang, data = split_data(data)
lang = bytes_to_int(lang)
assert lang in [0, 1]
lang = 'python' if lang else 'mel'
bgcolor, data = split_data(data)
bgcolor = bytes_to_rgb(bgcolor)
txtcolor, data = split_data(data)
txtcolor = bytes_to_rgb(txtcolor)
label_size, data = split_data(data)
if label_size == b'ff' * 4:
label = ''
else:
label, data = split_data(data, label_size)
label = bytes_to_string(label)
targets, data = extract_button_targets(data)
button = dict(
id=button_id, x=x, y=y, w=width, h=height, action=action,
lang=lang, bgcolor=bgcolor, txtcolor=txtcolor, label=label,
targets=targets)
return button, data
def parse_animschool_picker(picker_path, verbose=False):
with open(picker_path, 'rb') as file:
data = hexlify(file.read())
# Get version
version, data = split_data(data)
version = bytes_to_int(version)
print("this picker is build with AnimSchool v" + str(version))
# Get title
title, data = extract_string(data)
if verbose:
print('Title: "{title}"'.format(title=title))
# Extract PNG
png_data, data = extract_png_data(data)
if verbose and png_data:
print('PNG data found')
# Get number of buttons
number_of_buttons, data = split_data(data)
number_of_buttons = int(number_of_buttons, 16)
if verbose:
print('Number of buttons: "{num}"'.format(num=number_of_buttons))
# Parse buttons one by one:
buttons = []
while data:
button, data = extract_button_data(data, version, verbose)
buttons.append(button)
if len(buttons) != number_of_buttons:
raise Exception('Parsing buttons went wrong.')
return title, buttons, png_data
def extract_to_files(pkr_path, verbose=False):
"""
Extract data and image to .json and .png (if any) next to the .pkr
"""
title, buttons, png_data = parse_animschool_picker(pkr_path, verbose)
# Save to json
with open(pkr_path + '.json', 'w') as f:
json.dump([title, buttons], f, indent=4)
# Write PNG to file:
png_path = pkr_path + '.png'
if png_data and not os.path.exists(png_path):
save_png(png_data, png_path)
return title, buttons, png_data
def save_png(png_data, dst):
print('Saving PNG to "{dst}"'.format(dst=dst))
with open(dst, 'wb') as f:
f.write(unhexlify(png_data))
if __name__ == '__main__':
import sys
arg = sys.argv[-1]
if arg == 'dir':
# Extract json and png for all .pkr files in current dir:
import glob
for pkr_path in glob.glob('./*.pkr'):
print(os.path.basename(pkr_path))
try:
extract_to_files(pkr_path)
except BaseException:
print('Failed to parse {pkr_path}'.format(pkr_path=pkr_path))
elif arg.endswith('.pkr') and os.path.exists(arg):
# Extract given path to json and png:
import pprint
print('Parsing {arg}'.format(arg=arg))
title, buttons, png_data = extract_to_files(arg, verbose=True)
print(title)
pprint.pprint(buttons)