Skip to content

Instantly share code, notes, and snippets.

Created June 11, 2018 21:56
Show Gist options
  • Save isanae/caee5d1c727fd9fdc48a3d80055d3600 to your computer and use it in GitHub Desktop.
Save isanae/caee5d1c727fd9fdc48a3d80055d3600 to your computer and use it in GitHub Desktop.
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
# dumps all icons and index.html in the current directory
m = MorIcons("moricons.dll", "apps.inf")
: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>
<meta charset="utf-8"/>
width: 100px;
display: inline-block;
margin: 10px;
text-align: center;
vertical-align: top;
# bottom of the HTML file
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 == "":
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]":
# ignore comments and sections
if ln[0] == ";" or ln[0] == "[":
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
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
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:
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:
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))
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(
help="image format to use",
help="output directory",
help="name of the HTML index file",
help="path to MORICONS.DLL",
help="path to APPS.INF")
args = p.parse_args()
m = MorIcons(args.dll, args.INF, args.format)
m.write_html(os.path.join(args.output, args.index))
Copy link

voxel98 commented Nov 24, 2018


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment