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

@voxel98 voxel98 commented Nov 24, 2018

cool

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