Skip to content

Instantly share code, notes, and snippets.

@grassmunk
Last active October 16, 2019 08:00
Show Gist options
  • Save grassmunk/a1f586adb6ee22d31bc4d7abcaf6c0f1 to your computer and use it in GitHub Desktop.
Save grassmunk/a1f586adb6ee22d31bc4d7abcaf6c0f1 to your computer and use it in GitHub Desktop.
Microsoft Theme Parser
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
import os
import collections
import PIL.Image
import svgwrite
import pathlib
import shutil
import subprocess
import configparser
import xml.etree.ElementTree as ET
from pathlib import Path
from PIL import Image
from configparser import ConfigParser
def hexToRGB(h):
return tuple(int(h.lstrip('#')[i:i+2], 16) for i in (0, 2, 4))
def rgbaToRGB(tup):
return (tup[0],tup[1],tup[2])
def convert_icon_files(icon_filename, output_file_name):
convert_path = subprocess.check_output(["which", "convert"]).strip()
print("\t{:<21} {}".format(os.path.split(icon_filename)[1], os.path.split(output_file_name)[1]))
args = [
convert_path,
icon_filename,
output_file_name
]
subprocess.check_call(args)
if os.path.isfile(output_file_name[:-4]+"-0.png"):
shutil.move(output_file_name[:-4]+"-0.png", output_file_name[:-4]+".png")
def make_x11_cursors(icons_file, seq=False, rate=False, in_filename="tmp_file", out_folder="tmp_file.png"):
#print("[Icons]")
icon_num = 1
icon_file_names = []
for icon in icons_file:
if len(icon) == 0:
# Skip empty icons
continue
icon_type = int.from_bytes(icon[2:4],"little")
number_of_images = int.from_bytes(icon[4:6],"little")
if icon_type == 1:
ext = ".ico"
else:
ext = ".cur"
path_to_ani, ani_file_name = os.path.split(in_filename)
icon_name = os.path.splitext(ani_file_name)[0].replace(" ","_")
if seq:
num = "_{:02}".format(seq[icon_num-1])
else:
num = "_{:02}".format(icon_num)
if rate and type(rate) is list:
jif = "_{}".format(rate[icon_num-1])
elif rate:
jif = "_{}".format(rate)
else:
jif = "_{}".format(10)
icon_file = icon_name + jif + num + ext
#print("\t{:<21} {}".format(icon_file, out_folder))
icon_num += 1
icon_file_names.append(out_folder+icon_file)
f = open(out_folder+icon_file,"wb")
f.write(icon)
f.close()
return icon_file_names
def build_cursors(cursor_folder):
#xcursors defs
xcursors_conf = {
"01_AngleNW.conf" : "ul_angle",
"02_AngleNW.conf" : "dnd-none",
"03_AngleNW.conf" : "dnd-move",
"04_AngleNE.conf" : "ur_angle",
"05_AngleNE.conf" : "ll_angle",
"06_AngleNW.conf" : "lr_angle",
"07_AppStarting.conf" : "left_ptr_watch",
"08_AppStarting.conf" : "08e8e1c95fe2fc01f976f1e063a24ccd",
"09_AppStarting.conf" : "3ecb610c1bf2410f44200f48c40d3599",
"10_Arrow.conf" : "arrow",
"11_Arrow.conf" : "draft_large",
"12_Arrow.conf" : "draft_small",
"13_Arrow.conf" : "left_ptr",
"14_Arrow.conf" : "right_ptr",
"15_Arrow.conf" : "top_left_arrow",
"16_ArrowRight.conf" : "right_ptr",
"17_BaseN.conf" : "base_arrow_up",
"18_BaseN.conf" : "based_arrow_up",
"19_BaseN.conf" : "base_arrow_down",
"20_BaseN.conf" : "based_arrow_down",
"21_Circle.conf" : "circle",
"22_Copy.conf" : "copy",
"23_Copy.conf" : "1081e37283d90000800003c07f3ef6bf",
"24_Copy.conf" : "6407b0e94181790501fd1e167b474872",
"25_Copy.conf" : "08ffe1cb5fe6fc01f906f1c063814ccf",
"26_Cross.conf" : "cross",
"27_Cross.conf" : "cross_reverse",
"28_Cross.conf" : "tcross",
"29_Crosshair.conf" : "crosshair",
"30_DND-ask.conf" : "dnd-ask",
"31_DND-copy.conf" : "dnd-copy",
"32_DND-link.conf" : "dnd-link",
"33_Hand.conf" : "hand",
"34_Hand.conf" : "hand1",
"35_Hand.conf" : "hand2",
"36_Hand.conf" : "e29285e634086352946a0e7090d73106",
"37_Handgrab.conf" : "HandGrab",
"38_Handgrab.conf" : "9d800788f1b08800ae810202380a0822",
"39_Handgrab.conf" : "5aca4d189052212118709018842178c0",
"40_Handsqueezed.conf" : "HandSqueezed",
"41_Handsqueezed.conf" : "208530c400c041818281048008011002",
"42_Handwriting.conf" : "pencil",
"43_Help.conf" : "question_arrow",
"44_Help.conf" : "d9ce0ab605698f320427677b458ad60b",
"45_Help.conf" : "5c6cd98b3f3ebcb1f9c7f1c204630408",
"46_IBeam.conf" : "xterm",
"47_IBeam.conf" : "ibeam",
"48_Link.conf" : "link",
"49_Link.conf" : "3085a0e285430894940527032f8b26df",
"50_Link.conf" : "640fb0e74195791501fd1ed57b41487f",
"51_Link.conf" : "0876e1c15ff2fc01f906f1c363074c0f",
"52_NO.conf" : "crossed_circle",
"53_NO.conf" : "dnd-none",
"54_NO.conf" : "03b6e0fcb3499374a867c041f52298f0",
"55_Move.conf" : "move",
"56_Move.conf" : "plus",
"57_Move.conf" : "4498f0e0c1937ffe01fd06f973665830",
"58_Move.conf" : "9081237383d90e509aa00f00170e968f",
"59_SizeAll.conf" : "fleur",
"60_AngleNE.conf" : "bottom_left_corner",
"61_AngleNE.conf" : "fd_double_arrow",
"62_AngleNE.conf" : "top_right_corner",
"63_AngleNE.conf" : "fcf1c3c7cd4491d801f1e1c78f100000",
"64_BaseN.conf" : "bottom_side",
"65_BaseN.conf" : "double_arrow",
"66_BaseN.conf" : "top_side",
"67_BaseN.conf" : "00008160000006810000408080010102",
"68_AngleNW.conf" : "bd_double_arrow",
"69_AngleNW.conf" : "bottom_right_corner",
"70_AngleNW.conf" : "top_left_corner",
"71_AngleNW.conf" : "c7088f0f3e6c8088236ef8e1e3e70000",
"72_SizeWE.conf" : "left_side",
"73_SizeWE.conf" : "right_side",
"74_SizeWE.conf" : "028006030e0e7ebffc7f7070c0600140",
"75_UpArrow.conf" : "center_ptr",
"76_UpArrow.conf" : "sb_up_arrow",
"77_DownArrow.conf" : "sb_down_arrow",
"78_LeftArrow.conf" : "sb_left_arrow",
"79_RightArrow.conf" : "sb_right_arrow",
"80_HDoubleArrow.conf" : "h_double_arrow",
"81_HDoubleArrow.conf" : "sb_h_double_arrow",
"82_HDoubleArrow.conf" : "14fef782d02440884392942c11205230",
"83_VDoubleArrow.conf" : "v_double_arrow",
"84_VDoubleArrow.conf" : "sb_v_double_arrow",
"85_VDoubleArrow.conf" : "2870a09082c103050810ffdffffe0204",
"86_Wait.conf" : "watch",
"87_X.conf" : "X_cursor",
"88_X.conf" : "X-cursor",
"89_ZoomIn.conf" : "zoomIn",
"90_ZoomIn.conf" : "f41c0e382c94c0958e07017e42b00462",
"91_ZoomOut.conf" : "zoomOut",
"92_ZoomOut.conf" : "f41c0e382c97c0938e07017e42800402"
}
#print("\n\t[Building Cursors]")
xcursorgen_path = subprocess.check_output(["which", "xcursorgen"]).strip()
src_folder = cursor_folder + "src/"
build_folder = cursor_folder + "cursors/"
shutil.rmtree(build_folder)
os.mkdir(build_folder)
for gen in xcursors_conf:
conf_file = src_folder + gen[3:]
cursor_file_output = build_folder + xcursors_conf[gen]
print("\t{:<21} {}".format(os.path.split(conf_file)[1], os.path.split(cursor_file_output)[1]))
args = [
xcursorgen_path,
"-p",
src_folder,
conf_file,
cursor_file_output
]
subprocess.check_call(args, stdout=subprocess.DEVNULL)
def parse_ani(file_name):
# convert an ani file to a list of bytearray icons
# input: ani file location/name
f = open(file_name,'rb')
ani_file = f.read()
f.close()
ani_bytes = bytearray(ani_file)
loc = ani_bytes.find("anih".encode()) + 8
anih = {
"size" : 0,
"num_frames" : 0,
"num_steps" : 0,
"width" : 0,
"height" : 0,
"bit_count" : 0,
"num_planes" : 0,
"jif_rate" : 0,
"flags" : 0
}
for i in anih:
anih[i] = int.from_bytes(ani_bytes[loc:loc+4],"little")
loc +=4
#print("[ANI Header]")
#for i in anih:
# print("\t{:<30} {}".format(i,anih[i]))
rate_length = anih["jif_rate"]
if ani_bytes.find("rate".encode()) > -1:
rate_length = []
# print("\n[Rate]")
loc = ani_bytes.find("rate".encode()) + 4
rate_size = int.from_bytes(ani_bytes[loc:loc+4],"little")
loc += 4
for i in range(anih["num_steps"]):
rate_length.append(int.from_bytes(ani_bytes[loc:loc+4],"little"))
loc += 4
# print("\t{}".format(rate_length))
seq_length = False
if ani_bytes.find("seq ".encode()) > -1:
seq_length = []
# print("\n[Seq]")
loc = ani_bytes.find("seq ".encode()) + 4
seq_size = int.from_bytes(ani_bytes[loc:loc+4],"little")
loc += 4
for i in range(anih["num_steps"]):
seq_length.append(int.from_bytes(ani_bytes[loc:loc+4],"little"))
loc += 4
# print("\t{}".format(seq_length))
# Now find the icons
loc = ani_bytes.find("LIST".encode()) + 4
num_icons = int.from_bytes(ani_bytes[loc:loc+4],"little")
loc = ani_bytes.find("fram".encode()) + 8
icons = []
count = 0
## At first icon
for i in range(anih["num_steps"]):
icon_size = int.from_bytes(ani_bytes[loc:loc+4],"little")
icon = ani_bytes[loc+4:(loc+4)+icon_size]
icons.append(icon)
loc = loc + icon_size + 8
count = count + 1
return icons, seq_length, rate_length
def convert_icon(folder, ico_file_name, theme_folder, squaresize = 20, overlap = 2, tmp_file="./chicago95_tmp_file.svg", max_colors=25 ):
## Converts Icons to PNG
# Input:
# folder: svg file destination folder
# ico_file_name: theme icon file to be processed
# theme_folder: dict of the folder for case insensitivty
# squaresize: how big svg 'pixels'
# overlap: do the squares overlap
# tmp_file: tmp working file for inkscape
# max_colors = max colors to try and merge in svg
# Lots of code lifted from pixel2svg
path_to_icon, icon_file_name = os.path.split(ico_file_name)
icon_name, icon_ext = os.path.splitext(icon_file_name)
svg_name = icon_name+".svg"
if not os.path.exists(ico_file_name):
for lower_file in theme_folder:
if ico_file_name in lower_file:
ico_file_name = theme_folder[lower_file]
# Open the icon file
image = Image.open(ico_file_name)
image = image.convert("RGBA")
(width, height) = image.size
rgb_values = list(image.getdata())
rgb_values = list(image.getdata())
svgdoc = svgwrite.Drawing(filename = folder + svg_name,
size = ("{0}px".format(width * squaresize),
"{0}px".format(height * squaresize)))
# If --overlap is given, use a slight overlap to prevent inaccurate SVG rendering
rectangle_size = ("{0}px".format(squaresize + overlap),
"{0}px".format(squaresize + overlap))
rowcount = 0
while rowcount < height:
colcount = 0
while colcount < width:
rgb_tuple = rgb_values.pop(0)
# Omit transparent pixels
if rgb_tuple[3] > 0:
rectangle_posn = ("{0}px".format(colcount * squaresize),
"{0}px".format(rowcount * squaresize))
rectangle_fill = svgwrite.rgb(rgb_tuple[0], rgb_tuple[1], rgb_tuple[2])
alpha = rgb_tuple[3];
if alpha == 255:
svgdoc.add(svgdoc.rect(insert = rectangle_posn,
size = rectangle_size,
fill = rectangle_fill))
else:
svgdoc.add(svgdoc.rect(insert = rectangle_posn,
size = rectangle_size,
fill = rectangle_fill,
opacity = alpha/float(255)))
colcount = colcount + 1
rowcount = rowcount + 1
svgdoc.save()
convert_to_proper_svg_with_inkscape(tmp_file, svgdoc.filename)
SVG_NS = "http://www.w3.org/2000/svg"
svg = ET.parse(tmp_file)
rects = svg.findall('.//{%s}rect' % SVG_NS)
rgbs = {}
for rect in rects:
rect_id = rect.attrib['id']
rgb = rect.attrib['fill']
if rgb not in rgbs:
rgbs[rgb] = rect_id
if len(rgbs) < max_colors:
print("\t{:<21} joining same colors, inkscape will open {} times".format(svg_name,len(rgbs)))
else:
print("\t{:<21} too many colors ({}>{}), skipping".format(svg_name, len(rgbs), max_colors))
count = 0
for rgb in rgbs:
count = count + 1
if len(rgbs) >= max_colors:
break
#if count % 10 == 0:
print("\t{:<21} [{:<3} / {:<3} {:<5}] Converting {}".format(' ',count, len(rgbs),str(round((float(count)/float(len(rgbs))*100),0)), rgb ))
fix_with_inkscape( rgbs[rgb] , tmp_file )
shutil.move(tmp_file, svgdoc.filename)
return(svgdoc.filename)
def fix_with_inkscape(color, tmpfile):
inkscape_path = subprocess.check_output(["which", "inkscape"]).strip()
args = [
inkscape_path,
"--select="+color,
"--verb", "EditSelectSameFillColor",
"--verb", "SelectionCombine",
"--verb", "SelectionUnion",
"--verb", "FileSave",
"--verb", "FileQuit",
tmpfile
]
subprocess.check_call(args, stderr=subprocess.DEVNULL ,stdout=subprocess.DEVNULL)
def convert_to_png_with_inkscape(svg_in, size, png_out):
inkscape_path = subprocess.check_output(["which", "inkscape"]).strip()
size = str(size)
args = [
inkscape_path,
"--without-gui",
"-f", svg_in,
"--export-area-page",
"-w", size,
"-h", size,
"--export-png=" + png_out
]
subprocess.check_call(args, stdout=subprocess.DEVNULL)
def convert_to_proper_svg_with_inkscape(svg_out, svg_in):
inkscape_path = subprocess.check_output(["which", "inkscape"]).strip()
args = [
inkscape_path,
"-l", svg_out, svg_in
]
subprocess.check_call(args, stdout=subprocess.DEVNULL)
def xfconf_query(svg_out, svg_in):
inkscape_path = subprocess.check_output(["which", "inkscape"]).strip()
args = [
inkscape_path,
"-l", svg_out, svg_in
]
subprocess.check_call(args, stdout=subprocess.DEVNULL)
def get_file_name(config, section, key):
#input:
# config = parsed .theme file
# section = the section in the config file
# key = key in theme file
# Returns: filename
# To Do: incorporate icon number
print
reg_key = "Software\\Classes\\"
file_name = ''
icon_number = 0
if section in config and key in config[section]:
file_name = config[section][key].lower()
elif reg_key+section in config and key in config[reg_key+section]:
file_name = config[reg_key+section][key].lower()
else:
return False
if file_name == '':
# The key was here but its empty
return False
if "%windir%" in file_name:
# we dont bother changing system icons
return False
if "%ThemeDir%".lower() in file_name:
file_name = file_name.replace("%ThemeDir%".lower(),'')
if "," in file_name:
file_name, icon_number = file_name.split(",")
file_name = file_name.split("\\")[-1]
return file_name
def null_string(data):
data = bytearray(data)
return data[:data.find(0)].decode('ascii')
def parse_NONCLIENTMETRICS(NONCLIENTMETRICSA):
font_weight = {
"0":"FW_DONTCARE",
"100":"FW_THIN",
"200":"FW_EXTRALIGHT",
"200":"FW_ULTRALIGHT",
"300":"FW_LIGHT",
"400":"FW_NORMAL",
"400":"FW_REGULAR",
"500":"FW_MEDIUM",
"600":"FW_SEMIBOLD",
"600":"FW_DEMIBOLD",
"700":"FW_BOLD",
"800":"FW_EXTRABOLD",
"800":"FW_ULTRABOLD",
"900":"FW_HEAVY",
"900":"FW_BLACK"
}
x = []
for i in NONCLIENTMETRICSA.split():
x.append(int(i))
nonclientmetrics = {
"cbSize" : int.from_bytes(x[0:4],"little"),
"iBorderWidth" : int.from_bytes(x[4:8],"little"),
"iScrollWidth" : int.from_bytes(x[8:12],"little"),
"iScrollHeight" : int.from_bytes(x[12:16],"little"),
"iCaptionWidth" : int.from_bytes(x[16:20],"little"),
"iCaptionHeight": int.from_bytes(x[20:24],"little")
}
lfcaptionfont = {
"Name:" : "lfcaptionfont",
"lfHeight" : int.from_bytes(x[24:28],"little"),
"lfWidth" : int.from_bytes(x[28:32],"little"),
"lfEscapement" : int.from_bytes(x[32:36],"little"),
"lfOrientation" : int.from_bytes(x[36:40],"little"),
"lfWeight" : font_weight[str(int.from_bytes(x[40:44],"little"))],
"lfItalic" : x[44],
"lfUnderline" : x[45],
"lfStrikeOut" : x[46],
"lfCharSet" : x[47],
"lfOutPrecision" : x[48],
"lfClipPrecision" : x[49],
"lfQuality" : x[50],
"lfPitchAndFamily" : x[51],
"lfFaceName[32]" : null_string(x[52:52+32])
}
nonclientmetrics["iSmCaptionWidth"] = int.from_bytes(x[84:88],"little")
nonclientmetrics["iSmCaptionHeight"] = int.from_bytes(x[88:92],"little")
lfSmCaptionFont = {
"Name" : "lfSmCaptionFont",
"lfHeight" : int.from_bytes(x[92:96],"little"),
"lfWidth" : int.from_bytes(x[96:100],"little"),
"lfEscapement" : int.from_bytes(x[100:104],"little"),
"lfOrientation" : int.from_bytes(x[104:108],"little"),
"lfWeight" : font_weight[str(int.from_bytes(x[108:112],"little"))],
"lfItalic" : x[112],
"lfUnderline" : x[113],
"lfStrikeOut" : x[114],
"lfCharSet" : x[115],
"lfOutPrecision" : x[116],
"lfClipPrecision" : x[117],
"lfQuality" : x[118],
"lfPitchAndFamily" : x[119],
"lfFaceName[32]" : null_string(x[120:120+32])
}
nonclientmetrics["iMenuWidth"] = int.from_bytes(x[152:156],"little")
nonclientmetrics["iMenuHeight"] = int.from_bytes(x[156:160],"little")
lfMenuFont = {
"Name" : "lfMenuFont",
"lfHeight" : int.from_bytes(x[160:164],"little"),
"lfWidth" : int.from_bytes(x[164:168],"little"),
"lfEscapement" : int.from_bytes(x[168:172],"little"),
"lfOrientation" : int.from_bytes(x[172:176],"little"),
"lfWeight" : font_weight[str(int.from_bytes(x[176:180],"little"))],
"lfItalic" : x[180],
"lfUnderline" : x[181],
"lfStrikeOut" : x[182],
"lfCharSet" : x[183],
"lfOutPrecision" : x[184],
"lfClipPrecision" : x[185],
"lfQuality" : x[186],
"lfPitchAndFamily" : x[187],
"lfFaceName[32]" : null_string(x[188:188+32])
}
lfStatusFont = {
"Name" : "lfStatusFont",
"lfHeight" : int.from_bytes(x[220:224],"little"),
"lfWidth" : int.from_bytes(x[224:228],"little"),
"lfEscapement" : int.from_bytes(x[228:232],"little"),
"lfOrientation" : int.from_bytes(x[232:236],"little"),
"lfWeight" : font_weight[str(int.from_bytes(x[236:240],"little"))],
"lfItalic" : x[240],
"lfUnderline" : x[241],
"lfStrikeOut" : x[242],
"lfCharSet" : x[243],
"lfOutPrecision" : x[244],
"lfClipPrecision" : x[245],
"lfQuality" : x[246],
"lfPitchAndFamily" : x[247],
"lfFaceName[32]" : null_string(x[248:248+32])
}
lfMessageFont = {
"Name" : "lfMessageFont",
"lfHeight" : int.from_bytes(x[280:284],"little"),
"lfWidth" : int.from_bytes(x[284:288],"little"),
"lfEscapement" : int.from_bytes(x[288:292],"little"),
"lfOrientation" : int.from_bytes(x[292:296],"little"),
"lfWeight" : font_weight[str(int.from_bytes(x[296:300],"little"))],
"lfItalic" : x[300],
"lfUnderline" : x[301],
"lfStrikeOut" : x[302],
"lfCharSet" : x[303],
"lfOutPrecision" : x[304],
"lfClipPrecision" : x[305],
"lfQuality" : x[306],
"lfPitchAndFamily" : x[307],
"lfFaceName[32]" : null_string(x[308:308+32])
}
return lfcaptionfont["lfFaceName[32]"], lfcaptionfont["lfWeight"]
def main():
print("Microsoft Theme file parser")
error = False
if len(sys.argv) < 2:
print("USAGE:",sys.argv[0]," theme_file")
error = True
if not os.path.exists(str(Path.home())+"/.icons/Chicago95") and not os.path.exists(str(Path.home())+"/.icons/Chicago95_tux"):
print("ERROR: Either the Chicago95 or Chicago95_tux icon theme must be installed to {} to use this script".format( os.path.exists(str(Path.home())+"/.icons/")))
error = True
if not os.path.exists(str(Path.home())+"/.icons/Chicago95_Cursor_Black"):
print("ERROR: The Chicago95 cursor Chicago95_Cursor_Black must be installed to {} to use this script".format( os.path.exists(str(Path.home())+"/.icons/")))
error = True
if not os.path.exists(str(Path.home())+"/.themes/Chicago95"):
print("ERROR: The Chicago95 theme must be installed to {} to use this script".format( os.path.exists(str(Path.home())+"/.themes/")))
error = True
try:
inkscape_path = subprocess.check_output(["which", "inkscape"]).strip()
except subprocess.CalledProcessError:
print("ERROR: You need inkscape installed to use this script.")
error = True
try:
convert_path = subprocess.check_output(["which", "convert"]).strip()
except subprocess.CalledProcessError:
print("ERROR: You need imagemagick installed to use this script.")
error = True
try:
convert_path = subprocess.check_output(["which", "xcursorgen"]).strip()
except subprocess.CalledProcessError:
print("ERROR: You need xcursorgen installed to use this script.")
error = True
if error:
sys.exit(1)
# Get the file name and extions in to useable names
theme_file = sys.argv[1]
path_to_theme, theme_file_name = os.path.split(theme_file)
if len(path_to_theme) != 0:
path_to_theme = path_to_theme + "/"
else:
path_to_theme = "./"
theme_name_spaces, theme_ext = os.path.splitext(theme_file_name)
index_theme_name = theme_name_spaces + "(Chicago 95 Variant)" # For various Index.theme files
theme_name = theme_name_spaces.capitalize().replace(" ", "_")
new_theme_folder = os.getcwd() + "/" + theme_name + "_Chicago95/"
print("[Parser] Parsing Theme File:", theme_file)
#config = ConfigParser(dict_type=CaseInsensitiveDict,interpolation=None)
config = ConfigParser(interpolation=None)
config.read(theme_file)
# Themes have a weird structure we use thise dict to remove case but keep the filename
theme_files = {}
for root, dirs, files in os.walk(path_to_theme, topdown=False):
for name in files:
theme_files[os.path.join(root, name).lower()] = os.path.join(root, name)
## Get the icons
print("\n[Parser] Parsing Icons")
icons = {}
icons["my_computer"] = get_file_name(config,"CLSID\\{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\DefaultIcon","DefaultValue")
if get_file_name(config,"CLSID\\{450D8FBA-AD25-11D0-98A8-0800361B1103}\\DefaultIcon","DefaultValue"):
icons["my_documents"] = get_file_name(config,"CLSID\\{450D8FBA-AD25-11D0-98A8-0800361B1103}\\DefaultIcon","DefaultValue")
elif get_file_name(config,"CLSID\\{59031A47-3F72-44A7-89C5-5595FE6B30EE}\\DefaultIcon","DefaultValue"):
icons["my_documents"] = get_file_name(config,"CLSID\\{59031A47-3F72-44A7-89C5-5595FE6B30EE}\\DefaultIcon","DefaultValue")
else:
icons["my_documents"] = False
if get_file_name(config,"CLSID\\{208D2C60-3AEA-1069-A2D7-08002B30309D}\\DefaultIcon","DefaultValue"):
icons["network_neighborhood"] = get_file_name(config,"CLSID\\{208D2C60-3AEA-1069-A2D7-08002B30309D}\\DefaultIcon","DefaultValue")
elif get_file_name(config,"CLSID\\{F02C1A0D-BE21-4350-88B0-7367FC96EF3C}\\DefaultIcon","DefaultValue"):
icons["network_neighborhood"] = get_file_name(config,"CLSID\\{F02C1A0D-BE21-4350-88B0-7367FC96EF3C}\\DefaultIcon","DefaultValue")
else:
icons["network_neighborhood"] = False
icons["recycle_bin_full"] = get_file_name(config,"CLSID\\{645FF040-5081-101B-9F08-00AA002F954E}\\DefaultIcon","Full")
icons["recycle_bin_empty"] = get_file_name(config,"CLSID\\{645FF040-5081-101B-9F08-00AA002F954E}\\DefaultIcon","Empty")
for i in icons:
print("\t{:<21} {}".format(i,icons[i]))
print("\n[Parser] Parsing Colors")
colors = {}
if "Control Panel\Colors" in config:
for color_name in config["Control Panel\Colors"]:
r, g, b = config["Control Panel\Colors"][color_name].split()
colors[color_name] = '#{:02x}{:02x}{:02x}'.format(int(r),int(g),int(b))
print("\t{:<21} {:<7} ({:<15})".format(color_name, colors[color_name], config["Control Panel\Colors"][color_name]))
print("\n[Parser] Parsing Cursors")
cursors = {}
if "Control Panel\Cursors" in config:
for cursor_name in config["Control Panel\Cursors"]:
cursors[cursor_name] = get_file_name(config,"Control Panel\Cursors",cursor_name)
print("\t{:<21} {}".format(cursor_name, cursors[cursor_name]))
## Get Sound files
print("\n[Parser] Parsing Sounds")
sound_names = [
"AppEvents\\Schemes\\Apps\\.Default\\AppGPFault\\.Current",
"AppEvents\\Schemes\\Apps\\.Default\\Close\\.Current",
"AppEvents\\Schemes\\Apps\\.Default\\.Default\\.Current",
"AppEvents\\Schemes\\Apps\\.Default\\MailBeep\\.Current",
"AppEvents\\Schemes\\Apps\\.Default\\Maximize\\.Current",
"AppEvents\\Schemes\\Apps\\.Default\\MenuCommand\\.Current",
"AppEvents\\Schemes\\Apps\\.Default\\MenuPopup\\.Current",
"AppEvents\\Schemes\\Apps\\.Default\\Minimize\\.Current",
"AppEvents\\Schemes\\Apps\\.Default\\Open\\.Current",
"AppEvents\\Schemes\\Apps\\.Default\\RestoreDown\\.Current",
"AppEvents\\Schemes\\Apps\\.Default\\RestoreUp\\.Current",
"AppEvents\\Schemes\\Apps\\.Default\\RingIn\\.Current",
"AppEvents\\Schemes\\Apps\\.Default\\Ringout\\.Current",
"AppEvents\\Schemes\\Apps\\.Default\\SystemAsterisk\\.Current",
"AppEvents\\Schemes\\Apps\\.Default\\SystemDefault\\.Current",
"AppEvents\\Schemes\\Apps\\.Default\\SystemExclamation\\.Current",
"AppEvents\\Schemes\\Apps\\.Default\\SystemExit\\.Current",
"AppEvents\\Schemes\\Apps\\.Default\\SystemHand\\.Current",
"AppEvents\\Schemes\\Apps\\.Default\\SystemQuestion\\.Current",
"AppEvents\\Schemes\\Apps\\.Default\\SystemStart\\.Current",
"AppEvents\\Schemes\Apps\Explorer\EmptyRecycleBin\\.Current"
]
sounds = {}
for i in sound_names:
sound_name = i.split("\\")[-2]
if get_file_name(config,i,"DefaultValue"):
wav_file = get_file_name(config,i,"DefaultValue")
sounds[sound_name] = wav_file
print("\t{:<21} {}".format(sound_name, wav_file))
## Get the wallpaper
print("\n[Parser] Parsing Wallpaper")
wallpaper = get_file_name(config,"Control Panel\Desktop","Wallpaper")
print("\t{:<21} {}".format("wallpaper",wallpaper))
print("\n[Parser] Parsing NonClientMetrics")
NonclientMetrics = config["Metrics"]["nonclientmetrics"]
(lfcaptionfont, weight) = parse_NONCLIENTMETRICS(NonclientMetrics)
print("\n[Parser] Parsing Complete!\n", "=" * 80)
print("\n\n[Theme Builder] Making folders for theme: {}".format(theme_name))
chicago95_icons_folder = str(Path.home())+"/.icons/Chicago95" if os.path.exists(str(Path.home())+"/.icons/Chicago95") else str(Path.home())+"/.icons/Chicago95_tux"
chicago95_cursors_folder = str(Path.home())+"/.icons/Chicago95_Cursor_Black"
chicago95_theme_folder = str(Path.home())+"/.themes/Chicago95"
folder_names = {
"root" : new_theme_folder,
"icons" : new_theme_folder + theme_name + "_Icons/",
"theme" : new_theme_folder + theme_name + "_Theme/",
"cursors" : new_theme_folder + theme_name + "_Cursors/",
"sounds" : new_theme_folder + theme_name + "_Sounds/"
}
for i in folder_names:
print("\t{:<21} {}".format(i,folder_names[i]))
shutil.rmtree(folder_names[i], ignore_errors=True)
if i == "icons":
shutil.copytree(chicago95_icons_folder,folder_names[i],symlinks=True,ignore_dangling_symlinks=True)
elif i == "cursors":
shutil.copytree(chicago95_cursors_folder,folder_names[i],symlinks=True,ignore_dangling_symlinks=True)
#elif i == "theme":
# shutil.copytree(chicago95_theme_folder,folder_names[i],symlinks=True,ignore_dangling_symlinks=True)
else:
os.mkdir(folder_names[i])
print("\n[DefaultIcon] Creating New Icons in {}".format(folder_names['icons']))
icon_sizes = [16,22,24,32,48]
svg_file_names = {}
png_file_names = {
"my_computer" : "user-home.png",
"my_documents" : "folder-documents.png",
"network_neighborhood" : "network-server.png",
"recycle_bin_empty" : "user-trash.png",
"recycle_bin_full" : "user-trash-full.png"
}
for i in png_file_names:
svg_file_names[i] = png_file_names[i].replace(".png",".svg")
for i in icons:
if not icons[i]: #Skip ithe icon if it doesn't exist in the theme
continue
svg_icon_file = convert_icon(folder_names['icons'],icons[i], theme_files)
for size in icon_sizes:
if size <= 32 and i == "documents_ico":
continue
sized_target = folder_names['icons']+"places/"+str(size)+"/"+png_file_names[i]
convert_to_png_with_inkscape( svg_icon_file, size, sized_target)
scaled_target = folder_names['icons']+"places/scalable/"+svg_file_names[i]
shutil.copy(svg_icon_file, scaled_target)
# Now replace Icons
icon_theme_config = configparser.RawConfigParser(interpolation=None)
icon_theme_config.optionxform = str
icon_theme_config.read(folder_names['icons']+"/index.theme")
icon_theme_config.set("Icon Theme","Name",index_theme_name)
with open(folder_names['icons']+"/index.theme", 'w') as configfile:
icon_theme_config.write(configfile, space_around_delimiters=False)
# Cursors
print("\n[ControlPanel\Cursors] Generating New Cursors")
pointers = {
"arrow" : "Arrow",
"help" : "Help",
"appstarting" : "AppStarting",
"wait" : "Wait",
"nwpen" : "Handwriting",
"no" : "NO",
"sizens" : "BaseN",
"sizewe" : "SizeWE",
"crosshair" : "Crosshair",
"ibeam" : "IBeam",
"sizenwse" : "AngleNE",
"sizenesw" : "AngleNW",
"sizeall" : "SizeAll",
"uparrow" : "UpArrow"
}
cursor_src_folder = folder_names['cursors'] + "src/"
for current_cursor in pointers:
if current_cursor not in cursors or not cursors[current_cursor]:
continue
if not os.path.exists(path_to_theme+cursors[current_cursor]):
for lower_file in theme_files:
if cursors[current_cursor] in lower_file:
theme_cursor_file_name = theme_files[lower_file]
else:
theme_cursor_file_name = cursors[current_cursor]
x11_cursor_file_name = cursor_src_folder+pointers[current_cursor]+".png"
os.remove(x11_cursor_file_name)
print("\t{:<21} {}".format(os.path.split(theme_cursor_file_name)[1],os.path.split(x11_cursor_file_name)[1]))
if os.path.splitext(cursors[current_cursor])[1] in ".ani":
icon_cur_files, seq, rate = parse_ani(theme_cursor_file_name)
icon_file_names = make_x11_cursors(icon_cur_files, seq, rate, theme_cursor_file_name, cursor_src_folder)
with open(cursor_src_folder+pointers[current_cursor]+".conf") as f:
(g1, g2, g3, cursor_n) = f.readline().strip().split(" ")
write_conf = open(cursor_src_folder+pointers[current_cursor]+".conf", 'w')
#print("\n[Convert]")
for icon_cur in icon_file_names:
x11_cursor_file_name = cursor_src_folder+pointers[current_cursor]+".png"
path_to_src, png_file_name = os.path.split(x11_cursor_file_name)
cur_name = os.path.splitext(png_file_name)[0]
path_to_icon, icon_file_name = os.path.split(icon_cur)
orig_icon_name = os.path.splitext(icon_file_name)[0]
seq = orig_icon_name.split("_")[-1]
rate = orig_icon_name.split("_")[-2]
x11_cursor_file_name = x11_cursor_file_name[:-4] + "_{}_{}.png".format(rate, seq)
convert_icon_files(icon_cur,x11_cursor_file_name)
# Xcursorgen conf file format: <size> <xhot> <yhot> <filename> <ms-delay>
# Ani to png file format: <filename> <jiffie> <sequence>
cursor_conf_string = "{} {} {} {} {}\n".format(g1, g2, g3, os.path.split(x11_cursor_file_name)[1],int(rate) * 17 )
write_conf.write(cursor_conf_string)
write_conf.close()
else:
convert_icon_files(theme_cursor_file_name,x11_cursor_file_name)
# Cursors are all done now we need to generate X11 cursors with xcursorgen
build_cursors(folder_names['cursors'])
cur_theme_config = configparser.RawConfigParser(interpolation=None)
cur_theme_config.optionxform = str
cur_theme_config.read(folder_names['cursors']+"index.theme")
cur_theme_config.set("Icon Theme","Name",index_theme_name)
with open(folder_names['cursors']+"index.theme", 'w') as configfile:
cur_theme_config.write(configfile, space_around_delimiters=False)
print("\n[ControlPanel\Colors] Changing theme colors:")
original_theme_folder = os.path.expanduser("~/.themes/Chicago95")
target_theme_folder = folder_names["theme"]
remapColors = {
"#000080": colors['activetitle'], #Active Window and Text Highlight - RED
"#dfdfdf": colors['menu'], #highlight? - Does Nothing - Yellow
"#c0c0c0": colors['menu'], #main window outline/buttons/bars color and inactive text - Green
"#ffffff": colors['window'], #main window color inner and main text color - Blue
"#808080": colors['inactivetitle'], #shadow window color (Inactive?) - turqoise
"#000000": colors['windowtext'], #Inactive window text color - Purple
}
#Make sure none of them overlap
for x in remapColors:
if remapColors[x] in remapColors:
if x.lower() == remapColors[x].lower():
continue
elif remapColors[x][-1].lower() == 'f':
remapColors[x] = remapColors[x][:6] + 'e'
else:
remapColors[x] = remapColors[x][:6] + str(int(remapColors[x][-1]) + 1)
for i in remapColors:
print("\tCurrent: {:<12} New Color: {}".format(i,remapColors[i]))
if (os.path.isdir(target_theme_folder)):
shutil.rmtree(target_theme_folder)
os.makedirs(target_theme_folder)
for root,dirs,files in os.walk(original_theme_folder):
for dir in dirs:
fpath = os.path.join(root,dir)
nfpath = fpath.replace(original_theme_folder,target_theme_folder)
if not (os.path.isdir(nfpath)):
os.makedirs(nfpath)
for file in files:
fpath = os.path.join(root,file)
nfpath = fpath.replace(original_theme_folder,target_theme_folder)
lpath = fpath.replace(original_theme_folder + "/","")
ext = os.path.splitext(fpath)[1].lower()
if (ext == ".css") or (ext == ".scss") or (ext == ".xpm") or (ext == ".svg") or (ext == ".rc")\
or (lpath == "gtk-2.0/gtkrc") or (lpath == "xfwm4/hidpi/themerc") or (lpath == "xfwm4/themerc"):
fileh = open(fpath,"r")
nfileh = open(nfpath,"w")
for line in fileh:
for color in remapColors:
if color.lower() == remapColors[color].lower():
continue
if color.upper() in line:
#print("\t{:<30} from: {} to: {}".format( os.path.split(fpath)[1],color,remapColors[color]))
line = line.replace(color.upper(),remapColors[color].upper())
elif color.lower() in line:
#print("\t{:<30} from: {} to: {}".format( os.path.split(fpath)[1],color,remapColors[color]))
line = line.replace(color.lower(),remapColors[color].lower())
nfileh.write(line)
fileh.close()
nfileh.close()
if (ext == ".png"):
img = Image.open(fpath)
img = img.convert("RGBA")
pixels = img.load()
width, height = img.size
for y in range(height):
for x in range(width):
pixel = pixels[x,y]
for color in remapColors:
if color.lower() == remapColors[color].lower():
continue
colorV = remapColors[color]
rgbColor = hexToRGB(color)
rgbColorV = hexToRGB(colorV)
if (rgbaToRGB(pixel) == rgbColor):
#print("\t{:<30} from: {} to: {}".format( os.path.split(fpath)[1],color,remapColors[color]))
pixels[x,y] = (rgbColorV[0],rgbColorV[1],rgbColorV[2],pixel[3])
break
img.save(nfpath)
img.close()
if not (os.path.isfile(nfpath)):
shutil.copy(fpath,nfpath)
cur_theme_config = configparser.RawConfigParser(interpolation=None)
cur_theme_config.optionxform = str
cur_theme_config.read(folder_names['theme']+"index.theme")
cur_theme_config.set("Desktop Entry","Name",index_theme_name)
cur_theme_config.set("X-GNOME-Metatheme","GtkTheme",index_theme_name)
cur_theme_config.set("X-GNOME-Metatheme","MetacityTheme",index_theme_name)
cur_theme_config.set("X-GNOME-Metatheme","IconTheme",index_theme_name)
cur_theme_config.set("X-GNOME-Metatheme","CursorTheme",index_theme_name)
with open(folder_names['theme']+"index.theme", 'w') as configfile:
cur_theme_config.write(configfile, space_around_delimiters=False)
if wallpaper:
print("\n[Control Panel\Desktop]")
if not os.path.exists(folder_names['root']+wallpaper):
for lower_file in theme_files:
if wallpaper.lower() in lower_file:
theme_wallpaper = theme_files[lower_file]
else:
theme_wallpaper = wallpaper
print("\t{}".format(os.path.split(wallpaper)[1]))
shutil.copy(theme_wallpaper,folder_names['root'])
theme_wallpaper = folder_names['root'] + os.path.split(theme_wallpaper)[1]
print("\n[Fonts]")
fonts = []
for files in theme_files:
if ".ttf" in files:
print("\t{}".format(os.path.split(files)[1]))
shutil.copy(theme_files[files],folder_names['root']+os.path.split(files)[1])
fonts.append(folder_names['root']+os.path.split(files)[1])
print("[Theme Building] Completed!")
install_icons_dir = str(Path.home())+"/.icons/"+folder_names['icons'].split("/")[-2]
install_cursors_dir = str(Path.home())+"/.icons/"+folder_names['cursors'].split("/")[-2]
install_themes_dir = str(Path.home())+"/.themes/"+folder_names['theme'].split("/")[-2]
shutil.rmtree(install_icons_dir, ignore_errors=True)
shutil.rmtree(install_cursors_dir, ignore_errors=True)
shutil.rmtree(install_themes_dir, ignore_errors=True)
print("\n[Installing]")
print("\tCopying {} to {}".format(folder_names['icons'], install_icons_dir))
shutil.copytree(folder_names['icons'],install_icons_dir,symlinks=True,ignore_dangling_symlinks=True)
print("\tCopying {} to {}".format(folder_names['cursors'], install_cursors_dir))
shutil.copytree(folder_names['cursors'],install_cursors_dir,symlinks=True,ignore_dangling_symlinks=True)
print("\tCopying {} to {}".format(folder_names['theme'], install_themes_dir))
shutil.copytree(folder_names['theme'],install_themes_dir,symlinks=True,ignore_dangling_symlinks=True)
print("\tCopying {} to {}".format(theme_wallpaper, str(Path.home())+"/Pictures/" ))
shutil.copy(theme_wallpaper, str(Path.home())+"/Pictures/")
for i in fonts:
if not os.path.exists( str(Path.home())+"/.fonts/"+os.path.split(i)[1]):
print("\tCopying {} to {}".format(i, str(Path.home())+"/.fonts/" ))
shutil.copy(i, str(Path.home())+"/.fonts/")
font = lfcaptionfont
if font == "MS Sans Serif":
font = "Sans Serif"
font = font + " " + weight[3:].lower().capitalize() + " 8"
print("\tChanging Font: {}".format(font))
print("\n[Updating System]")
xfconf_item = [
["xsettings","/Gtk/CursorThemeName", theme_name+"_Cursors", "Cursors" ],
["xsettings","/Net/IconThemeName", theme_name+"_Icons", "Icons" ],
["xsettings","/Net/ThemeName", theme_name+"_Theme", "Theme" ],
["xfwm4","/general/theme", theme_name+"_Theme", "Windows Manager" ],
["xfwm4","/general/title_font", font, "Font" ]
]
xfconf_query_path = subprocess.check_output(["which", "xfconf-query"]).strip()
for i in xfconf_item:
print("\tChanging {} to {}".format(i[3], i[2]))
args = [
xfconf_query_path,
"-c", i[0],
"-p", i[1],
"-s", i[2]
]
#print(args)
subprocess.check_call(args, stdout=subprocess.DEVNULL)
print("\n{}\n".format("=-" * 40))
plus = ''' ___
.---. .'/ \
_________ _...._ | | / / \
\ |.' '-. | | | | |
\ .'```'. '. | | | | |
\ | \ \| | |/`. .'
| | | || | _ _ _ `.| |
| \ / . | | | ' / | .' | ||___|
| |\`'-.-' .' | | .' | .' | . | /|/___/
| | '-....-'` | | / | / | .'.'| |//.'.--.
.' '. '---'| `'. | .'.'.-' /| | |
'-----------' ' .'| '/.' \_.' \_\ /
`-' `--' `''--' '''
print(plus)
print("Your new theme is installed!\n\n\n\n\n")
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment