Skip to content

Commit

Permalink
Add icon extractor
Browse files Browse the repository at this point in the history
  • Loading branch information
Renzo904 committed Dec 3, 2024
1 parent af4d87c commit 348f0c4
Show file tree
Hide file tree
Showing 6 changed files with 8,288 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ build
diff
*.obj
*.exe
resources/original.ico
log.txt
scripts/dls
scripts/prefix
Expand Down
11 changes: 10 additions & 1 deletion scripts/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@ def main():
)
parser.add_argument(
"--build-type",
choices=["normal", "diffbuild", "tests", "dllbuild", "objdiffbuild"],
choices=[
"normal",
"diffbuild",
"tests",
"dllbuild",
"objdiffbuild",
"binary_matchbuild",
],
default="normal",
)
parser.add_argument(
Expand Down Expand Up @@ -84,6 +91,8 @@ def main():
build_type = BuildType.DLLBUILD
elif args.build_type == "objdiffbuild":
build_type = BuildType.OBJDIFFBUILD
elif args.build_type == "binary_matchbuild":
build_type = BuildType.BINARY_MATCHBUILD

if args.object_name is not None:
object_name = Path(args.object_name).name
Expand Down
18 changes: 17 additions & 1 deletion scripts/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class BuildType(Enum):
TESTS = 3
DLLBUILD = 4
OBJDIFFBUILD = 5
BINARY_MATCHBUILD = 6


def configure(build_type):
Expand All @@ -36,6 +37,11 @@ def configure(build_type):
if build_type in [BuildType.DIFFBUILD, BuildType.DLLBUILD]:
writer.variable("cl_flags", "$cl_flags /DDIFFBUILD")
writer.variable("cl_flags_pbg3", "$cl_flags_pbg3 /DDIFFBUILD")

if build_type == BuildType.BINARY_MATCHBUILD:
writer.variable("cl_flags", "$cl_flags /DBINARYMATCHBUILD")
writer.variable("cl_flags_pbg3", "$cl_flags_pbg3 /DBINARYMATCHBUILD")

writer.variable("rc", "rc.exe")
writer.variable("link", "link.exe")
writer.variable(
Expand Down Expand Up @@ -69,6 +75,7 @@ def configure(build_type):
"copyicon",
'python -c "import shutil; import sys; shutil.copyfile(sys.argv[1], sys.argv[2])" $in $out',
)
writer.rule("extracticon", "python scripts/extract_icon.py --output $out")
writer.rule(
"geni18n",
"""python -c "import sys; open(sys.argv[2], 'wb').write(open(sys.argv[1], 'rb').read().decode('utf8').encode('shift_jis'))" $in $out""",
Expand Down Expand Up @@ -259,7 +266,16 @@ def configure(build_type):
implicit=["scripts/generate_globals.py"],
)
writer.build("$builddir/autogenerated/i18n.hpp", "geni18n", "src/i18n.tpl")
writer.build("$builddir/icon.ico", "copyicon", "resources/placeholder.ico")
if build_type == BuildType.BINARY_MATCHBUILD:
writer.build("resources/original.ico", "extracticon")
writer.build(
"$builddir/icon.ico",
"copyicon",
"resources/original.ico",
implicit="resources/original.ico",
)
else:
writer.build("$builddir/icon.ico", "copyicon", "resources/placeholder.ico")
writer.build(
"$builddir/th06.res",
"rc",
Expand Down
30 changes: 30 additions & 0 deletions scripts/extract_icon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import icon_extractor
from pathlib import Path
import sys
import os
import argparse


SCRIPT_PATH = Path(os.path.realpath(__file__)).parent
RESOURCES_PATH = SCRIPT_PATH.parent / "resources"
FILENAME = RESOURCES_PATH / "game.exe"


parser = argparse.ArgumentParser(
prog="extract_icon", description="Extract the original icon from the game."
)
parser.add_argument(
"-o", "--output", required=True, help="Path to write the extracted icon"
)
args = parser.parse_args()

if not FILENAME.exists():
sys.stderr.write(
"extract_icon.py: 'game.exe' not found. Copy your executable of Touhou 06 to 'resources/game.exe'"
)
sys.exit(1)
icon = icon_extractor.ExtractIcon(str(FILENAME))

with open(str(args.output), "wb") as icon_file:
entries = icon.get_group_icons()
icon_file.write(icon.export_raw(entries[0], 0))
169 changes: 169 additions & 0 deletions scripts/icon_extractor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import struct
import pefile
from builtins import range

"""
extract-icon-py - Extract Icon from PE Executable using Python
---
Forked into a single library
Original can be found @ https://github.com/firodj/extract-icon-py
Licensed MIT.
"""


class ExtractIcon(object):
GRPICONDIRENTRY_format = (
"GRPICONDIRENTRY",
(
"B,Width",
"B,Height",
"B,ColorCount",
"B,Reserved",
"H,Planes",
"H,BitCount",
"I,BytesInRes",
"H,ID",
),
)
GRPICONDIR_format = ("GRPICONDIR", ("H,Reserved", "H,Type", "H,Count"))
RES_ICON = 1
RES_CURSOR = 2

def __init__(self, filepath):
self.pe = pefile.PE(filepath)

def find_resource_base(self, type):
rt_base_idx = [
entry.id for entry in self.pe.DIRECTORY_ENTRY_RESOURCE.entries
].index(pefile.RESOURCE_TYPE[type])

if rt_base_idx is not None:
return self.pe.DIRECTORY_ENTRY_RESOURCE.entries[rt_base_idx]

return None

def find_resource(self, type, res_index):
rt_base_dir = self.find_resource_base(type)

if res_index < 0:
try:
idx = [entry.id for entry in rt_base_dir.directory.entries].index(
-res_index
)
except Exception:
return None
else:
idx = res_index if res_index < len(rt_base_dir.directory.entries) else None

if idx is None:
return None

test_res_dir = rt_base_dir.directory.entries[idx]
res_dir = test_res_dir
if test_res_dir.struct.DataIsDirectory:
# another Directory
# probably language take the first one
res_dir = test_res_dir.directory.entries[0]
if res_dir.struct.DataIsDirectory:
# Ooooooooooiconoo no !! another Directory !!!
return None

return res_dir

def get_group_icons(self):
rt_base_dir = self.find_resource_base("RT_GROUP_ICON")
groups = list()
for res_index in range(0, len(rt_base_dir.directory.entries)):
grp_icon_dir_entry = self.find_resource("RT_GROUP_ICON", res_index)

if not grp_icon_dir_entry:
continue

data_rva = grp_icon_dir_entry.data.struct.OffsetToData
size = grp_icon_dir_entry.data.struct.Size
data = self.pe.get_memory_mapped_image()[data_rva : data_rva + size]
file_offset = self.pe.get_offset_from_rva(data_rva)

grp_icon_dir = pefile.Structure(
self.GRPICONDIR_format, file_offset=file_offset
)
grp_icon_dir.__unpack__(data)

if grp_icon_dir.Reserved != 0 or grp_icon_dir.Type != self.RES_ICON:
continue
offset = grp_icon_dir.sizeof()

entries = list()
for idx in range(0, grp_icon_dir.Count):
grp_icon = pefile.Structure(
self.GRPICONDIRENTRY_format, file_offset=file_offset + offset
)
grp_icon.__unpack__(data[offset:])
offset += grp_icon.sizeof()
entries.append(grp_icon)

groups.append(entries)
return groups

def best_icon(self, entries):
b = 0
w = 0
best = None
for i in range(len(entries)):
icon = entries[i]
if icon.BitCount > b:
b = icon.BitCount
best = i
if icon.Width > w and icon.BitCount == b:
w = icon.Width
b = icon.BitCount
best = i
return best

def get_icon(self, index):
icon_entry = self.find_resource("RT_ICON", -index)
if not icon_entry:
return None

data_rva = icon_entry.data.struct.OffsetToData
size = icon_entry.data.struct.Size
data = self.pe.get_memory_mapped_image()[data_rva : data_rva + size]

return data

def export_raw(self, entries, index=None):
if index is not None:
entries = entries[index : index + 1]

ico = struct.pack("<HHH", 0, self.RES_ICON, len(entries))
data_offset = None
data = []
info = []
for grp_icon in entries:
if data_offset is None:
data_offset = len(ico) + ((grp_icon.sizeof() + 2) * len(entries))

nfo = grp_icon.__pack__()[:-2] + struct.pack("<L", data_offset)
info.append(nfo)

raw_data = self.get_icon(grp_icon.ID)
if not raw_data:
continue

data.append(raw_data)
data_offset += len(raw_data)

raw = ico + b"".join(info + data)
return raw

def _get_bmp_header(self, data):
if data[0:4] == b"\x89PNG":
header = b""
else:
dib_size = struct.unpack("<L", data[0:4])[0]
header = b"BM" + struct.pack("<LLL", len(data) + 14, 0, 14 + dib_size)
return header
Loading

0 comments on commit 348f0c4

Please sign in to comment.