Init commit
This commit is contained in:
+155
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract bitmap images from .egf files (Win32 PE resource containers).
|
||||
|
||||
EGF files are PE binaries whose images Resource Hacker shows as RT_BITMAP
|
||||
(type 2) resources, stored as raw DIBs (BITMAPINFOHEADER, no 14-byte BM
|
||||
file header). This walks the PE resource directory, pulls every bitmap, and
|
||||
writes a proper .bmp by prepending a reconstructed BITMAPFILEHEADER.
|
||||
|
||||
Usage:
|
||||
python3 extract_egf.py [file_or_dir ...]
|
||||
No args -> all *.egf in current directory.
|
||||
"""
|
||||
import struct, sys, os, glob
|
||||
|
||||
RT_BITMAP = 2
|
||||
|
||||
def u16(d, o): return struct.unpack_from('<H', d, o)[0]
|
||||
def u32(d, o): return struct.unpack_from('<I', d, o)[0]
|
||||
|
||||
def get_sections(d):
|
||||
e_lfanew = u32(d, 0x3c)
|
||||
if d[e_lfanew:e_lfanew+4] != b'PE\x00\x00':
|
||||
raise ValueError('not a PE file')
|
||||
coff = e_lfanew + 4
|
||||
nsec = u16(d, coff + 2)
|
||||
sizeopt = u16(d, coff + 16)
|
||||
secoff = coff + 20 + sizeopt
|
||||
secs = []
|
||||
for i in range(nsec):
|
||||
o = secoff + i * 40
|
||||
vaddr = u32(d, o + 12)
|
||||
vsize = u32(d, o + 8)
|
||||
raw = u32(d, o + 20)
|
||||
rsize = u32(d, o + 16)
|
||||
secs.append((vaddr, vsize, raw, rsize))
|
||||
return secs
|
||||
|
||||
def make_rva2off(secs):
|
||||
def rva2off(rva):
|
||||
for vaddr, vsize, raw, rsize in secs:
|
||||
if vaddr <= rva < vaddr + max(vsize, rsize):
|
||||
return rva - vaddr + raw
|
||||
raise ValueError('rva %x not in any section' % rva)
|
||||
return rva2off
|
||||
|
||||
def dib_to_bmp(dib):
|
||||
"""Prepend BITMAPFILEHEADER. Compute pixel-data offset from DIB header."""
|
||||
biSize = u32(dib, 0)
|
||||
biBitCount = u16(dib, 14)
|
||||
biClrUsed = u32(dib, 32)
|
||||
# palette size
|
||||
if biBitCount <= 8:
|
||||
n = biClrUsed if biClrUsed else (1 << biBitCount)
|
||||
palbytes = n * 4
|
||||
else:
|
||||
palbytes = 0
|
||||
# BI_BITFIELDS (compression==3) adds 3 (or 4) DWORD masks
|
||||
biCompression = u32(dib, 16)
|
||||
if biCompression == 3:
|
||||
palbytes = 3 * 4
|
||||
pixoff = 14 + biSize + palbytes
|
||||
fileheader = b'BM' + struct.pack('<IHHI', 14 + len(dib), 0, 0, pixoff)
|
||||
return fileheader + dib
|
||||
|
||||
def parse_dir(d, rsrc_base, off):
|
||||
nnamed = u16(d, off + 12)
|
||||
nid = u16(d, off + 14)
|
||||
base = off + 16
|
||||
out = []
|
||||
for i in range(nnamed + nid):
|
||||
e = base + i * 8
|
||||
nameid = u32(d, e)
|
||||
offfield = u32(d, e + 4)
|
||||
out.append((nameid, offfield))
|
||||
return out
|
||||
|
||||
def resname(d, rsrc_base, nameid):
|
||||
if nameid & 0x80000000:
|
||||
no = rsrc_base + (nameid & 0x7fffffff)
|
||||
ln = u16(d, no)
|
||||
return d[no+2:no+2+ln*2].decode('utf-16le', 'replace')
|
||||
return str(nameid)
|
||||
|
||||
def extract(path, outdir):
|
||||
d = open(path, 'rb').read()
|
||||
secs = get_sections(d)
|
||||
rva2off = make_rva2off(secs)
|
||||
rsrc_va = next(v for (v, vs, r, rs) in secs) # placeholder, fixed below
|
||||
# find .rsrc by RVA range that holds resource dir: use section flagged via data dir? simplest: locate .rsrc by name
|
||||
# reparse to grab .rsrc section base
|
||||
e_lfanew = u32(d, 0x3c); coff = e_lfanew + 4
|
||||
nsec = u16(d, coff + 2); sizeopt = u16(d, coff + 16)
|
||||
secoff = coff + 20 + sizeopt
|
||||
rsrc_va = rsrc_raw = None
|
||||
for i in range(nsec):
|
||||
o = secoff + i * 40
|
||||
if d[o:o+8].rstrip(b'\x00') == b'.rsrc':
|
||||
rsrc_va = u32(d, o + 12); rsrc_raw = u32(d, o + 20)
|
||||
if rsrc_va is None:
|
||||
print(' no .rsrc in %s' % path); return 0
|
||||
rsrc_base = rsrc_raw # offset of resource dir start (= .rsrc raw, since dir at section start)
|
||||
|
||||
count = 0
|
||||
top = parse_dir(d, rsrc_base, rsrc_base)
|
||||
for tid, toff in top:
|
||||
if (tid & 0x7fffffff) != RT_BITMAP or not (toff & 0x80000000):
|
||||
continue
|
||||
namedir = rsrc_base + (toff & 0x7fffffff)
|
||||
for nid, noff in parse_dir(d, rsrc_base, namedir):
|
||||
name = resname(d, rsrc_base, nid)
|
||||
if not (noff & 0x80000000):
|
||||
continue
|
||||
langdir = rsrc_base + (noff & 0x7fffffff)
|
||||
for lid, loff in parse_dir(d, rsrc_base, langdir):
|
||||
if loff & 0x80000000:
|
||||
continue # expect leaf
|
||||
leaf = rsrc_base + (loff & 0x7fffffff)
|
||||
data_rva = u32(d, leaf)
|
||||
size = u32(d, leaf + 4)
|
||||
doff = rva2off(data_rva)
|
||||
dib = d[doff:doff+size]
|
||||
bmp = dib_to_bmp(dib)
|
||||
base = os.path.splitext(os.path.basename(path))[0]
|
||||
safe = name.replace('/', '_').replace('\\', '_')
|
||||
fn = '%s_%s.bmp' % (base, safe)
|
||||
open(os.path.join(outdir, fn), 'wb').write(bmp)
|
||||
count += 1
|
||||
return count
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
files = []
|
||||
if not args:
|
||||
files = sorted(glob.glob('*.egf'))
|
||||
for a in args:
|
||||
if os.path.isdir(a):
|
||||
files += sorted(glob.glob(os.path.join(a, '*.egf')))
|
||||
else:
|
||||
files.append(a)
|
||||
if not files:
|
||||
print('no .egf files'); return
|
||||
total = 0
|
||||
for f in files:
|
||||
outdir = os.path.splitext(f)[0] + '_extracted'
|
||||
os.makedirs(outdir, exist_ok=True)
|
||||
try:
|
||||
n = extract(f, outdir)
|
||||
except Exception as ex:
|
||||
print(' ERROR %s: %s' % (f, ex)); continue
|
||||
print('%s -> %d images (%s)' % (f, n, outdir))
|
||||
total += n
|
||||
print('TOTAL %d images' % total)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert extracted .bmp sprites to .png with the black background made
|
||||
transparent (colorkey transparency).
|
||||
|
||||
The EGF bitmaps store no usable alpha channel (alpha bytes are all zero);
|
||||
transparency is by colorkey, and the background is pure black RGB(0,0,0).
|
||||
This loads each BMP as RGB (ignoring the junk alpha), sets every pure-black
|
||||
pixel to fully transparent, and writes a PNG.
|
||||
|
||||
Usage:
|
||||
python3 to_png.py [dir ...] # default: all gfx*_extracted dirs
|
||||
python3 to_png.py --key RRGGBB ... # custom colorkey (hex), default 000000
|
||||
python3 to_png.py --keep # keep .bmp files (default keeps them)
|
||||
"""
|
||||
import sys, os, glob
|
||||
from PIL import Image
|
||||
|
||||
def convert(path, key):
|
||||
im = Image.open(path).convert('RGB') # force RGB, drop bogus alpha
|
||||
im = im.convert('RGBA')
|
||||
px = im.getdata()
|
||||
kr, kg, kb = key
|
||||
out = [(r, g, b, 0) if (r, g, b) == (kr, kg, kb) else (r, g, b, 255)
|
||||
for (r, g, b, a) in px]
|
||||
im.putdata(out)
|
||||
dst = os.path.splitext(path)[0] + '.png'
|
||||
im.save(dst)
|
||||
return dst
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
key = (0, 0, 0)
|
||||
dirs = []
|
||||
i = 0
|
||||
while i < len(args):
|
||||
if args[i] == '--key':
|
||||
h = args[i+1].lstrip('#')
|
||||
key = (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
|
||||
i += 2
|
||||
else:
|
||||
dirs.append(args[i]); i += 1
|
||||
if not dirs:
|
||||
dirs = sorted(glob.glob('gfx*_extracted'))
|
||||
total = 0
|
||||
for d in dirs:
|
||||
bmps = sorted(glob.glob(os.path.join(d, '*.bmp')))
|
||||
for b in bmps:
|
||||
try:
|
||||
convert(b, key)
|
||||
total += 1
|
||||
except Exception as ex:
|
||||
print(' ERROR %s: %s' % (b, ex))
|
||||
print('%s -> %d png' % (d, len(bmps)))
|
||||
print('TOTAL %d png (colorkey %02x%02x%02x)' % (total, *key))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user