Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Icon extractor #350

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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