Created
February 11, 2022 14:11
-
-
Save dmitmel/6e3d4062fb7bdff6c986e8e8775df494 to your computer and use it in GitHub Desktop.
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 | |
# Font patcher for this game: https://store.steampowered.com/app/1056180/Cathedral/ | |
import gzip | |
import io | |
import json | |
import struct | |
import sys | |
from typing import Any, Dict, List, TypedDict, cast | |
import PIL.Image | |
FONT_RESOURCE_IDS = { | |
"default": "RESOURCES\\FONT\\DEFAULT", | |
"small-gui": "RESOURCES\\FONT\\SMALL-GUI", | |
} | |
SUPPLEMENTARY_FONT_PATHS = { | |
"default": "default-ru.png", | |
"small-gui": "small-gui-ru.png", | |
} | |
PATCHED_CHARACTERS = "абвгдеёжзийклмнопрстуфхцчшщъыьэюяАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ" | |
class MetadataAnimationTrack(TypedDict): | |
start: int | |
end: int | |
fps: int | |
class MetadataAnimation(TypedDict): | |
frameWidth: int | |
frameHeight: int | |
tracks: Dict[str, MetadataAnimationTrack] | |
class MetadataResource(TypedDict): | |
key: str | |
value: str | |
class MetadataLayout(TypedDict): | |
x: int | |
y: int | |
width: int | |
height: int | |
class Metadata(TypedDict): | |
animations: Dict[str, MetadataAnimation] | |
properties: Dict[str, Dict[str, object]] | |
resources: List[MetadataResource] | |
MetadataAtlasLayout = Dict[str, MetadataLayout] | |
def main(argv: List[str]) -> int: | |
pak_file_path = sys.argv[1] | |
with gzip.GzipFile(pak_file_path, "rb") as pak_file: | |
data_len: int | |
(data_len,) = struct.unpack("<i", pak_file.read(4)) | |
metadata: Metadata = json.loads(pak_file.read(data_len).decode("utf8")) | |
(data_len,) = struct.unpack("<i", pak_file.read(4)) | |
atlas_layout: MetadataAtlasLayout = json.loads(pak_file.read(data_len).decode("utf8")) | |
atlas_data = pak_file.read() | |
with PIL.Image.open(io.BytesIO(atlas_data), formats=["PNG"]) as orig_atlas_img: | |
new_atlas_img = orig_atlas_img | |
resources_to_files_mapping = {res["key"]: res["value"] for res in metadata["resources"]} | |
additional_height = 0 | |
for font_name, resource_id in FONT_RESOURCE_IDS.items(): | |
font_sprite = atlas_layout[resources_to_files_mapping[resource_id]] | |
additional_height = max(additional_height, font_sprite["height"]) | |
new_atlas_img = PIL.Image.new( | |
cast(Any, orig_atlas_img.mode), | |
(orig_atlas_img.width, orig_atlas_img.height + additional_height) | |
) | |
new_atlas_img.paste(orig_atlas_img, (0, 0)) | |
font_pos_x = 0 | |
font_pos_y = orig_atlas_img.height | |
for font_name, resource_id in FONT_RESOURCE_IDS.items(): | |
font_sprite = atlas_layout[resources_to_files_mapping[resource_id]] | |
font_animation = metadata["animations"][resource_id] | |
with PIL.Image.open(SUPPLEMENTARY_FONT_PATHS[font_name], formats=["PNG"]) as suppl_img: | |
expected_suppl_width = font_animation["frameWidth"] * len(PATCHED_CHARACTERS) | |
expected_suppl_height = font_animation["frameHeight"] | |
if suppl_img.width != expected_suppl_width or suppl_img.height != expected_suppl_height: | |
raise Exception( | |
f"Invalid dimensions of the supplementary font: {suppl_img.width}x{suppl_img.height}, must be: {expected_suppl_width}x{expected_suppl_height}" | |
) | |
orig_font_img = orig_atlas_img.crop(( | |
font_sprite["x"], | |
font_sprite["y"], | |
font_sprite["x"] + font_sprite["width"], | |
font_sprite["y"] + font_sprite["height"], | |
)) | |
font_sprite["x"] = font_pos_x | |
font_sprite["y"] = font_pos_y | |
font_sprite["width"] = orig_font_img.width + suppl_img.width | |
font_sprite["height"] = max(orig_font_img.height, suppl_img.height) | |
font_pos_x += font_sprite["width"] | |
new_atlas_img.paste(orig_font_img, (font_sprite["x"], font_sprite["y"])) | |
new_atlas_img.paste( | |
suppl_img, (font_sprite["x"] + orig_font_img.width, font_sprite["y"]) | |
) | |
start_idx = orig_font_img.width // font_animation["frameWidth"] | |
for char in PATCHED_CHARACTERS: | |
font_animation["tracks"]["_0x" + char.encode("utf8").hex()] = ( | |
MetadataAnimationTrack(start=start_idx, end=start_idx, fps=1) | |
) | |
start_idx += 1 | |
if new_atlas_img is orig_atlas_img: | |
new_atlas_img = orig_atlas_img.copy() | |
with gzip.GzipFile(pak_file_path, "wb") as pak_file: | |
data: bytes | |
data = json.dumps(metadata).encode("utf8") | |
pak_file.write(struct.pack("<i", len(data))) | |
pak_file.write(data) | |
data = json.dumps(atlas_layout).encode("utf8") | |
pak_file.write(struct.pack("<i", len(data))) | |
pak_file.write(data) | |
data_io = io.BytesIO() | |
new_atlas_img.save(data_io, format="PNG") | |
pak_file.write(data_io.getvalue()) | |
return 0 | |
if __name__ == "__main__": | |
sys.exit(main(sys.argv)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment