Skip to content

Commit 173f9af

Browse files
committed
Add BNET icon file (BNI) parsing
Adds Pillow, a PIL fork, as an external requirement. Can extract individual icons from the BNI and save them as other formats.
1 parent 4b1e6af commit 173f9af

File tree

4 files changed

+139
-2
lines changed

4 files changed

+139
-2
lines changed

bncs/bni.py

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
2+
from struct import unpack
3+
import sys
4+
from tempfile import TemporaryFile
5+
6+
from utils import unmake_dword
7+
8+
from PIL import Image as ImageProc
9+
10+
11+
class IconEntry:
12+
def __init__(self, flags, width, height, top):
13+
self.flags = flags
14+
self.x = width
15+
self.y = height
16+
self.codes = []
17+
self.top = top
18+
self.image = None
19+
20+
@property
21+
def width(self):
22+
return self.x
23+
24+
@property
25+
def height(self):
26+
return self.y
27+
28+
@property
29+
def left(self):
30+
return 0
31+
32+
@property
33+
def size(self):
34+
return self.width, self.height
35+
36+
def get_name(self):
37+
return f"Flags_0x{self.flags:08X}" if self.flags > 0 else \
38+
f"Code_{self.codes[0]}" if len(self.codes) > 0 else \
39+
f"Unknown_{self.top}"
40+
41+
42+
class BnetIconFile:
43+
def __init__(self, fp):
44+
self.path = fp
45+
self.version = 0
46+
self.count = 0
47+
self.offset = 0
48+
self.icons = []
49+
self.image = None
50+
51+
@classmethod
52+
def load(cls, file_path):
53+
"""Loads and parses a BNI file"""
54+
obj = cls(file_path)
55+
obj.parse()
56+
return obj
57+
58+
def parse(self):
59+
"""Parses the BNI header from the file"""
60+
with open(self.path, 'rb') as fh:
61+
header_size = unpack('<I', fh.read(4))[0]
62+
self.version, _, self.count, self.offset = unpack('<HHII', fh.read(header_size - 4))
63+
64+
for i in range(self.count):
65+
top = 0 if i == 0 else (self.icons[-1].top + self.icons[-1].height)
66+
icon = IconEntry(*unpack('<III', fh.read(12)), top)
67+
68+
if icon.flags == 0:
69+
while (code := unpack('<I', fh.read(4))[0]) != 0:
70+
icon.codes.append(unmake_dword(code))
71+
else:
72+
fh.read(4)
73+
74+
self.icons.append(icon)
75+
76+
def open_image(self):
77+
"""Reads the image data and crops out individual icons"""
78+
with TemporaryFile() as temp:
79+
self.extract_tga(temp)
80+
self.image = ImageProc.open(temp)
81+
for icon in self.icons:
82+
icon.image = self.image.crop((0, icon.top, icon.width, icon.top + icon.height))
83+
return self.image
84+
85+
def extract_tga(self, dest):
86+
"""Extracts the image data from the BNI file and saves it to 'dest'."""
87+
with open(self.path, 'rb') as reader:
88+
reader.seek(self.offset)
89+
90+
try:
91+
writer = open(dest, 'wb') if isinstance(dest, str) else dest
92+
writer.write(reader.read(-1))
93+
finally:
94+
if dest != writer:
95+
writer.close()
96+
97+
def extract_icons(self, fname=None):
98+
"""Extracts individual icons and saves them to disk.
99+
'fname' should be a function taking an IconEntry and returning a file name
100+
"""
101+
self.open_image()
102+
for idx, icon in enumerate(self.icons):
103+
name = fname(icon) if fname else icon.get_name() + ".png"
104+
icon.image.save(name)
105+
yield icon, name
106+
107+
108+
def main():
109+
fp = sys.argv[1]
110+
file = BnetIconFile.load(fp)
111+
112+
print(f"BNI file - version: {file.version}, count: {file.count}, offset: {file.offset}")
113+
for i in range(len(file.icons)):
114+
icon = file.icons[i]
115+
print(f"\t#{i + 1:02} - flags: 0x{icon.flags:02X}, size: {icon.width}x{icon.height}, codes: {icon.codes}")
116+
117+
print("Extracting TARGA image and importing into Pillow...")
118+
image = file.open_image()
119+
print("\tFormat:", image.format)
120+
print("\t Size:", image.size)
121+
print("\t Mode:", image.mode)
122+
123+
print("Cutting out individual icons...")
124+
counter = 0
125+
for _, name in file.extract_icons():
126+
print(f"\tSaved icon #{counter} to '{name}'")
127+
counter += 1
128+
129+
130+
if __name__ == "__main__":
131+
main()

bncs/utils/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11

2-
from .buffer import make_dword, format_buffer, DataBuffer, DataReader
2+
from .buffer import make_dword, unmake_dword, format_buffer, DataBuffer, DataReader
33
from .client import AsyncClientBase, InvalidOperationError
44
from .packet import get_packet_name, PacketBuilder, PacketReader, InvalidPacketException

bncs/utils/buffer.py

+6
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ def make_dword(v):
3434
raise TypeError("DWORD must be a string with at most 4 characters.")
3535

3636

37+
def unmake_dword(v):
38+
""" Converts an int DWORD value to a string """
39+
buff = pack('<I', v)
40+
return buff[::-1].decode('ascii')
41+
42+
3743
def format_buffer(data):
3844
""" Formats binary data in a human-friendly manner. """
3945
if len(data) == 0:

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
author_email='[email protected]',
1212
url='https://github.com/Davnit/bncs.py',
1313
packages=find_packages(),
14-
install_requires=['pefile', 'signify'],
14+
install_requires=['pefile', 'signify', 'Pillow'],
1515
package_data={
1616
".": ["products.json"]
1717
},

0 commit comments

Comments
 (0)