Created
June 11, 2018 21:56
-
-
Save isanae/caee5d1c727fd9fdc48a3d80055d3600 to your computer and use it in GitHub Desktop.
This file contains 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
import sys | |
import extract_icon | |
import os | |
import argparse | |
class MorIcons: | |
""" | |
parses an ``APPS.INF`` file, dumps all icons from a PE binary and | |
generates an HTML file with all the icons and associated program | |
names | |
usage:: | |
# dumps all icons and index.html in the current directory | |
m = MorIcons("moricons.dll", "apps.inf") | |
m.write_icons() | |
m.write_html() | |
:param dll: path to ``moricons.dll`` | |
:param inf: path to ``APPS.INF`` | |
:param ext: extension for the images, such as ``png`` | |
""" | |
# | |
# top of the HMTL file | |
_HTML_HEADER = """<!doctype html> | |
<html> | |
<head> | |
<meta charset="utf-8"/> | |
<style> | |
div | |
{ | |
width: 100px; | |
display: inline-block; | |
margin: 10px; | |
text-align: center; | |
vertical-align: top; | |
} | |
</style> | |
</head> | |
<body>""" | |
# bottom of the HTML file | |
_HTML_FOOTER = """ | |
</body> | |
</html>""" | |
def __init__(self, dll, inf, ext="png"): | |
self.dll = dll | |
self.inf = inf | |
self.ext = ext | |
self.icon_count = 0 | |
self.names = self._get_names() | |
def _get_names(self): | |
""" | |
opens the INF file, parses the CSV lines and returns all the | |
program names and icon indexes | |
:returns: a ``dict`` of program names indexed by the index of | |
the icon in the binary | |
""" | |
# reading apps.inf | |
with open(self.inf, "r") as f: | |
lines = f.readlines() | |
# program names indexed by icon index in the binary | |
names = {} | |
# will be set when seeing a line that contains "[pif]", which is | |
# the line just before the first CSV line | |
started = False | |
for ln in lines: | |
ln = ln.strip() | |
# skip empty lines | |
if ln == "": | |
continue | |
if started: | |
# once "[pif]" has been seen, apps.inf only has: | |
# 1) sections | |
# 2) comments | |
# 3) CSV lines | |
# 4) empty lines (handled above) | |
# once the section "[std_dflt]", there are no more CSV | |
# lines in the file | |
if ln == "[std_dflt]": | |
break | |
# ignore comments and sections | |
if ln[0] == ";" or ln[0] == "[": | |
continue | |
pair = self.parse_line(ln) | |
if pair is not None: | |
names[pair["index"]] = pair["name"] | |
elif ln == "[pif]": | |
# CSV lines start here | |
started = True | |
return names | |
def parse_line(self, ln): | |
""" | |
parses a single CSV line and the index and program name | |
:param ln: the string to parse | |
:returns: ``{'index': icon index, 'name': program name}`` | |
""" | |
# here is an example of a CSV line from APPS.INF: | |
# | |
# QD3.EXE = QD3,"Q-DOS 3",,cwe,moricons.dll,47,std_QD3,enha_QD3 | |
# | |
# normally, the line would be split on '=' to get the | |
# value, then the CSV properly parsed to handle the | |
# double quotes around the name, but a quick check | |
# showed that no name actually contains a comma | |
# | |
# so in this example, a simple comma split makes that: | |
# | |
# [0] QD3.EXE = QD3 | |
# [1] "Q-DOS 3" | |
# [2] | |
# [3] cwe | |
# [4] moricons.dll | |
# [5] 47 | |
# [6] std_QD3 | |
# [7] enha_QD3 | |
# | |
# which is perfect | |
cols = ln.split(",") | |
# sanity check | |
if len(cols) != 8: | |
return None | |
# apps.inf also describes icons from other binaries | |
# (such as progman.exe), and [4] contains the filename | |
if cols[4] != "moricons.dll": | |
return None | |
# index of the icon in the binary | |
index = int(cols[5]) | |
# name, may be surrounded by double quotes | |
name = cols[1].strip().strip("\"") | |
return {"index": index, "name": name} | |
def write_icons(self, dir="."): | |
""" | |
dumps all the icons in their best format to the given directory | |
using their index (0-based) as the filename, in the image format | |
given in the constructor | |
:param dir: the output directory | |
""" | |
# https://github.com/firodj/extract-icon-py | |
ex = extract_icon.ExtractIcon(self.dll) | |
# in extract_icon, each icon is called a "group", because it | |
# can contain multiple icons of various size and depth | |
groups = ex.get_group_icons() | |
# remember the number of unique icons so it can be used when | |
# writing the HTML | |
self.icon_count = len(groups) | |
# 'i' is needed to generate the filename | |
for i in range(self.icon_count): | |
group = groups[i] | |
# index of the "best" icon in the group; extract_icon finds | |
# the largest bit count and returns the index of the widest | |
# icon with that bit count | |
# | |
# moricons.dll is mostly 1-bit (monochrome) and 4-bit, but | |
# the last few Office icons are 8-bit | |
best = ex.best_icon(group) | |
# creates a pillow Image out of the best icon | |
# https://pillow.readthedocs.io | |
image = ex.export(group, best) | |
filename = self.icon_filename(i) | |
path = os.path.join(dir, filename) | |
# example path would be "dir/0.png" | |
with open(path, "wb") as f: | |
image.save(f) | |
def write_html(self, path="index.html"): | |
""" | |
writes an HTML file that contains a series of ``<div>`` for | |
each icon and program name | |
:param path: the path of the output file | |
""" | |
# writes a header, an html_entry() for each icon, then a footer | |
with open(path, "w") as f: | |
f.write(self._HTML_HEADER) | |
for i in range(self.icon_count): | |
icon = self.icon_filename(i) | |
name = self.names.get(i, "(none)") | |
f.write("\n" + self.html_entry(icon, name)) | |
f.write(self._HTML_FOOTER) | |
def icon_filename(self, i): | |
""" | |
makes the filename for an icon | |
:param i: 0-based index of the icon | |
:returns: a string that embed the index and extension | |
""" | |
return str(i) + "." + self.ext | |
def html_entry(self, icon, name): | |
""" | |
makes a single ``<div>`` for an icon | |
:param icon: path to the icon | |
:param name: name of the program associated with the icon | |
:returns: a string with an ``<img>`` and name | |
""" | |
return '<div><img src="{}"><br>{}</div>'.format(icon, name) | |
if __name__ == "__main__": | |
p = argparse.ArgumentParser( | |
formatter_class=argparse.ArgumentDefaultsHelpFormatter) | |
p.add_argument( | |
"--format", | |
help="image format to use", | |
default="png") | |
p.add_argument( | |
"--output", | |
help="output directory", | |
default=".") | |
p.add_argument( | |
"--index", | |
help="name of the HTML index file", | |
default="index.html") | |
p.add_argument( | |
"--dll", | |
help="path to MORICONS.DLL", | |
default="C:\\Windows\\System32\\moricons.dll") | |
p.add_argument( | |
"INF", | |
help="path to APPS.INF") | |
args = p.parse_args() | |
m = MorIcons(args.dll, args.INF, args.format) | |
m.write_icons(args.output) | |
m.write_html(os.path.join(args.output, args.index)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
cool