Skip to content

Instantly share code, notes, and snippets.

@isanae isanae/moricons.py
Created Jun 11, 2018

Embed
What would you like to do?
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))
@voxel98

This comment has been minimized.

Copy link

commented Nov 24, 2018

cool

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.