diff --git a/Plus/Chicago95_Plus.py b/Plus/Chicago95_Plus.py deleted file mode 100755 index cdede97..0000000 --- a/Plus/Chicago95_Plus.py +++ /dev/null @@ -1,1216 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- - -# Tool to parse and generate new Chicago95 themes based on Microsoft Theme files -# Parses ANI and ICO files, installs Icons/Theme and fonts -# Auto changes your theme -# Thanks to png2svg for icon converter and Fierelier for recoloring script -# Requires: -# - Inkscape -# - Imagemagick -# - xcursorgen -# - Chicago95 icons, theme and cursors installed -# TODO: -# - Add sound support -# - Add better themeing color support -# - Add a gui -# - Better font identification - -import sys -import os -import collections -import PIL.Image -import svgwrite -import pathlib -import shutil -import subprocess -import argparse -import logging -import configparser -import xml.etree.ElementTree as ET -from pathlib import Path -from PIL import Image -from configparser import ConfigParser - -plus = ''' - ██████╗██╗ ██╗██╗ ██████╗ █████╗ ██████╗ ██████╗ █████╗ ███████╗ -██╔════╝██║ ██║██║██╔════╝██╔══██╗██╔════╝ ██╔═══██╗██╔══██╗██╔════╝ -██║ ███████║██║██║ ███████║██║ ███╗██║ ██║╚██████║███████╗ -██║ ██╔══██║██║██║ ██╔══██║██║ ██║██║ ██║ ╚═══██║╚════██║ -╚██████╗██║ ██║██║╚██████╗██║ ██║╚██████╔╝╚██████╔╝ █████╔╝███████║ - ╚═════╝╚═╝ ╚═╝╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚════╝ ╚══════╝ - ___ - .---. .'/ \ - _________ _...._ | | / / \ - \ |.' '-. | | | | | - \ .'```'. '. | | | | | - \ | \ \| | |/`. .' - | | | || | _ _ _ `.| | - | \ / . | | | ' / | .' | ||___| - | |\`'-.-' .' | | .' | .' | . | /|/___/ - | | '-....-'` | | / | / | .'.'| |//.'.--. - .' '. '---'| `'. | .'.'.-' /| | | - '-----------' ' .'| '/.' \_.' \_\ / - `-' `--' `''--' ''' - - -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() - logging.info("\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"): - logging.debug("[x11_cursors]") - 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 - logging.debug("\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" - } - - logging.debug("\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] - logging.info("\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 - - logging.debug("[ANI Header]") - for i in anih: - logging.debug("\t{:<30} {}".format(i,anih[i])) - - rate_length = anih["jif_rate"] - if ani_bytes.find("rate".encode()) > -1: - rate_length = [] - logging.debug("\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 - logging.debug("\t{}".format(rate_length)) - - seq_length = False - if ani_bytes.find("seq ".encode()) > -1: - seq_length = [] - logging.debug("\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 - logging.debug("\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("\tinkscape will open {} times to process {}. (use option -v for more information)".format(len(rgbs), svg_name)) - else: - logging.info("{:<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: - logging.info("{:<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 - 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('cp1252') - - -def parse_IconMetrics(IconMetrics): - 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 IconMetrics.split(): - x.append(int(i)) - - - iconmetrics = { - "cbSize" : int.from_bytes(x[0:4],"little"), - "iHorzSpacing" : int.from_bytes(x[4:8],"little"), - "iVertSpacing" : int.from_bytes(x[8:12],"little"), - "iTitleWrap" : int.from_bytes(x[12:16],"little") - } - - lfFont = { - "Name:" : "lfFont", - "lfHeight" : int.from_bytes(x[16:20],"little"), - "lfWidth" : int.from_bytes(x[20:24],"little"), - "lfEscapement" : int.from_bytes(x[24:28],"little"), - "lfOrientation" : int.from_bytes(x[28:32],"little"), - "lfWeight" : font_weight[str(int.from_bytes(x[32:36],"little"))], - "lfItalic" : x[37], - "lfUnderline" : x[38], - "lfStrikeOut" : x[39], - "lfCharSet" : x[40], - "lfOutPrecision" : x[41], - "lfClipPrecision" : x[42], - "lfQuality" : x[43], - "lfPitchAndFamily" : x[44], - "lfFaceName[32]" : null_string(x[44:44+32]) - } - logging.info("[iconmetrics]") - for i in iconmetrics: - logging.info("\t{:<21} {}".format(i, iconmetrics[i])) - logging.info("[lfFont]") - for i in lfFont: - logging.info("\t{:<21} {}".format(i, lfFont[i])) - - -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]) - } - logging.info("[nonclientmetrics]") - for i in nonclientmetrics: - logging.info("\t{:<21} {}".format(i, nonclientmetrics[i])) - logging.info("[lfcaptionfont]") - for i in lfcaptionfont: - logging.info("\t{:<21} {}".format(i, lfcaptionfont[i])) - logging.info("[lfSmCaptionFont]") - for i in lfSmCaptionFont: - logging.info("\t{:<21} {}".format(i, lfSmCaptionFont[i])) - logging.info("[lfMenuFont]") - for i in lfMenuFont: - logging.info("\t{:<21} {}".format(i, lfMenuFont[i])) - logging.info("[lfStatusFont]") - for i in lfStatusFont: - logging.info("\t{:<21} {}".format(i, lfStatusFont[i])) - logging.info("[lfMessageFont]") - for i in lfMessageFont: - logging.info("\t{:<21} {}".format(i, lfMessageFont[i])) - - return lfcaptionfont["lfFaceName[32]"], lfcaptionfont["lfWeight"] - - -def main(): - print(plus) - print("Microsoft Theme file parser") - desc = '''Chicago95 Plus! is a python script that can parse Windows 95/98/ME/XP theme files and create new Chicago95 themes. Chicago95 Plus! supports Icons, Cursors, Fonts, Wallpapers and Theme colors! Use this against themes you can find on ThemeWorld or any site that have Windows Plus! Themes!\nThis script can be called from any folder, execute it and provide it the path to a theme file (e.g. %(prog)s /home/bgates/Wicked/Wicked.theme) and your new theme will be created!''' - arg_parser = argparse.ArgumentParser(description=desc, - usage='%(prog)s [options] MS_Theme_File', - epilog="Part of the Chicago95 theme project", - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - arg_parser.add_argument('-d', '--debug', help="Print lots of debugging statements", action="store_const", dest="loglevel", const=logging.DEBUG, default=logging.WARNING) - arg_parser.add_argument('-v', '--verbose', help="Be verbose", action="store_const", dest="loglevel", const=logging.INFO) - arg_parser.add_argument("--noinstall", help="Do not place folders nor change theme", action="store_true") - arg_parser.add_argument("--info", help="Does nothing except give information about the theme file", action="store_true") - arg_parser.add_argument('-c', '--colors', help='How many colors before skipping Inkscape fix/merge for SVGs. Set to 1 to speed up conversion. WARNING: This may result in transparent icons!', default=32, type=int) - arg_parser.add_argument('-o', '--overlap', help='Pixel overlap for SVG icons', default=1, type=int) - arg_parser.add_argument('-s', '--squaresize', help='Square size for SVG icons', default=20, type=int) - arg_parser.add_argument("--installdir", help="Folder to create new theme in, default is current working directory", default=os.getcwd()) - arg_parser.add_argument("theme_file", help="Microsoft Windows 95/98/ME .theme file") - args = arg_parser.parse_args() - - if args.info: - args.loglevel = logging.INFO - - logging.basicConfig(level=args.loglevel, format="\t%(message)s") - - error = False - - if not os.path.exists(str(Path.home())+"/.icons/Chicago95") and not os.path.exists(str(Path.home())+"/.icons/Chicago95_tux"): - print("\nERROR: Either the Chicago95 or Chicago95_tux icon theme must be installed to {} to use this script\n".format(str(Path.home())+"/.icons/")) - error = True - - if not os.path.exists(str(Path.home())+"/.icons/Chicago95_Cursor_Black"): - print("\nERROR: The Chicago95 cursor Chicago95_Cursor_Black must be installed to {} to use this script\n".format(str(Path.home())+"/.icons/")) - error = True - - if not os.path.exists(str(Path.home())+"/.themes/Chicago95"): - print("\nERROR: The Chicago95 theme must be installed to {} to use this script\n".format(str(Path.home())+"/.themes/")) - error = True - - try: - inkscape_path = subprocess.check_output(["which", "inkscape"]).strip() - except subprocess.CalledProcessError: - print("\nERROR: You need inkscape installed to use this script.\n") - error = True - - try: - convert_path = subprocess.check_output(["which", "convert"]).strip() - except subprocess.CalledProcessError: - print("\nERROR: You need imagemagick installed to use this script.\n") - error = True - try: - convert_path = subprocess.check_output(["which", "xcursorgen"]).strip() - except subprocess.CalledProcessError: - print("\nERROR: You need xcursorgen installed to use this script.\n") - error = True - - if error: - sys.exit(1) - - # Get the file name and extions in to useable names - theme_file = args.theme_file - 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(" ", "_") - if args.installdir[-1] != "/": - new_theme_folder = args.installdir + "/" + theme_name + "_Chicago95/" - else: - new_theme_folder = args.installdir + 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("[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: - logging.info("{:<21} {}".format(i,icons[i])) - - print("[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)) - logging.info("{:<21} {:<7} ({:<15})".format(color_name, colors[color_name], config["Control Panel\Colors"][color_name])) - - print("[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) - logging.info("{:<21} {}".format(cursor_name, cursors[cursor_name])) - - - ## Get Sound files - print("[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 - logging.info("{:<21} {}".format(sound_name, wav_file)) - - #Screen Saver - screensaver = False - if "boot" in config: - print("[Parser] Parsing Boot (screensaver)") - screensaver = get_file_name(config,"boot","SCRNSAVE.EXE") - logging.info("{:<21} {}".format("screensaver",screensaver)) - ## Get the wallpaper - wallpaper = False - if "Control Panel\Desktop" in config: - print("[Parser] Parsing Wallpaper") - wallpaper = get_file_name(config,"Control Panel\Desktop","Wallpaper") - logging.info("{:<21} {}".format("wallpaper",wallpaper)) - #NonClientMetrics/IconMetrics - print("[Parser] Parsing NonClientMetrics") - NonclientMetrics = config["Metrics"]["nonclientmetrics"] - (lfcaptionfont, weight) = parse_NONCLIENTMETRICS(NonclientMetrics) - print("[Parser] Parsing IconMetrics") - IconMetrics = config["Metrics"]["iconmetrics"] - parse_IconMetrics(IconMetrics) - - print("[Parser] Parsing Complete!\n\n", "=" * 80) - if args.info: - print("\nThank you for using Chicago95 Plus!\n") - sys.exit() - print("\n[Theme Builder]\tMaking 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: - logging.info("{:<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("[Icons]\t\tCreating 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, squaresize = args.squaresize, overlap = args.overlap, max_colors=args.colors) - 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] - logging.info("Size: {:<21} {} {}".format(size, svg_icon_file, sized_target)) - 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("[Cursors]\tGenerating New cursors in {}".format(folder_names['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) - logging.info("{:<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') - logging.debug("\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: - # Ani to png file format: - 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("[Colors]\tChanging theme colors in {}".format(folder_names['theme'])) - - 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: - logging.info("\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: - logging.debug("\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: - logging.debug("\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): - logging.debug("\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("[Desktop]\tWallpaper: {}".format(wallpaper)) - theme_wallpaper = False - if not os.path.exists(path_to_theme+wallpaper): - for lower_file in theme_files: - if wallpaper.lower() in lower_file: - theme_wallpaper = theme_files[lower_file] - else: - theme_wallpaper = wallpaper - if theme_wallpaper: - logging.info("{}".format(os.path.split(wallpaper)[1])) - shutil.copy(theme_wallpaper,folder_names['root']) - theme_wallpaper = folder_names['root'] + os.path.split(theme_wallpaper)[1] - else: - logging.info("Could not copy wallpaper {} to {}".format(path_to_theme+wallpaper,theme_screensaver)) - - if screensaver: - print("[Screensaver]\t{0} copied to {1} (usage: wine {0} /s)".format(path_to_theme+screensaver, folder_names['root'])) - theme_screensaver = False - if not os.path.exists(path_to_theme+screensaver): - for lower_file in theme_files: - if screensaver.lower() in lower_file: - theme_screensaver = theme_files[lower_file] - else: - theme_screensaver = screensaver - if theme_screensaver: - logging.info("{}".format(os.path.split(theme_screensaver)[1])) - shutil.copy(theme_screensaver,folder_names['root']) - theme_screensaver = folder_names['root'] + os.path.split(theme_screensaver)[1] - else: - logging.info("Could not copy screensaver {} to {}".format(path_to_theme+screensaver,theme_screensaver)) - - print("[Fonts]\t\tGetting any font files") - fonts = [] - for files in theme_files: - if ".ttf" in files: - logging.info("{}".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("[Colors]\tCopying Sounds to {}".format(folder_names['sounds'])) - for sound in sounds: - if not sound: - continue - if not os.path.exists(path_to_theme+sounds[sound]): - for lower_file in theme_files: - if sounds[sound] in lower_file: - theme_sound_file_name = theme_files[lower_file] - else: - theme_sound_file_name = sounds[sound] - new_sound_file_name = "{}_{}".format(sound,sounds[sound].replace(" ","_")) - logging.info("Sound: {} copying {} to {}".format(sound,theme_sound_file_name,folder_names['sounds']+new_sound_file_name)) - shutil.copy(theme_sound_file_name,folder_names['sounds']+new_sound_file_name) - - print("[Theme Builder]\tCompleted!\n") - - print("-" * 80) - - 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] - - if not args.noinstall: - - print("\n[Installing]") - shutil.rmtree(install_icons_dir, ignore_errors=True) - shutil.rmtree(install_cursors_dir, ignore_errors=True) - shutil.rmtree(install_themes_dir, ignore_errors=True) - - logging.info("\tCopying {} to {}".format(folder_names['icons'], install_icons_dir)) - shutil.copytree(folder_names['icons'],install_icons_dir,symlinks=True,ignore_dangling_symlinks=True) - logging.info("\tCopying {} to {}".format(folder_names['cursors'], install_cursors_dir)) - shutil.copytree(folder_names['cursors'],install_cursors_dir,symlinks=True,ignore_dangling_symlinks=True) - logging.info("\tCopying {} to {}".format(folder_names['theme'], install_themes_dir)) - shutil.copytree(folder_names['theme'],install_themes_dir,symlinks=True,ignore_dangling_symlinks=True) - logging.info("\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]): - logging.info("\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("[Updating XFCE]") - 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: - logging.info("\tChanging {} to {}".format(i[3], i[2])) - args = [ - xfconf_query_path, - "-c", i[0], - "-p", i[1], - "-s", i[2] - ] - logging.debug(args) - subprocess.check_call(args, stdout=subprocess.DEVNULL) - print("Done!") - - print("\n{}\n".format("=-" * 40)) - print("The Microsoft theme {} is installed!\n".format(theme_name)) - else: - font = lfcaptionfont - print("\n[Skipped Install]\n\tTo install the theme {} run the following commands:\n".format(theme_name)) - print("\tcp -rav {} {}".format(folder_names['icons'], install_icons_dir)) - print("\tcp -rav {} {}".format(folder_names['cursors'], install_cursors_dir)) - print("\tcp -rav {} {}".format(folder_names['theme'], install_themes_dir)) - print("\tcp -rav {} {}".format(theme_wallpaper, str(Path.home())+"/Pictures/" )) - for i in fonts: - print("\tcp {} {}".format(i, str(Path.home())+"/.fonts/" )) - print("\n\tThen change the icons, cursors, appearance and window manager to {}.\n\tThe theme font for window titles is: {}\n".format(theme_name, font)) - - print("Thank you for using Chicago95 Plus!\n") - -main() diff --git a/Plus/Chicago95_no_theme.png b/Plus/Chicago95_no_theme.png deleted file mode 100644 index 0458515..0000000 Binary files a/Plus/Chicago95_no_theme.png and /dev/null differ diff --git a/Plus/Chicago95_theme_1.gif b/Plus/Chicago95_theme_1.gif deleted file mode 100644 index cf665c7..0000000 Binary files a/Plus/Chicago95_theme_1.gif and /dev/null differ diff --git a/Plus/Chicago95_theme_1.png b/Plus/Chicago95_theme_1.png new file mode 100644 index 0000000..a0c78a2 Binary files /dev/null and b/Plus/Chicago95_theme_1.png differ diff --git a/Plus/Chicago95_theme_2.gif b/Plus/Chicago95_theme_2.gif deleted file mode 100644 index ffa15e3..0000000 Binary files a/Plus/Chicago95_theme_2.gif and /dev/null differ diff --git a/Plus/Chicago95_theme_2.png b/Plus/Chicago95_theme_2.png new file mode 100644 index 0000000..18c6579 Binary files /dev/null and b/Plus/Chicago95_theme_2.png differ diff --git a/Plus/Chicago95_themed.png b/Plus/Chicago95_themed.png deleted file mode 100644 index 6aa5582..0000000 Binary files a/Plus/Chicago95_themed.png and /dev/null differ diff --git a/Plus/ChicagoPlus.py b/Plus/ChicagoPlus.py new file mode 100755 index 0000000..397aa45 --- /dev/null +++ b/Plus/ChicagoPlus.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 + +from pluslib import ChicagoPlus +import argparse +import logging +import os +from pathlib import Path +import sys + +def main(): + + + desc = '''Chicago95 Plus! is a python script that can parse Windows 95/98/ME/XP theme files and create new Chicago95 themes. Chicago95 Plus! supports Icons, Cursors, Fonts, Wallpapers, Sounds and Theme colors! Use this against themes you can find on ThemeWorld or any site that have Microsoft Plus! Themes!\nThis script can be called from any folder, execute it and provide it the path to a theme file (e.g. %(prog)s /home/bgates/Wicked/Wicked.theme) and your new theme will be created!''' + arg_parser = argparse.ArgumentParser(description=desc, + usage='%(prog)s [options] MS_Theme_File', + epilog="Part of the Chicago95 theme project", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + arg_parser.add_argument('-d', '--debug', help="Print lots of debugging statements", action="store_const", dest="loglevel", const=logging.DEBUG, default=logging.WARNING) + arg_parser.add_argument('-v', '--verbose', help="Be verbose", action="store_const", dest="loglevel", const=logging.INFO) + arg_parser.add_argument("--noinstall", help="Do not place folders nor change theme", action="store_true") + arg_parser.add_argument("--info", help="Prints the json conversion of the theme file", action="store_true") + arg_parser.add_argument('-c', '--colors', help='How many colors before skipping Inkscape fix/merge for SVGs. Set to 1 to speed up conversion. WARNING: This may result in transparent icons!', default=32, type=int) + arg_parser.add_argument('-o', '--overlap', help='Pixel overlap for SVG icons', default=1, type=int) + arg_parser.add_argument('-s', '--squaresize', help='Square size for SVG icons', default=20, type=int) + arg_parser.add_argument('--cursorfolder', help="Chicago95 cursor folder to convert to new theme", default=str(Path.home())+"/.icons/Chicago95_Cursor_Black") + arg_parser.add_argument('--themefolder', help="Chicago95 theme folder to convert to new theme", default=str(Path.home())+"/.themes/Chicago95") + arg_parser.add_argument('--iconsfolder', help="Chicago95 icons folder to convert to new theme", default=str(Path.home())+"/.icons/Chicago95") + arg_parser.add_argument("--installdir", help="Folder to create new theme in, default is current working directory", default=os.getcwd()) + arg_parser.add_argument("--logfile", help="Filename for debug logging", default="chicago95_plus.log") + arg_parser.add_argument("theme_file", help="Microsoft Windows 95/98/ME .theme file") + specific_args = arg_parser.add_argument_group("Specific Installs") + specific_args.add_argument("--nocursors", help="Do not generate/install/enable the cursors", action="store_false") + specific_args.add_argument("--noicons", help="Do not generate/install/enable the icons", action="store_false") + specific_args.add_argument("--nowallpaper", help="Do not generate/install/enable the wallpaper", action="store_false") + specific_args.add_argument("--nosounds", help="Do not generate/install/enable the sounds", action="store_false") + specific_args.add_argument("--nocolors", help="Do not generate/install/enable the colors", action="store_false") + specific_args.add_argument("--nofonts", help="Do not generate/install/enable the fonts", action="store_false") + specific_args.add_argument("--noscreensaver", help="Do not generate/install the screensaver", action="store_false") + + args = arg_parser.parse_args() + + plus = ChicagoPlus(themefile=args.theme_file, + loglevel=args.loglevel, + colors=args.colors, + overlap=args.overlap, + squaresize=args.squaresize, + installdir=args.installdir, + chicago95_cursor_path=args.cursorfolder, + chicago95_theme_path=args.themefolder, + chicago95_icons_path=args.iconsfolder, + logfile=args.logfile) + + print(plus.logo()) + + if args.info: + plus.parse_theme() + plus.print_theme_config() + sys.exit(0) + + if args.noinstall: + plus.check_software(args.nocursors, args.noicons, args.colors) + plus.parse_theme() + plus.generate_theme(cursors=args.nocursors, icons=args.noicons, wallpaper=args.nowallpaper, sounds=args.nosounds, colors=args.nocolors, fonts=args.nofonts, screensaver=args.noscreensaver) + else: + plus.go(cursors=args.nocursors, icons=args.noicons, wallpaper=args.nowallpaper, sounds=args.nosounds, colors=args.nocolors, fonts=args.nofonts, screensaver=args.noscreensaver) + +main() diff --git a/Plus/README.MD b/Plus/README.MD index b332b81..0997ffc 100644 --- a/Plus/README.MD +++ b/Plus/README.MD @@ -1,34 +1,29 @@ # Chicago95 Plus! #### Windows Plus! Theme Conversion for XFCE/Chicago95 -Is Chicago95 not authentic enough for you? Do you want to use Windows Plus! themes to personalize your Chicago95 experience? Well look no further! +Is Chicago95 not authentic enough for you? Do you want to use Microsoft Plus! themes to personalize your Chicago95 experience? Well look no further! -Download any `.theme` file from the web, point `Chicago95_plus.py` at it and it will generate a wholy new theme for you to install. - -Change Chicago95 from this: - -![No Theme](Chicago95_no_theme.png) - -to this! - -![Themed](Chicago95_themed.png) +Download any Windows 95, 98, ME or XP theme file (from places like themeworld.com or archive.org) and use this tool to switch to your new theme! Currently supports: -* Icons -* Cursors +* Icons (ICL, DLL and ICO files) +* Cursors (CUR and ANI files) * Theme colors * Fonts +* Sounds * Wallpapers (installed to `/home/$USER/Pictures`) -Sounds are copied to the output folder and named but arent installed yet. +Sounds are installed assuming you have XFCE sounds enabled per the Chicago95 Install instructions. + +To use Chicago95 Plus! on Xubuntu you need to install the following: `python3-svgwrite` `python3-fonttools` `inkscape`: `sudo apt install python3-svgwrite python3-fonttools inkscape` By default Chicago95 Plus! creates the theme in your working folder and copies the files to their required location then automatically changes the current theme to the new theme, just like Windows! If you want to get more out Chicago95 Plus! it supports multiple options: * `--installdir DIRECTORY` generates the new theme files and outputs them to DIRECTORY -* `--noinstall` generates the theme files but does not move them or run the commands to switch the theme +* `--noinstall` generates the theme files but does not move them or run the commands to enable the theme * `--info` Gives verbose information about the theme file, does not do any conversion * `--verbose`/`--debug` gives more/way more information about what is happening * `--colors`/`--squaresize`/`--overlap` controls the way Icon files are converted to svg @@ -36,16 +31,15 @@ If you want to get more out Chicago95 Plus! it supports multiple options: Detailed Help output: ``` -Microsoft Theme file parser -usage: Chicago95_Plus.py [options] MS_Theme_File +usage: ChicagoPlus.py [options] MS_Theme_File Chicago95 Plus! is a python script that can parse Windows 95/98/ME/XP theme files and create new Chicago95 themes. Chicago95 Plus! supports Icons, -Cursors, Fonts, Wallpapers and Theme colors! Use this against themes you can -find on ThemeWorld or any site that have Windows Plus! Themes! This script can -be called from any folder, execute it and provide it the path to a theme file -(e.g. Chicago95_Plus.py /home/bgates/Wicked/Wicked.theme) and your new theme -will be created! +Cursors, Fonts, Wallpapers, Sounds and Theme colors! Use this against themes +you can find on ThemeWorld or any site that have Microsoft Plus! Themes! This +script can be called from any folder, execute it and provide it the path to a +theme file (e.g. ChicagoPlus.py /home/bgates/Wicked/Wicked.theme) and your new +theme will be created! positional arguments: theme_file Microsoft Windows 95/98/ME .theme file @@ -55,8 +49,8 @@ optional arguments: -d, --debug Print lots of debugging statements (default: 30) -v, --verbose Be verbose (default: None) --noinstall Do not place folders nor change theme (default: False) - --info Does nothing except give information about the theme - file (default: False) + --info Prints the json conversion of the theme file (default: + False) -c COLORS, --colors COLORS How many colors before skipping Inkscape fix/merge for SVGs. Set to 1 to speed up conversion. WARNING: This @@ -65,19 +59,44 @@ optional arguments: Pixel overlap for SVG icons (default: 1) -s SQUARESIZE, --squaresize SQUARESIZE Square size for SVG icons (default: 20) + --cursorfolder CURSORFOLDER + Chicago95 cursor folder to convert to new theme + (default: /home/phil/.icons/Chicago95_Cursor_Black) + --themefolder THEMEFOLDER + Chicago95 theme folder to convert to new theme + (default: /home/phil/.themes/Chicago95) + --iconsfolder ICONSFOLDER + Chicago95 icons folder to convert to new theme + (default: /home/phil/.icons/Chicago95) --installdir INSTALLDIR Folder to create new theme in, default is current - working directory (default: - /home/grassmunk/Themes/Chicago95/Plus) + working directory (default: /home/phil/PlusGTK) + --logfile LOGFILE Filename for debug logging (default: + chicago95_plus.log) + +Specific Installs: + --nocursors Do not generate/install/enable the cursors (default: + True) + --noicons Do not generate/install/enable the icons (default: + True) + --nowallpaper Do not generate/install/enable the wallpaper (default: + True) + --nosounds Do not generate/install/enable the sounds (default: + True) + --nocolors Do not generate/install/enable the colors (default: + True) + --nofonts Do not generate/install/enable the fonts (default: + True) + --noscreensaver Do not generate/install the screensaver (default: + True) Part of the Chicago95 theme project + ``` -Here are two examples running Chicago95 Plus! to change the theme: +Here are two examples of themes built using Chicago95 Plus! -Changing themes: +![Tiberian Sun](Chicago95_theme_1.png) -![Original to Hackerz](Chicago95_theme_1.gif) - -![Hackers to Christmas](Chicago95_theme_2.gif) +![FRIENDS](Chicago95_theme_2.png) diff --git a/Plus/pluslib.py b/Plus/pluslib.py new file mode 100644 index 0000000..06e0db8 --- /dev/null +++ b/Plus/pluslib.py @@ -0,0 +1,3088 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Tool to parse and generate new Chicago95 themes based on Microsoft Theme files +# Parses ANI and ICO files, installs Icons/Theme and fonts +# Auto changes your theme +# Thanks to png2svg for icon converter and Fierelier for recoloring script +# Requires: +# - Inkscape +# - Imagemagick +# - xcursorgen +# - Chicago95 icons, theme and cursors installed +# TODO: +# - Add a gui +# - Fix if missing colors +# Known Bugs: +# - If the theme is missing one of these colors the script will crash: ButtonDKShadow, ButtonLight, ButtonShadow, ButtonHilight, ButtonFace, ButtonText. Fix it by adding the missing color to the theme + + + +import io +import os +import sys +import json +import struct +import shutil +import logging +import svgwrite +import PIL.Image +import subprocess +import configparser +import logging.handlers +import xml.etree.ElementTree as ET + +from pathlib import Path +from pprint import pprint +from fontTools import ttLib +from configparser import ConfigParser +from PIL import BmpImagePlugin, PngImagePlugin, Image + +SCREEN_SAVER_SCRIPT = '''#!/bin/sh + +# *** DEPENDS ON xprintidle AND wmctrl AND wine *** +# From: https://joefreeman.weebly.com/using-windows-sreensavers-on-linux.html +# UNTESTED USE AT YOUR OWN RISK +# Screensaver to use +Screensaver="{scr_file}" + +# Minutes to wait before activating +Timeout=10 + +#Convert minutes to milliseconds +IDLE_TIME=$(($Timeout*60*1000)) + +# Clobber normal Linux Screensaver and screen-blanking. +xset s off -dpms + +sleep_time=$IDLE_TIME +triggered=false + +# ceil() instead of floor() +while sleep $(((sleep_time+999)/1000)); do + idle=$(xprintidle) + if [ $idle -ge $IDLE_TIME ]; then + if ! $triggered; then + # Get a list of open windows and count the number of times YouTube &c is on it. + youtube=`wmctrl -l|egrep -c 'YouTube|My5|All 4'` + if [ $youtube -ge 1 ]; then + triggered=false + sleep_time=$IDLE_TIME + else + wine $Screensaver /s + triggered=true + sleep_time=$IDLE_TIME + fi + fi + else + triggered=false + # Give 100 ms buffer to avoid frantic loops shortly before triggers. + sleep_time=$((IDLE_TIME-idle+100)) + fi''' + + +#ChicagoPlus Class +# Input: +# - themefile: Full path to a Micosoft Windows .theme file +# - colors/overlap/squaresize: determines how inkscape creates scaled icons from ICO files. Less colors == faster. +# - installdir: the director to install the converted theme to, decaults to current working directory +# - chicago95_cursor_path/chicago95_theme_path/chicago95_icons_path: This class needs Chicago95 icons/themes/cursor folders to convert from +# the default is set to the install locations for these themes +# - loglevel: the current log level based on python logging to display to STDOUT +# - logfile: file which will house debug log + + +class ChicagoPlus: + def __init__(self, themefile, colors=32, overlap=1, + squaresize=20, installdir=os.getcwd(), + chicago95_cursor_path=str(Path.home())+"/.icons/Chicago95_Cursor_Black", + chicago95_theme_path=str(Path.home())+"/.themes/Chicago95", + chicago95_icons_path=str(Path.home())+"/.icons/Chicago95", + loglevel=logging.WARNING, + logfile='plus.log'): + + self.theme_file = themefile + self.max_colors = colors + self.overlap = overlap + self.squaresize = squaresize + self.installdir = installdir + self.path_to_theme='' + self.theme_file_name='' + self.theme_name_spaces='' + self.theme_ext='' + self.theme_name = '' + self.new_theme_folder='' + self.chicago95_cursor_folder = chicago95_cursor_path + self.chicago95_theme_folder = chicago95_theme_path + self.chicago95_icons_folder = chicago95_icons_path + + + # Create the Logger + self.logger = logging.getLogger(__name__) + self.logger.setLevel(logging.DEBUG) + #logger_formatter = logging.Formatter('%(name)s :: %(levelname)s :: %(message)s') + logger_formatter = logging.Formatter('%(levelname)-8s :: %(funcName)-22s :: %(message)s') + # Log everything to the log file + fh = logging.FileHandler(logfile, mode='w') + fh.setLevel(logging.DEBUG) + fh.setFormatter(logger_formatter) + # Log whatever we get passed to stderr + ch = logging.StreamHandler() + ch.setFormatter(logger_formatter) + ch.setLevel(loglevel) + # Add the Handler to the Logger + self.logger.addHandler(fh) + self.logger.addHandler(ch) + self.logger.debug("Theme File: {}".format(self.theme_file)) + self.logger.debug("Install directory: {}".format(self.installdir)) + self.logger.debug("Convert icon colors: {}, overlap: {}, squaresize: {}".format(self.max_colors, self.overlap, self.squaresize)) + + def set_installdir(self, new_install_directory): + self.logger.debug("Changing install directory to: {}".format(new_install_directory)) + self.installdir = new_install_directory + + def go(self, cursors=True, icons=True, wallpaper=True, sounds=True, colors=True, fonts=True, screensaver=True): + self.logger.info("Starting Chicago Plus! with the folowing settings: cursors={}, icons={}, wallpaper={}, sounds={}, colors={}, fonts={}, screensaver={}".format(cursors, icons, wallpaper, sounds, colors, fonts, screensaver)) + self.check_software(cursors, icons, colors) + self.parse_theme() + self.generate_theme(cursors, icons, wallpaper, sounds, colors, fonts, screensaver) + self.install_theme(cursors, icons, wallpaper, sounds, colors, fonts, screensaver) + self.enable_theme(cursors, icons, wallpaper, sounds, colors, fonts, screensaver) + + def check_software(self, cursors=True, icons=True, colors=True): + self.logger.info("Checking for required installed software") + + error = False + if icons: + if not os.path.exists(self.chicago95_icons_folder): + self.logger.critical("Either the Chicago95 or Chicago95_tux icon theme must be installed to {} to use this library".format(str(Path.home())+"/.icons/")) + error = True + try: + inkscape_path = subprocess.check_output(["which", "inkscape"]).strip() + except subprocess.CalledProcessError: + self.logger.critical("You need inkscape installed to use this library.") + error = True + if cursors: + + if not os.path.exists(self.chicago95_cursor_folder): + self.logger.critical("The Chicago95 cursor Chicago95_Cursor_Black must be installed to {} to use this library".format(str(Path.home())+"/.icons/")) + error = True + + try: + convert_path = subprocess.check_output(["which", "xcursorgen"]).strip() + except subprocess.CalledProcessError: + self.logger.critical("You need xcursorgen installed to use this library.") + error = True + + if colors: + if not os.path.exists(self.chicago95_theme_folder): + self.logger.critical("The Chicago95 theme must be installed to {} to use this library".format(str(Path.home())+"/.themes/")) + error = True + + try: + convert_path = subprocess.check_output(["which", "convert"]).strip() + except subprocess.CalledProcessError: + self.logger.critical("You need imagemagick (convert) installed to use this library.") + error = True + + try: + convert_path = subprocess.check_output(["which", "mogrify"]).strip() + except subprocess.CalledProcessError: + self.logger.critical("You need imagemagick (mogrify) installed to use this library.") + error = True + + if error: + sys.exit(-1) + + + def parse_theme(self): + # This function takes the theme file passed at object instanciation and converts it to an easier to parse JSON file + # Also fixes disparities between theme file case and filename case + # Tries to fix 8 char limit as well + + self.theme_paths() + self.read_theme_file() + self.parse_icons() + icons = self.icons + for i in self.icons: + if self.icons[i]: + ico_filename = self.icons[i][0] + index = self.icons[i][1] + ico_file_path = self.get_actual_path(ico_filename) + icons[i] = { + 'filename' : ico_filename, + 'index' : index, + 'path' : ico_file_path, + 'type' : os.path.splitext(ico_filename)[1][1:] + } + + self.parse_cursors() + self.parse_wallpaper() + self.parse_sound_files() + self.parse_nonclientmetrics() + self.parse_iconmetrics() + self.parse_colors() + self.parse_screensaver() + self.parse_font_files() + if self.screensaver: + scr = self.get_actual_path(self.screensaver) + else: + scr = False + self.find_all_wallpapers() + + self.theme_config = { + 'theme_name' : os.path.splitext(self.theme_file_name)[0], + 'theme_file' : self.theme_file, + 'installdir' : self.installdir, + 'icons' : icons, + 'cursors' : self.cursors, + 'fonts' : self.fonts, + 'wallpaper' : {'theme_wallpaper': self.wallpaper, 'extra_wallpapers' : self.extra_wallpapers}, + 'colors' : self.colors, + 'nonclientmetrics' : self.nonclientmetrics, + 'iconmetrics' : self.iconmetrics, + 'sounds': self.sounds, + 'screensaver': scr, + 'all_files': self.theme_files + } + + def generate_theme(self, cursors=True, icons=True, wallpaper=True, sounds=True, colors=True, fonts=True, screensaver=True): + + self.install_folders() + + self.create_folders() + + if cursors: + self.create_cursors() + if icons: + self.create_icons() + if wallpaper: + self.generate_wallpaper() + if sounds: + self.generate_sounds() + if colors: + self.convert_colors() + if fonts: + self.generate_fonts() + if screensaver: + self.generate_screensaver() + + self.dump_json(self.new_theme_folder+self.theme_name+".json") + + def dump_json(self, json_file_target='windows_theme.json'): + self.logger.info("Dumping {} to JSON file {}".format(self.theme_file, json_file_target)) + with open(json_file_target, 'w') as outfile: + json.dump(self.theme_config, outfile) + self.logger.debug("Done".format(self.theme_file, json_file_target)) + + def print_theme_config(self): + self.logger.info("Print {} to JSON config".format(self.theme_file)) + pprint(self.theme_config) + + + def get_actual_path(self, filename): + # function to get the actual path, thanks windows caselessness + if not filename: + return False + self.logger.debug("Finding '{}'".format(filename)) + if not os.path.exists(self.path_to_theme + filename.rstrip('\x00')): + try: + actual_file_path = [self.theme_files[i] for i in self.theme_files if filename.lower() in i][0] + except IndexError: + actual_file_path = False + else: + actual_file_path = self.path_to_theme + filename + self.logger.debug("Path to '{}': {}".format(filename, actual_file_path)) + return(actual_file_path) + + +#### Theme Parser functions + + def theme_paths(self): + # There's a few ways theme files come packaged + # 1) All the files in one folder + # 2) The .theme file in one folder and everything else in a sub folder + # 3) A folder with Program Files/Plus!/Themes/ and WINDOWS/SYSTEM + # This method tries to find all the files/folders no matter what + + self.logger.debug("Using file {}".format(self.theme_file)) + self.path_to_theme, self.theme_file_name = os.path.split(self.theme_file) + if len(self.path_to_theme) != 0: + self.path_to_theme = self.path_to_theme + "/" + else: + self.path_to_theme = "./" + + theme_name_spaces, theme_ext = os.path.splitext(self.theme_file_name) + + self.index_theme_name = theme_name_spaces + " (Chicago 95 Variant)" # For various Index.theme files + self.theme_name = theme_name_spaces.capitalize().replace(" ", "_") + if self.installdir[-1] != "/": + self.new_theme_folder = self.installdir + "/" + self.theme_name + "_Chicago95/" + else: + self.new_theme_folder = self.installdir + self.theme_name + "_Chicago95/" + + if "Program Files/Plus!/Themes/".lower() in self.path_to_theme.lower(): + paths = self.splitall(self.path_to_theme) + + self.path_to_theme = ('/'.join(paths[0:-4])) + "/" + + self.logger.debug("Path to theme: {}, theme file name: {}".format(self.path_to_theme, self.theme_file_name)) + self.logger.debug("New theme folder: {}".format(self.new_theme_folder)) + + # we use thise dict to remove case but keep the filenames + self.theme_files = {} + self.logger.debug("Files in theme directory {}".format(self.path_to_theme)) + for root, dirs, files in os.walk(self.path_to_theme, topdown=False): + for name in files: + self.theme_files[os.path.join(root, name).lower()] = os.path.join(root, name) + self.logger.debug(self.theme_files[os.path.join(root, name).lower()]) + + def read_theme_file(self): + + if os.stat(self.theme_file).st_size == 0: + self.logger.critical("Theme file {} is empty".format(self.theme_file)) + sys.exit(-1) + + if not os.path.exists(self.theme_file): + self.logger.critical("Theme file {} does not exist".format(self.theme_file)) + sys.exit(-1) + + self.config = ConfigParser(interpolation=None) + try: + self.config.read(self.theme_file) + except UnicodeDecodeError: + try: + self.config.read(self.theme_file, encoding='iso-8859-14') + except configparser.DuplicateSectionError as w: + self.logger.critical("Error reading {}. Remove duplicate section to use this theme. Error: {}".format(self.theme_file, w)) + sys.exit(-1) + except configparser.DuplicateOptionError as w: + self.logger.critical("Error reading {}. Remove duplicate options to use this theme. Error: {}".format(self.theme_file, w)) + sys.exit(-1) + except configparser.MissingSectionHeaderError as w: + self.logger.critical("Error reading {}. Make sure all comments start with ; in the theme file and reload this theme. Error: {}".format(self.theme_file, w)) + sys.exit(-1) + except configparser.ParsingError as w: + self.logger.critical("Error reading {}. Error: {}".format(self.theme_file, w)) + sys.exit(-1) + except configparser.DuplicateSectionError as w: + self.logger.critical("Error reading {}. Remove duplicate section to use this theme. Error: {}".format(self.theme_file, w)) + sys.exit(-1) + except configparser.DuplicateOptionError as w: + self.logger.critical("Error reading {}. Remove duplicate options to use this theme. Error: {}".format(self.theme_file, w)) + sys.exit(-1) + except configparser.MissingSectionHeaderError as w: + self.logger.critical("Error reading {}. Make sure all comments start with ; in the theme file and reload this theme. Error: {}".format(self.theme_file, w)) + sys.exit(-1) + except configparser.ParsingError as w: + self.logger.critical("Error reading {}. Error: {}".format(self.theme_file, w)) + sys.exit(-1) + + #except: + # print("Error reading theme file: {}".format(self.theme_file)) + # print("Usually this is because of comments missing the ; on the first line.") + # sys.exit(-1) + + def parse_icons(self): + self.logger.info("Parsing Icons") + self.icons = {} + + self.icons["my_computer"] = self.get_icon_file_name("CLSID\\{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\DefaultIcon","DefaultValue") + + if self.get_file_name("CLSID\\{450D8FBA-AD25-11D0-98A8-0800361B1103}\\DefaultIcon","DefaultValue"): + self.icons["my_documents"] = self.get_icon_file_name("CLSID\\{450D8FBA-AD25-11D0-98A8-0800361B1103}\\DefaultIcon","DefaultValue") + elif self.get_file_name("CLSID\\{59031A47-3F72-44A7-89C5-5595FE6B30EE}\\DefaultIcon","DefaultValue"): + self.icons["my_documents"] = self.get_icon_file_name("CLSID\\{59031A47-3F72-44A7-89C5-5595FE6B30EE}\\DefaultIcon","DefaultValue") + else: + self.icons["my_documents"] = False + + if self.get_file_name("CLSID\\{208D2C60-3AEA-1069-A2D7-08002B30309D}\\DefaultIcon","DefaultValue"): + self.icons["network_neighborhood"] = self.get_icon_file_name("CLSID\\{208D2C60-3AEA-1069-A2D7-08002B30309D}\\DefaultIcon","DefaultValue") + elif self.get_file_name("CLSID\\{F02C1A0D-BE21-4350-88B0-7367FC96EF3C}\\DefaultIcon","DefaultValue"): + self.icons["network_neighborhood"] = self.get_icon_file_name("CLSID\\{F02C1A0D-BE21-4350-88B0-7367FC96EF3C}\\DefaultIcon","DefaultValue") + else: + self.icons["network_neighborhood"] = False + + self.icons["recycle_bin_full"] = self.get_icon_file_name("CLSID\\{645FF040-5081-101B-9F08-00AA002F954E}\\DefaultIcon","Full") + + self.icons["recycle_bin_empty"] = self.get_icon_file_name("CLSID\\{645FF040-5081-101B-9F08-00AA002F954E}\\DefaultIcon","Empty") + + self.logger.debug("{:<21} {}".format('Type', 'Icon Name')) + for i in self.icons: + self.logger.info("{:<21} | {}".format(i,self.icons[i])) + + def parse_colors(self): + self.logger.info("Parsing Control Panel\Colors") + + color_desc = { + # From: + # https://www.neowin.net/forum/topic/624901-windows-colors-explained/ + # https://web.archive.org/web/20151019120141/https://www.neowin.net/forum/topic/624901-windows-colors-explained/ + + # Text Colors + 'buttontext': 'shown on 3D Buttons', + 'graytext': 'Unknown, MS documentation is inaccurate', + 'hottrackingcolor': 'active text, such as a link', + 'menutext': 'shown on the MenuBar and Menus', + 'windowtext': 'the main text inside a window', + + # Selection + 'hilight': 'background color of a selection', + 'highlight': 'background color of a selection', + 'hilighttext': 'foreground text color of the selection', + 'highlighttext': 'foreground text color of the selection', + 'menuhilight': 'background color of a selection on a menu', + + # Active Caption Bar (i.e. focused window title bar) + 'activetitle': 'Title bar active color', + 'titletext': 'Title bar text color for active windows', + 'gradientactivetitle': 'Windows 98 second color gradient on right', + + # Inactive Caption Bar + 'inactivetitle': 'Title bar color for inactive windows', + 'inactivetitletext': 'Title bar text color for inactive windows', + 'gradientinactivetitle': 'Windows 98 one of two colors, this is the right gradient', + + # Tooltip + 'infotext': 'tooltip text color', + 'infowindow': 'the color of the tooltip itself (default is a light yellow)', + + # Windows 3D Colors (the 4, one pixel wide, colors on every button/windows/etc that provide the 3d look) + 'buttonface': 'main button color', + 'buttonlight': 'top/left inside', + 'buttondkshadow': 'bottom/right outside', + 'buttonhilight': 'top/left outside', + 'buttonhighlight': 'top/left outside', + 'buttonshadow': 'bottom/right inside', + + # Other Window Colors + 'activeborder': ' border of the active window, drawn inside the 3D colors', + 'inactiveborder': 'border of the inactive window, drawn inside the 3D colors', + 'menu': 'color of a menu, overrides the ButtonFace attribute', + 'menubar': 'color of the menubar, unused by Plus!', + 'scrollbar': 'color of the scrollbar TRACK, scrollbar itself drawn using 3D Button attributes', + 'window': ' main background color inside a window, typically white', + 'windowframe': 'typically the single line border seen around active buttons', + 'appworkspace': 'background color in an application that may contain windows, such as GIMP', + 'background': 'desktop area, with no wallpaper applied', + 'buttonalternateface' : "Unknown", + 'messageboxtext': 'Text in a messagebox, unused by Plus!', + 'messagebox': 'messagebox, unused by Plus!' + } + + self.colors = {} + + if "Control Panel\Colors" in self.config: + for color_name in self.config["Control Panel\Colors"]: + if len(self.config["Control Panel\Colors"][color_name].split()) > 2: + r, g, b = self.config["Control Panel\Colors"][color_name].split() + self.colors[color_name] = {'color': '#{:02x}{:02x}{:02x}'.format(int(r),int(g),int(b)), 'description' : color_desc[color_name] } + self.logger.info("{:<21} | {:<7} ({:<11}) desc: {}".format(color_name, self.colors[color_name]['color'], self.config["Control Panel\Colors"][color_name],self.colors[color_name]['description'])) + + def parse_cursors(self): + self.logger.info("Parsing Control Panel\Cursors") + self.cursors = {} + if "Control Panel\Cursors" in self.config: + for cursor_name in self.config["Control Panel\Cursors"]: + if self.get_file_name("Control Panel\Cursors",cursor_name): + cur_type = os.path.splitext(self.get_file_name("Control Panel\Cursors",cursor_name))[1][1:] + self.cursors[cursor_name] = { + 'type' : cur_type , + 'filename': self.get_file_name("Control Panel\Cursors",cursor_name), + 'path' : self.get_actual_path(self.get_file_name("Control Panel\Cursors",cursor_name))} + else: + self.cursors[cursor_name] = False + self.logger.info("{:<21} | {}".format(cursor_name, self.cursors[cursor_name])) + + + def parse_sound_files(self): + ## Get Sound files + self.logger.info("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" + ] + + self.sounds = {} + for i in sound_names: + sound_name = i.split("\\")[-2] + if self.get_file_name(i,"DefaultValue"): + wav_file = self.get_file_name(i,"DefaultValue") + self.sounds[sound_name] = self.get_actual_path(wav_file) + self.logger.info("{:<21} | {}".format(sound_name, wav_file)) + + def parse_screensaver(self): + #Screen Saver + self.screensaver = False + if "boot" in self.config: + self.logger.info("Parsing Boot (screensaver)") + self.screensaver = self.get_file_name("boot","SCRNSAVE.EXE", True) + self.logger.info("{:<21} | {}".format("screensaver",self.screensaver)) + + def parse_wallpaper(self):## Get the wallpaper + self.wallpaper = False + + + if "Control Panel\Desktop" in self.config: + self.wallpaper = {'wallpaper' : False, "tilewallpaper": False, "wallpaperstyle" : 0, 'filename': ''} + self.logger.info("Parsing Control Panel\Desktop Wallpaper") + self.wallpaper['wallpaper'] = self.get_file_name("Control Panel\Desktop","Wallpaper", ignore_windir=True) + # TileWallpaper=0 + # 0: The wallpaper picture should not be tiled + # 1: The wallpaper picture should be tiled + + if self.config.has_option("Control Panel\Desktop",'tilewallpaper') and self.config["Control Panel\Desktop"]["tilewallpaper"] != '0': + self.wallpaper['tilewallpaper'] = True + if self.config.has_option("Control Panel\Desktop", "wallpaperstyle"): + if isinstance(self.config["Control Panel\Desktop"]["wallpaperstyle"], list): + self.wallpaper["wallpaperstyle"] = self.config["Control Panel\Desktop"]["wallpaperstyle"][0] + else: + self.wallpaper["wallpaperstyle"] = self.config["Control Panel\Desktop"]["wallpaperstyle"] + + + # WallpaperStyle=2 + #; 0: The image is centered if TileWallpaper=0 or tiled if TileWallpaper=1 + #; 2: The image is stretched to fill the screen + #; 6: The image is resized to fit the screen while maintaining the aspect + # ratio. (Windows 7 and later) + #; 10: The image is resized and cropped to fill the screen while maintaining + # the aspect ratio. (Windows 7 and later) + + if self.wallpaper['wallpaper']: + self.wallpaper['path'] = self.get_actual_path(self.wallpaper['wallpaper']) + try: + self.wallpaper['wallpaperstyle'] = int(self.wallpaper['wallpaperstyle']) + except ValueError: + self.wallpaper['wallpaperstyle'] = 2 + + self.wallpaper['new_filename'] = (os.path.splitext(self.wallpaper['wallpaper'])[0] + "_" + + str(int(self.wallpaper['tilewallpaper'])) + "_" + + str(int(self.wallpaper['wallpaperstyle'])) + os.path.splitext(self.wallpaper['wallpaper'])[1]) + for i in self.wallpaper: + self.logger.info("{:<21} | {}".format(i,self.wallpaper[i])) + + def parse_font_files(self): + self.logger.debug("Parsing Font files") + # First find font files + self.fonts = {} + fonts = [self.theme_files[i] for i in self.theme_files if "ttf" in i] + for font in fonts: + self.logger.info("Parsing font file: {}".format(font)) + try: + ttf = ttLib.TTFont(font) + except ttLib.TTLibError as error: + self.logger.error("Font {} cannot be read: {}".format(font, error)) + return + + name, family = self.font_name(ttf) + self.fonts[family] = { "name" : name, "family" : family, 'path': font } + for i in self.fonts: + self.logger.info("{:<21} | {}".format("Font file:", i)) + self.logger.info("{:<21} | {}".format("Font name:", self.fonts[i]['name'])) + self.logger.info("{:<21} | {}".format("Font family:", self.fonts[i]['family'])) + + + def parse_nonclientmetrics(self): + self.logger.info("Parsing NonClientMetrics") + self.nonclientmetrics = False + if 'Metrics' not in self.config: + return + + if 'NonClientMetrics' not in self.config['Metrics']: + return + + if len(self.config["Metrics"]["NonClientMetrics"]) <= 1: + return + + NONCLIENTMETRICSA = self.config["Metrics"]["nonclientmetrics"] + + 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", + "1000":"FW_BLACK" + + } + + x = [] + for i in NONCLIENTMETRICSA.split(): + if(int(i)) > 256: + self.logger.error("NonClientMetrics has a byte larger than 255, cannot parse NonClientMetrics!") + return + x.append(int(i)) + + + self.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") + } + self.nonclientmetrics['lfcaptionfont'] = { # This is the font used for titlebars + "desc:" : "Font used for windows title bar (captions)", + "lfHeight" : int.from_bytes(x[24:28],"little", signed=True), + "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(round(int.from_bytes(x[40:44],"little"),-2)))], + "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]" : self.null_string(x[52:52+32]) + } + + self.nonclientmetrics["iSmCaptionWidth"] = int.from_bytes(x[84:88],"little") + self.nonclientmetrics["iSmCaptionHeight"] = int.from_bytes(x[88:92],"little") + + self.nonclientmetrics['lfSmCaptionFont'] = { # This is used for docked title bars + "desk" : "Font used for docked windows title bar (small caption)", + "lfHeight" : int.from_bytes(x[92:96],"little", signed=True), + "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(round(int.from_bytes(x[108:112],"little"),-2)))], + "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]" : self.null_string(x[120:120+32]) + } + + self.nonclientmetrics["iMenuWidth"] = int.from_bytes(x[152:156],"little") + self.nonclientmetrics["iMenuHeight"] = int.from_bytes(x[156:160],"little") + + self.nonclientmetrics['lfMenuFont'] = { # The font used in menus + "desc" : "Font used in menus", + "lfHeight" : int.from_bytes(x[160:164],"little", signed=True), + "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(round(int.from_bytes(x[176:180],"little"),-2)))], + "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]" : self.null_string(x[188:188+32]) + } + + self.nonclientmetrics['lfStatusFont'] = { # Status bars and tooltips font + "desc" : "Font used in status bars and tooltips", + "lfHeight" : int.from_bytes(x[220:224],"little", signed=True), + "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(round(int.from_bytes(x[236:240],"little"),-2)))], + "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]" : self.null_string(x[248:248+32]) + } + + self.nonclientmetrics['lfMessageFont'] = { # Text in message boxes font + "desc" : "Font used in message boxes", + "lfHeight" : int.from_bytes(x[280:284],"little", signed=True), + "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(round(int.from_bytes(x[296:300],"little"),-2)))], + "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]" : self.null_string(x[308:308+32]) + } + self.logger.debug("[nonclientmetrics]") + for i in self.nonclientmetrics: + if i in ['lfcaptionfont','lfMessageFont','lfStatusFont','lfMenuFont','lfSmCaptionFont']: + for j in self.nonclientmetrics[i]: + self.logger.debug("{:<21} | {:<21} | {}".format(i, j, self.nonclientmetrics[i][j])) + else: + self.logger.debug("{:<21} | {}".format(i, self.nonclientmetrics[i])) + + + #return lfcaptionfont["lfFaceName[32]"], lfcaptionfont["lfWeight"] + + def parse_iconmetrics(self): + self.logger.info("Parsing IconMetrics") + self.iconmetrics = False + + if not self.config.has_section('Metrics'): + return + + if 'IconMetrics' not in self.config['Metrics']: + return + + if len(self.config["Metrics"]["IconMetrics"]) <= 1: + return + + IconMetrics = self.config["Metrics"]["iconmetrics"] + + 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 IconMetrics.split(): + if(int(i)) > 256: + self.logger.error("IconMetrics has a byte larger than 255, cannot parse IconMetrics") + return + x.append(int(i)) + + + self.iconmetrics = { + "cbSize" : int.from_bytes(x[0:4],"little"), + "iHorzSpacing" : int.from_bytes(x[4:8],"little"), + "iVertSpacing" : int.from_bytes(x[8:12],"little"), + "iTitleWrap" : int.from_bytes(x[12:16],"little") + } + + self.iconmetrics['lfFont'] = { + "desc:" : "Font used to display icons", + "lfHeight" : int.from_bytes(x[16:20],"little"), + "lfWidth" : int.from_bytes(x[20:24],"little"), + "lfEscapement" : int.from_bytes(x[24:28],"little"), + "lfOrientation" : int.from_bytes(x[28:32],"little"), + "lfWeight" : font_weight[str(int.from_bytes(x[32:36],"little"))], + "lfItalic" : x[37], + "lfUnderline" : x[38], + "lfStrikeOut" : x[39], + "lfCharSet" : x[40], + "lfOutPrecision" : x[41], + "lfClipPrecision" : x[42], + "lfQuality" : x[43], + "lfPitchAndFamily" : x[44], + "lfFaceName[32]" : self.null_string(x[44:44+32]) + } + self.logger.debug("[iconmetrics]") + for i in self.iconmetrics: + if i in ['lfFont']: + for j in self.iconmetrics[i]: + self.logger.debug("{:<21} | {:<21} | {}".format(i, j, self.iconmetrics[i][j])) + else: + self.logger.debug("{:<21} | {}".format(i, self.iconmetrics[i])) + + +#### Generator Functions + def install_folders(self): + self.chicago95_cursor_folder + self.chicago95_theme_folder + self.chicago95_icons_folder + + self.folder_names = { + "root" : self.new_theme_folder, + "icons" : self.new_theme_folder + self.theme_name + "_Icons/", + "theme" : self.new_theme_folder + self.theme_name + "_Theme/", + "cursors" : self.new_theme_folder + self.theme_name + "_Cursors/", + "sounds" : self.new_theme_folder + self.theme_name + "_Sounds/", + "screensaver" : self.new_theme_folder + self.theme_name + "_Screensaver/", + "fonts" : self.new_theme_folder + self.theme_name + "_Fonts/" + } + + self.logger.info("Install folder names") + for i in self.folder_names: + self.logger.info("{:<21} | {}".format(i, self.folder_names[i])) + + def create_folders(self, delete=True): + for i in self.folder_names: + + if delete: + self.logger.debug("Deleting {} previous folder {}".format(i,self.folder_names[i])) + shutil.rmtree(self.folder_names[i], ignore_errors=True) + + self.logger.debug("Creating {} folder: {}".format(i,self.folder_names[i])) + if i == "icons": + shutil.copytree(self.chicago95_icons_folder,self.folder_names[i],symlinks=True,ignore_dangling_symlinks=True) + elif i == "cursors": + shutil.copytree(self.chicago95_cursor_folder,self.folder_names[i],symlinks=True,ignore_dangling_symlinks=True) + elif i == "theme": + shutil.copytree(self.chicago95_theme_folder,self.folder_names[i],symlinks=True,ignore_dangling_symlinks=True) + else: + os.mkdir(self.folder_names[i]) + + def create_icons(self, create_48_document_icon = True): + self.logger.info("Creating new icons in {}".format(self.folder_names['icons'])) + + svg_file_names = {} + png_file_names = { + "my_computer" : "user-home.png", + "my_documents" : "folder-documents.png", + "network_neighborhood" : "folder-remote.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 iconname in self.theme_config['icons']: + if not self.theme_config['icons'][iconname]: + self.logger.debug("{:<21} | Icon does not exist in this theme".format(iconname)) + continue + + icon_sizes = [16,22,24,32,48] + + filename = self.theme_config['icons'][iconname]['filename'] + index = self.theme_config['icons'][iconname]['index'] + path = self.theme_config['icons'][iconname]['path'] + filetype = self.theme_config['icons'][iconname]['type'] + + if not path: + self.logger.error("{:<21} | {} does not exist in this theme".format(iconname, filename)) + continue + + + if filetype not in ['dll', 'icl', 'ico', 'bmp']: + # wut + self.logger.error("File type {} not supported: {}".format(filetype, filename)) + continue + + self.logger.info("{:<21} | {}".format(iconname, filename)) + + if filetype in ['dll', 'icl']: + # Get the icons stored in the DLL + self.logger.debug("{:<21} | Icons are stored in ICL file {}".format("", filename, path)) + icon_files = self.extract_icons_from_dll(path) + + + else: + self.logger.debug("{:<21} | Icons are stored in ICO file {} {}".format("", filename,path)) + icon_files = self.extract_ico(path) + if icon_files == 'bmp': + filetype = 'bmp' + + if not icon_files: + self.logger.error("Not a valid icon file: {}".format(self.theme_config['icons'][iconname])) + continue + + # If the icons exist at various sizes, write them and convert them instead of scaling the largest one + for size in icon_sizes: + self.logger.debug("{:<21} | Searching for icon size: {} in {}".format("", size, filename)) + + if filetype in ['dll','icl']: + icon_filename, icon_file = self.get_icons_size_dll(icon_files, index, size) + elif filetype in 'ico': + icon_filename, icon_file = self.get_icons_size_ico(icon_files, size) + else: + icon_filename = False + if icon_filename: + icon_sizes.remove(size) + f = open(self.folder_names['icons']+icon_filename,"wb") + f.write(icon_file) + f.close() + sized_target = self.folder_names['icons']+"places/"+str(size)+"/"+png_file_names[iconname] + self.logger.debug("{:<21} | Creating: {} {} {}".format("", size, self.folder_names['icons']+icon_filename, sized_target)) + self.convert_ico_files(self.folder_names['icons']+icon_filename, sized_target) + + # Now that we're done, get the largest file and use that for the rest + if filetype in ['dll', 'icl']: + + icon_filename, icon_file = self.get_largest_icon_dll(icon_files, index) + elif filetype in 'ico': + icon_filename, icon_file = self.get_largest_icon_ico(icon_files, size) + + + if filetype in ['dll', 'icl', 'ico'] and not isinstance(icon_file, str): + f = open(self.folder_names['icons']+filename,"wb") + f.write(icon_file) + f.close() + else: + shutil.copyfile(path, self.folder_names['icons']+filename) + + + svg_icon_file = self.convert_icon(self.folder_names['icons'], self.folder_names['icons']+filename) + for size in icon_sizes: + if size <= 32 and iconname == "documents_ico" and not create_48_document_icon: + continue + sized_target = self.folder_names['icons']+"places/"+str(size)+"/"+png_file_names[iconname] + self.logger.debug("{:<21} | Creating: {} {} {}".format("", size, svg_icon_file, sized_target)) + self.convert_to_png_with_inkscape( svg_icon_file, size, sized_target) + + scaled_target = self.folder_names['icons']+"places/scalable/"+svg_file_names[iconname] + shutil.copy(svg_icon_file, scaled_target) + + self.logger.debug("Updating icon index.theme file") + icon_theme_config = configparser.RawConfigParser(interpolation=None) + icon_theme_config.optionxform = str + icon_theme_config.read(self.folder_names['icons']+"/index.theme") + icon_theme_config.set("Icon Theme","Name",self.index_theme_name) + with open(self.folder_names['icons']+"/index.theme", 'w') as configfile: + icon_theme_config.write(configfile, space_around_delimiters=False) + + def create_cursors(self): + self.logger.info("Creating new xcursors in {}".format(self.folder_names['cursors'])) + pointers = { + #windows theme # X11 Theme + "arrow" : "Arrow", + "help" : "Help", + "appstarting" : "AppStarting", + "wait" : "Wait", + "nwpen" : "Handwriting", + "no" : "NO", + "sizens" : "BaseN", + "sizewe" : "SizeWE", + "crosshair" : "Crosshair", + "ibeam" : "IBeam", + "sizenwse" : "AngleNW", + "sizenesw" : "AngleNE", + "sizeall" : "SizeAll", + "uparrow" : "UpArrow" + } + + + cursor_src_folder = self.folder_names['cursors'] + "src/" + self.logger.debug("Cursor source folder: {}".format(cursor_src_folder)) + tabs = 21 + for i in self.theme_config['cursors']: + if self.theme_config['cursors'][i] and tabs < len(self.theme_config['cursors'][i]['filename']): + tabs = len(self.theme_config['cursors'][i]['filename']) + + + for current_cursor in pointers: + self.logger.debug("Current cursor: {} ({cur}.conf/{cur}.png)".format(current_cursor, cur=pointers[current_cursor])) + + if (current_cursor not in self.theme_config['cursors'] or not + self.theme_config['cursors'][current_cursor] or not + self.theme_config['cursors'][current_cursor]['path']): + continue + + theme_cursor_config = self.theme_config['cursors'][current_cursor] + + self.logger.info("{:<21} | file: {}".format(current_cursor, theme_cursor_config['filename'])) + + x11_cursor_file_name = cursor_src_folder+pointers[current_cursor]+".png" # Target Folder for converted cursors + os.remove(x11_cursor_file_name) + + self.logger.debug("{:<21} | {} --> {}".format("", theme_cursor_config['filename'],os.path.split(x11_cursor_file_name)[1])) + + if theme_cursor_config['type'] == 'ani': + self.logger.debug("{:<21} | Cursor {} is type ani".format(current_cursor, theme_cursor_config['filename'])) + ani_file_config = self.extract_ani(theme_cursor_config['path']) + #pprint(ani_file_config) + self.logger.debug("{:<21} | Header - nFrames: {}, nSteps: {}, iDispRate: {}".format(current_cursor, ani_file_config['anih']['nFrames'], ani_file_config['anih']['nSteps'], ani_file_config['anih']['iDispRate'])) + write_conf = open(cursor_src_folder+pointers[current_cursor]+".conf", 'w') + if ani_file_config['INFO']: + + ani_readme = open(cursor_src_folder+pointers[current_cursor]+".info", 'w') + if 'INAM' in ani_file_config['INFO']: + ani_readme.write("Title: {}\n".format(ani_file_config['INFO']['INAM'].replace('\x00', ''))) + self.logger.debug("{:<21} | Artist name (INAM): {}".format(current_cursor, ani_file_config['INFO']['INAM'].replace('\x00', ''))) + if 'IART' in ani_file_config['INFO']: + ani_readme.write("Artist: {}\n".format(ani_file_config['INFO']['IART'].replace('\x00', ''))) + self.logger.debug("{:<21} | Artist details (IART): {}".format(current_cursor, ani_file_config['INFO']['IART'].replace('\x00', ''))) + ani_readme.write("Theme: {}".format(self.theme_name)) + ani_readme.close() + if ani_file_config['seq']: + for sequence in ani_file_config['seq']: + if ani_file_config['rate']: + rate = ani_file_config['rate'][sequence] * 17 + else: + rate = ani_file_config['anih']['iDispRate'] * 17 + + for icon in ani_file_config['icon']: + if icon['index'] == sequence: + xhot = icon['rtIconDirEntry']['wPlanes'] + yhot = icon['rtIconDirEntry']['wBitCount'] + size = icon['rtIconDirEntry']['bHeight'] + self.logger.debug("{:<21} | Sequence: {}, rate: {}, size: {}, xhot: {}, yhot: {}".format(current_cursor, sequence, rate, size,xhot, yhot)) + cur_filename = pointers[current_cursor]+"_"+str(sequence) + f = open(cursor_src_folder+cur_filename+".cur","wb") + f.write(icon['ico_file']) + f.close() + self.convert_cur_files(cursor_src_folder+cur_filename+".cur", cursor_src_folder+cur_filename+".png") + write_conf.write("{size} {xhot} {yhot} {filename} {rate}\n".format(size=size, xhot=xhot, yhot=yhot, filename=cur_filename+".png", rate=rate )) + else: + for icon in ani_file_config['icon']: + xhot = icon['rtIconDirEntry']['wPlanes'] + yhot = icon['rtIconDirEntry']['wBitCount'] + size = icon['rtIconDirEntry']['bHeight'] + rate = ani_file_config['anih']['iDispRate'] * 17 + self.logger.debug("{:<21} | Sequence: {}, rate: {}, size: {}, xhot: {}, yhot: {}".format(current_cursor, icon['index'], rate, size,xhot, yhot)) + cur_filename = pointers[current_cursor]+"_"+str(icon['index']) + f = open(cursor_src_folder+cur_filename+".cur","wb") + f.write(icon['ico_file']) + f.close() + self.convert_cur_files(cursor_src_folder+cur_filename+".cur", cursor_src_folder+cur_filename+".png") + write_conf.write("{size} {xhot} {yhot} {filename} {rate}\n".format(size=size, xhot=xhot, yhot=yhot, filename=cur_filename+".png", rate=rate)) + + for icon in ani_file_config['icon']: + xhot = icon['rtIconDirEntry']['wPlanes'] + yhot = icon['rtIconDirEntry']['wBitCount'] + size = icon['rtIconDirEntry']['bHeight'] + #print(xhot, yhot, size) + write_conf.close() + + elif theme_cursor_config['type'] in ['cur', 'ico']: + self.logger.debug("{:<21} | Cursor {} is type cur".format(current_cursor, theme_cursor_config['filename'])) + cursor_file_config = self.extract_cur(theme_cursor_config['path']) + xhot = cursor_file_config['icon'][0]['rtIconDirEntry']['wPlanes'] + yhot = cursor_file_config['icon'][0]['rtIconDirEntry']['wBitCount'] + size = cursor_file_config['icon'][0]['rtIconDirEntry']['bHeight'] + icon_file = cursor_file_config['icon'][0]['ico_file'] + f = open(cursor_src_folder+pointers[current_cursor]+".cur","wb") + f.write(icon_file) + f.close() + self.convert_cur_files(cursor_src_folder+pointers[current_cursor]+".cur", cursor_src_folder+pointers[current_cursor]+".png") + write_conf = open(cursor_src_folder+pointers[current_cursor]+".conf", 'w') + self.logger.debug("{:<21} | Writting conf file {}: {size} {xhot} {yhot} {filename}".format(current_cursor, pointers[current_cursor]+".conf", size=size, xhot=xhot, yhot=yhot, filename=pointers[current_cursor]+".png")) + write_conf.write("{size} {xhot} {yhot} {filename}".format(size=size, xhot=xhot, yhot=yhot, filename=pointers[current_cursor]+".png")) + write_conf.close() + + + # Cursors are all done now we need to generate X11 cursors with xcursorgen + self.build_cursors(self.folder_names['cursors']) + cur_theme_config = configparser.RawConfigParser(interpolation=None) + cur_theme_config.optionxform = str + cur_theme_config.read(self.folder_names['cursors']+"index.theme") + cur_theme_config.set("Icon Theme","Name",self.index_theme_name) + with open(self.folder_names['cursors']+"index.theme", 'w') as configfile: + cur_theme_config.write(configfile, space_around_delimiters=False) + + def convert_colors(self): + windows_to_gtk = { + 'activeborder' : False, + 'activetitle' : 'window_title_bg_color', + 'appworkspace' : False, + 'background' : False, + 'buttondkshadow' : 'border_dark', + 'buttonface' : ['bg_color', 'border_color', 'button_bg_color'], + 'buttonlight' : 'border_light', + 'buttonhilight' : 'border_bright', + 'buttonshadow' : 'border_shade', + 'buttontext' : 'button_text_color', + 'graytext' : 'selected_bg_color', + 'hilight' : 'selected_bg_color', + 'hilighttext' : 'selected_fg_color', + 'inactiveborder' : False, + 'inactivetitle' : 'inactive_title_bg_color', + 'inactivetitletext' : 'inactive_title_text_color', + 'infotext' : 'tooltip_fg_color', + 'infowindow' : 'tooltip_bg_color', + 'menu' : 'menu_bg_color', + 'menutext' : 'menu_text_color', + 'scrollbar' : 'scrollbar_trough_bg_color', + 'titletext' : 'window_title_text_color', + 'window' : 'bg_bright', + 'windowframe' : False, + 'windowtext' : ['fg_color', 'text_color'], + } + + + + for color_name in self.theme_config['colors']: + + new_color = self.theme_config['colors'][color_name]['color'] + self.logger.info("{:<21} | New color: {}".format(color_name, new_color)) + for gtk_css in [ self.folder_names['theme']+"gtk-3.24/gtk.css", self.folder_names['theme']+"gtk-3.0/gtk.css"]: + self.logger.debug("{:<21} | {} in {}".format(color_name, new_color, gtk_css)) + shutil.move( gtk_css, gtk_css+"~" ) + fileh = open(gtk_css+"~","r") + nfileh = open(gtk_css,"w") + for line in fileh: + found = False + if color_name in windows_to_gtk and windows_to_gtk[color_name]: + if isinstance(windows_to_gtk[color_name], str): + if " " + windows_to_gtk[color_name] + " " in line: + self.logger.debug("{:<21} | FOUND! {} in {}".format("", windows_to_gtk[color_name],line.strip())) + start = line.find(windows_to_gtk[color_name]) + len(windows_to_gtk[color_name]) + 1 + end = line.find(";", start) + line = line.replace(line[start:end],new_color) + self.logger.debug("{:<21} | Writting: {}".format("", line.strip())) + else: + for name in windows_to_gtk[color_name]: + if " "+name in line: + self.logger.debug("{:<21} | FOUND! {} in {}".format("", name, line.strip())) + start = line.find(name) + len(name) + 1 + end = line.find(";", start) + line = line.replace(line[start:end],new_color) + self.logger.debug("{:<21} | Writting: {}".format("",line.strip())) + + nfileh.write(line) + fileh.close() + nfileh.close() + + #### THESE ARE LOCATED IN xfwm4/themerc + # #activetitle + #active_color_1=#000080 + #active_color_2=#000080 + + #inactivetitle + #inactive_color_1=#808080 + #inactive_color_2=#808080 + + #activetext + #active_text_color=#ffffff + + #inactivetext + #inactive_text_color=#C0C0C0 + + #buttonface + #active_mid_1=#C0C0C0 + #inactive_mid_1=#C0C0C0 + + #buttonLight + #active_hilight_1=#DFDFDF + #inactive_hilight_1=#DFDFDF + + #buttonhilight + #active_hilight_2=#ffffff + #inactive_hilight_2=#ffffff + + #activeborder + #active_border_color=#C0C0C0 + #inactive_border_color=#C0C0C0 + + #buttonShadow + #active_shadow_2=#808080 + #inactive_shadow_2=#808080 + + #buttonDKShadow + #active_shadow_1=#000000 + #inactive_shadow_1=#000000 + + #buttontext + #active_mid_2=#000000 + #inactive_mid_2=#000000 + themerc_colors = { + 'activetitle' : ['active_color_1','active_color_2'], + 'inactivetitle' : ['inactive_color_1','inactive_color_2'], + 'titletext' : ['active_text_color'], + 'inactivetitletext' : ['inactive_text_color'], + 'buttonface' : ['active_mid_1','inactive_mid_1'], + 'buttonlight' : ['active_hilight_1','inactive_hilight_1'], + 'buttonhilight' : ['active_hilight_2','inactive_hilight_2'], + 'activeborder' : ['active_border_color'], + 'inactiveborder' : ['inactive_border_color'], + 'buttonshadow' : ['active_shadow_2','inactive_shadow_2'], + 'buttondkshadow' : ['active_shadow_1','inactive_shadow_1'], + 'buttontext' : ['active_mid_2','inactive_mid_2'] + } + + if color_name in themerc_colors: + + self.logger.debug("{:<21} | {}".format(color_name, themerc_colors[color_name])) + + for themerc in [ self.folder_names['theme']+'xfwm4/themerc', self.folder_names['theme']+'xfwm4_hidpi/themerc' ]: + + shutil.move( themerc, themerc+"~" ) + fileh = open(themerc+"~","r") + nfileh = open(themerc,"w") + for line in fileh: + if line[0:line.find("=")] in themerc_colors[color_name]: + self.logger.debug("{:<21} | Editing themerc color {} changing to {}".format('', color_name, new_color)) + line = line.replace(line[line.find("=")+1:],new_color+"\n") + nfileh.write(line) + fileh.close() + nfileh.close() + + + self.create_windows_controls(self.folder_names['theme'], + self.theme_config['colors']['buttondkshadow']['color'], + self.theme_config['colors']['buttonlight']['color'], + self.theme_config['colors']['buttonshadow']['color'], + self.theme_config['colors']['buttonhilight']['color'], + self.theme_config['colors']['buttonface']['color'], + self.theme_config['colors']['buttontext']['color'] ) + + self.change_asset_colors(self.folder_names['theme'], + self.theme_config['colors']['buttondkshadow']['color'], + self.theme_config['colors']['buttonlight']['color'], + self.theme_config['colors']['buttonshadow']['color'], + self.theme_config['colors']['buttonhilight']['color'], + self.theme_config['colors']['buttonface']['color'], + self.theme_config['colors']['buttontext']['color'] ) + + + color_theme_config = configparser.RawConfigParser(interpolation=None) + color_theme_config.optionxform = str + color_theme_config.read(self.folder_names['theme']+"index.theme") + color_theme_config.set("Desktop Entry","Name",self.index_theme_name) + color_theme_config.set("X-GNOME-Metatheme","GtkTheme",self.index_theme_name) + color_theme_config.set("X-GNOME-Metatheme","MetacityTheme",self.index_theme_name) + color_theme_config.set("X-GNOME-Metatheme","IconTheme",self.index_theme_name) + color_theme_config.set("X-GNOME-Metatheme","CursorTheme",self.index_theme_name) + with open(self.folder_names['theme']+"index.theme", 'w') as configfile: + color_theme_config.write(configfile, space_around_delimiters=False) + + def find_all_wallpapers(self): + # Finds all wallpapers included with the theme + self.extra_wallpapers = [] + found = False + self.logger.debug("Checking for any other wallpapers in theme folder") + wallpaper_files = ('.bmp', '.gif', '.jpg', '.png', '.tif') + for ext in wallpaper_files: + for files in [self.theme_files[i] for i in self.theme_files if ext in i]: + self.logger.debug("Extra wallpaper found: {} ".format(files)) + self.extra_wallpapers.append(files) + found = True + if not found: + self.extra_wallpapers = False + + + def generate_wallpaper(self): + + if not self.theme_config['wallpaper']['theme_wallpaper'] and not self.theme_config['wallpaper']['extra_wallpapers']: + self.logger.info("No wallpapers included with this theme") + return + + + if (self.theme_config['wallpaper']['theme_wallpaper']['wallpaper'] + and self.theme_config['wallpaper']['theme_wallpaper']['path']): + wallpaper = self.theme_config['wallpaper']['theme_wallpaper']['wallpaper'] + path = self.theme_config['wallpaper']['theme_wallpaper']['path'] + tilewallpaper = self.theme_config['wallpaper']['theme_wallpaper']['tilewallpaper'] + wallpaperstyle = self.theme_config['wallpaper']['theme_wallpaper']['wallpaperstyle'] + filename = self.theme_config['wallpaper']['theme_wallpaper']['new_filename'] + self.logger.info("Wallpaper:Wallpaper:Copying {} to {}".format(wallpaper,filename)) + self.logger.debug("Wallpaper: Copying {} to {}".format(path,self.folder_names['root']+filename)) + self.logger.debug("Wallpaper: {} path: {} tile: {} style: {} new_filename: {} root folder:{}".format(wallpaper, path, tilewallpaper, wallpaperstyle,filename, self.folder_names['root'] )) + shutil.copy(path,self.folder_names['root']+filename) + + if self.theme_config['wallpaper']['extra_wallpapers']: + for files in self.theme_config['wallpaper']['extra_wallpapers']: + self.logger.info("Extra Wallpaper: Copying {} to {}".format(os.path.split(files)[-1],self.folder_names['root'])) + self.logger.debug("Extra Wallpaper: Copying {} to {}".format(files,self.folder_names['root'])) + shutil.copy(files,self.folder_names['root']) + + + #shutil.copy(files,self.folder_names['root']) + def generate_screensaver(self): + self.logger.info("Generating screensaver") + if self.theme_config['screensaver']: + self.logger.info("Copying {0} to {1}".format(self.theme_config['screensaver'], self.folder_names['screensaver'])) + shutil.copy(self.theme_config['screensaver'],self.folder_names['screensaver']) + theme_screensaver = self.folder_names['screensaver'] + os.path.split(self.theme_config['screensaver'])[1] + f = open(theme_screensaver[:-3]+"sh","w") + f.write(SCREEN_SAVER_SCRIPT.format(scr_file=theme_screensaver)) + f.close() + + + def generate_fonts(self): + self.logger.info("Copying fonts") + for font in self.theme_config['fonts']: + family = self.theme_config['fonts'][font]['family'] + path = self.theme_config['fonts'][font]['path'] + self.logger.info("Copying font {}: {} to {}".format(family, path, self.folder_names['fonts']+family+os.path.splitext(path)[1])) + shutil.copy(path,self.folder_names['fonts']+family+os.path.splitext(path)[1]) + + def generate_sounds(self): + + # Sound themes are like Icon theme. To disable a sound the .disable + + xdg_sounds_to_theme = { + 'alarm-clock-elapsed' : 'SystemAsterisk', + 'audio-channel-front-center' : 'None', + 'audio-channel-front-left' : 'None', + 'audio-channel-front-right' : 'None', + 'audio-channel-left' : 'None', + 'audio-channel-lfe' : 'None', + 'audio-channel-rear-center' : 'None', + 'audio-channel-rear-left' : 'None', + 'audio-channel-rear-right' : 'None', + 'audio-channel-right' : 'None', + 'audio-channel-side-left' : 'None', + 'audio-channel-side-right' : 'None', + 'audio-test-signal' : 'SystemStart', + 'audio-volume-change' : 'Close', + 'battery-caution' : 'None', + 'battery-full' : 'None', + 'battery-low' : 'SystemExclamation', + 'bell-terminal' : 'SystemExclamation', + 'bell-window-system' : 'SystemExclamation', + 'button-pressed' : 'MenuCommand', + 'button-released' : 'None', + 'button-toggle-off' : 'MenuCommand', + 'button-toggle-on' : 'MenuCommand', + 'camera-focus' : 'None', + 'camera-shutter' : 'None', + 'complete-copy' : 'None', + 'complete-download' : 'None', + 'complete-media-burn' : 'None', + 'complete-media-burn-test' : 'None', + 'complete-media-format' : 'None', + 'complete-media-rip' : 'None', + 'complete-scan' : 'None', + 'completion-fail' : 'None', + 'completion-partial' : 'None', + 'completion-rotation' : 'None', + 'completion-sucess' : 'None', + 'count-down' : 'None', + 'desktop-login' : 'SystemStart', + 'desktop-logout' : 'SystemExit', + 'desktop-screen-lock' : 'None', + 'desktop-switch-left' : 'RestoreUp', + 'desktop-switch-right' : 'RestoreDown', + 'device-added' : 'None', + 'device-added-audio' : 'None', + 'device-added-media' : 'None', + 'device-removed' : 'None', + 'device-removed-audio' : 'None', + 'device-removed-media' : 'None', + 'dialog-cancel' : 'None', + 'dialog-error' : 'SystemExclamation', + 'dialog-information' : 'SystemAsterisk', + 'dialog-ok' : 'None', + 'dialog-question' : 'SystemQuestion', + 'dialog-warning' : 'SystemHand', + 'drag-accept' : 'None', + 'drag-fail' : 'None', + 'drag-start' : 'None', + 'expander-toggle-off' : 'RestoreDown', + 'expander-toggle-on' : 'RestoreUp', + 'file-trash' : 'EmptyRecycleBin', + 'item-deleted' : 'EmptyRecycleBin', + 'item-selected' : 'MenuCommand', + 'lid-close' : 'None', + 'lid-open' : 'None', + 'link-pressed' : 'None', + 'link-released' : 'None', + 'menu-click' : 'MenuCommand', + 'menu-popdown' : 'MenuPopup', + 'menu-popup' : 'MenuPopup', + 'menu-replace' : 'MenuPopup', + 'message-new-email' : 'None', + 'message-new-instant' : 'None', + 'message-sent-email' : 'None', + 'message-sent-instant' : 'None', + 'network-connectivity-error' : 'SystemExclamation', + 'network-connectivity-established' : 'None', + 'network-connectivity-lost' : 'SystemAsterisk', + 'notebook-tab-changed' : 'None', + 'phone-failure' : 'None', + 'phone-hangup' : 'None', + 'phone-incoming-call' : 'None', + 'phone-outgoing-busy' : 'None', + 'phone-outgoing-calling' : 'None', + 'power-plug' : 'None', + 'power-unplug' : 'None', + 'power-unplug-battery-low' : 'SystemExclamation', + 'screen-capture' : 'Open', + 'scroll-down' : 'None', + 'scroll-down-end' : 'None', + 'scroll-left' : 'None', + 'scroll-left-end' : 'None', + 'scroll-right' : 'None', + 'scroll-right-end' : 'None', + 'scroll-up' : 'None', + 'scroll-up-end' : 'None', + 'search-results' : 'None', + 'search-results-empty' : 'None', + 'service-login' : 'None', + 'service-logout' : 'None', + 'software-update-available' : 'SystemQuestion', + 'software-update-urgent' : 'AppGPFault', + 'suspend-error' : 'SystemExclamation', + 'suspend-resume' : 'None', + 'suspend-start' : 'None', + 'system-bootup' : 'None', + 'system-ready' : 'None', + 'system-shutdown' : 'None', + 'theme-demo' : 'SystemStart', + 'tooltip-popdown' : 'None', + 'tooltip-popup' : 'None', + 'trash-empty' : 'EmptyRecycleBin', + 'window-attention-active' : 'AppGPFault', + 'window-attention-inactive' : 'AppGPFault', + 'window-close' : 'Close', + 'window-inactive-click' : 'SystemAsterisk', + 'window-maximized' : 'Maximize', + 'window-minimized' : 'Minimize', + 'window-move-end' : 'None', + 'window-move-start' : 'None', + 'window-new' : 'Open', + 'window-resize-end' : 'None', + 'window-resize-start' : 'None', + 'window-slide-in' : 'None', + 'window-slide-out' : 'None', + 'window-switch' : 'None', + 'window-unmaximized' : 'Minimize', + 'window-unminimized' : 'Maximize' + } + + os.mkdir(self.folder_names['sounds'] + "stereo/") + os.mkdir(self.folder_names['sounds'] + "theme_source/") + + for name in xdg_sounds_to_theme: + if xdg_sounds_to_theme[name] in self.theme_config['sounds']: + path = self.theme_config['sounds'][xdg_sounds_to_theme[name]] + theme_file = self.folder_names['sounds'] + "stereo/" + name + ".wav" #it's gotta be wav + self.logger.debug("Copying theme sound {}: {} to {}".format(xdg_sounds_to_theme[name], path, theme_file)) + if path: + shutil.copy(path, theme_file) + else: + theme_file = self.folder_names['sounds'] + "stereo/" + name + ".disabled" + #self.logger.debug("Copying theme sound {}: {} to {}".format(xdg_sounds_to_theme[name], path, theme_file)) + with open(theme_file, 'w') as fp: + pass + + for sound in self.theme_config['sounds']: + path = self.theme_config['sounds'][sound] + if path: + self.logger.info("Copying sound {}: {} to {}".format(sound, path, self.folder_names['sounds']+"theme_source/"+sound+'_'+os.path.split(path)[1])) + shutil.copy(path,self.folder_names['sounds']+"theme_source/"+sound+'_'+os.path.split(path)[1]) + + config = configparser.ConfigParser() + config.optionxform=str + config.add_section('Sound Theme') + config.set('Sound Theme', 'Name', self.theme_name) + config.set('Sound Theme', 'Directories', 'stereo') + config.add_section('stereo') + config.set('stereo', 'OutputProfile', 'stereo') + + self.logger.debug("Writting sound config file {}".format(self.folder_names['sounds']+"index.theme")) + with open(self.folder_names['sounds']+"index.theme", 'w') as configfile: + config.write(configfile, space_around_delimiters=False) + + + +#### Color Functions + def hexToRGB(self, h): + return tuple(int(h.lstrip('#')[i:i+2], 16) for i in (0, 2, 4)) + + def rgbaToRGB(self, tup): + return (tup[0],tup[1],tup[2]) + + def create_windows_controls(self, path="./", ButtonDKShadow="#000000", ButtonLight="#dfdfdf", ButtonShadow="#808080", ButtonHilight="#ffffff", ButtonFace="#c0c0c0", ButtonText="#FFFF00" ): + + convert_path = subprocess.check_output(["which", "convert"]).strip() + mogrify_path = subprocess.check_output(["which", "mogrify"]).strip() + # convert -size 18x18 xc:none + # -fill "#000000" -draw "rectangle 1,1 16,14" + # -fill "#ffffff" -draw "rectangle 1,1 15,13" + # -fill "#808080" -draw "rectangle 2,2 15,13" + # -fill "#dfdfdf" -draw "rectangle 2,2 14,12" + # -fill "#c0c0c0" -draw "rectangle 3,3 14,12" + # /home/phil/Chicago95/Theme/Chicago95/gtk-3.0/buttons/icon-restore.png -geometry +4+2 -composite + # test1.png + + size="18x18" + + self.logger.debug("{:<21} | Colors: ButtonDKShadow={}, ButtonLight={}, ButtonShadow={}, ButtonHilight={}, ButtonFace={}, ButtonText={} ".format("Colors",ButtonDKShadow, ButtonLight, ButtonShadow, ButtonHilight, ButtonFace, ButtonText)) + + for i in ['gtk-3.0/','gtk-3.24/']: + + folder = path + i + "buttons/" + + icons = { + 'close' : folder+"icon-close.png", + 'maximize' : folder+"icon-maximise.png", + 'minimize' : folder+"icon-minimise.png", + 'restore' : folder+"icon-restore.png" + } + + + # Windows control / title icons + for icon in icons: + + filename = icon+"_normal.png" + geometry="+3+2" + + if icon == 'close': + geometry="+4+2" + + + self.logger.debug("{:<21} | {}".format(icon, folder+filename)) + args = [ + convert_path, + "-size", size, + "xc:none", + "-fill", ButtonDKShadow, + "-draw", " rectangle 1,1 16,14", + "-fill", ButtonHilight, + "-draw", " rectangle 1,1 15,13", + "-fill", ButtonShadow, + "-draw", " rectangle 2,2 15,13", + "-fill", ButtonLight, + "-draw", "rectangle 2,2 14,12", + "-fill", ButtonFace, + "-draw", "rectangle 3,3 14,12", + "(", icons[icon], '-fill', ButtonText, '-opaque', '#000000', ")", + '-geometry', geometry, + '-composite', + folder+filename + ] + subprocess.check_call(args) + + filename = icon+"_pressed.png" + + geometry="+4+3" + if icon == 'close': + geometry="+5+3" + + self.logger.debug("{:<21} | {}".format(icon, folder+filename)) + args = [ + convert_path, + "-size", size, + "xc:none", + "-fill", ButtonHilight, + "-draw", " rectangle 1,1 16,14", + "-fill", ButtonDKShadow, + "-draw", " rectangle 1,1 15,13", + "-fill", ButtonLight, + "-draw", " rectangle 2,2 15,13", + "-fill", ButtonShadow, + "-draw", "rectangle 2,2 14,12", + "-fill", ButtonFace, + "-draw", "rectangle 3,3 14,12", + "(", icons[icon], '-fill', ButtonText, '-opaque', '#000000', ")", + '-geometry', geometry, + '-composite', + folder+filename + ] + subprocess.check_call(args) + + self.logger.debug("{:<21} | {}".format(icon, icons[icon])) + args = [ + mogrify_path, + '-fill', ButtonText, '-opaque', '#000000', + icons[icon] + ] + subprocess.check_call(args) + + + self.logger.debug("{:<21} | {}".format("dialog_button_normal", folder+"dialog_button_normal.png")) + args = [ + convert_path, + "-size", "16x16", + "xc:none", + "-fill", ButtonDKShadow, + "-draw", " rectangle 0,0 15,15", + "-fill", ButtonHilight, + "-draw", " rectangle 0,0 14,14", + "-fill", ButtonShadow, + "-draw", " rectangle 1,1 14,14", + "-fill", ButtonLight, + "-draw", "rectangle 1,1 13,13", + "-fill", ButtonFace, + "-draw", "rectangle 2,2 13,13", + folder+"dialog_button_normal.png", + ] + subprocess.check_call(args) + + self.logger.debug("{:<21} | {}".format("button_pressed", folder+"button_pressed.png")) + args = [ + convert_path, + "-size", "16x16", + "xc:none", + "-fill", ButtonHilight, + "-draw", " rectangle 0,0 15,15", + "-fill", ButtonDKShadow, + "-draw", " rectangle 0,0 14,14", + "-fill", ButtonShadow, + "-draw", " rectangle 1,1 14,14", + "-fill", ButtonFace, + "-draw", "rectangle 2,2 14,14", + folder+"button_pressed.png", + ] + subprocess.check_call(args) + + self.logger.debug("{:<21} | {}".format("combobox_button_normal", folder+"combobox_button_normal.png")) + args = [ + convert_path, + "-size", "16x16", + "xc:none", + "-fill", ButtonDKShadow, + "-draw", " rectangle 0,0 15,15", + "-fill", ButtonFace, #ButtonHilight, + "-draw", " rectangle 0,0 14,14", + "-fill", ButtonShadow, + "-draw", " rectangle 1,1 14,14", + "-fill", ButtonHilight, + "-draw", "rectangle 1,1 13,13", + "-fill", ButtonFace, + "-draw", "rectangle 2,2 13,13", + folder+"combobox_button_normal.png", + ] + subprocess.check_call(args) + + self.logger.debug("{:<21} | {}".format("decoration_border", folder+"decoration_border.png")) + args = [ + convert_path, + "-size", "16x16", + "xc:none", + "-fill", ButtonDKShadow, + "-draw", " rectangle 0,0 15,15", + "-fill", ButtonLight, + "-draw", " rectangle 0,0 14,14", + "-fill", ButtonShadow, + "-draw", " rectangle 1,1 14,14", + "-fill", ButtonHilight, + "-draw", "rectangle 1,1 13,13", + "-fill", ButtonFace, + "-draw", "rectangle 2,2 13,13", + folder+"decoration_border.png", + ] + subprocess.check_call(args) + + self.logger.debug("{:<21} | {}".format("dialog_button_active", folder+"dialog_button_active.png")) + args = [ + convert_path, + "-size", "16x16", + "xc:none", + "-fill", ButtonDKShadow, + "-draw", " rectangle 0,0 15,15", + "-fill", ButtonShadow, + "-draw", " rectangle 1,1 14,14", + "-fill", ButtonFace, + "-draw", "rectangle 2,2 13,13", + folder+"dialog_button_active.png", + ] + subprocess.check_call(args) + + self.logger.debug("{:<21} | {}".format("dialog_button_focus", folder+"dialog_button_focus.png")) + args = [ + convert_path, + "-size", "16x16", + "xc:none", + "-fill", ButtonDKShadow, + "-draw", " rectangle 0,0 15,15", + "-fill", ButtonHilight, + "-draw", " rectangle 1,1 13,13", + "-fill", ButtonShadow, + "-draw", " rectangle 2,2 13,13", + "-fill", ButtonLight, + "-draw", "rectangle 2,2 12,13", + "-fill", ButtonFace, + "-draw", "rectangle 3,3 12,13", + folder+"dialog_button_focus.png", + ] + subprocess.check_call(args) + + self.logger.debug("{:<21} | {}".format("toggle_pressed", folder+"toggle_pressed.png")) + args = [ + convert_path, + "-size", "16x16", + "xc:none", + "-fill", ButtonHilight, + "-draw", " rectangle 0,0 15,15", + "-fill", ButtonDKShadow, + "-draw", " rectangle 0,0 14,14", + "-fill", ButtonLight, + "-draw", " rectangle 1,1 14,14", + "-fill", ButtonShadow, + "-draw", "rectangle 1,1 13,13", + "-fill", ButtonHilight, + "-draw", "rectangle 2,2 13,12", + "-fill", ButtonFace, + "-draw", "rectangle 2,3 13,13", + folder+"toggle_pressed.png", + ] + subprocess.check_call(args) + + self.logger.debug("{:<21} | {}".format("decoration_border", folder+"decoration_border.png")) + args = [ + convert_path, + "-size", "16x16", + "xc:none", + "-fill", ButtonDKShadow, + "-draw", " rectangle 0,0 15,15", + "-fill", ButtonHilight, + "-draw", " rectangle 0,0 14,14", + "-fill", ButtonShadow, + "-draw", " rectangle 1,1 14,14", + "-fill", ButtonHilight, + "-draw", "rectangle 1,1 13,13", + "-fill", ButtonFace, + "-draw", "rectangle 2,2 13,13", + folder+"decoration_border.png", + ] + subprocess.check_call(args) + + def change_asset_colors(self, path="./", ButtonDKShadow="#000000", ButtonLight="#dfdfdf", ButtonShadow="#808080", ButtonHilight="#ffffff", ButtonFace="#c0c0c0", ButtonText="#000000" ): + + mogrify_path = subprocess.check_output(["which", "mogrify"]).strip() + + self.logger.debug("{:<21} | Colors: ButtonDKShadow={}, ButtonLight={}, ButtonShadow={}, ButtonHilight={}, ButtonFace={}, ButtonText={} ".format("Colors",ButtonDKShadow, ButtonLight, ButtonShadow, ButtonHilight, ButtonFace, ButtonText)) + + originals = { + 'ButtonDKShadow' :"#000000", + 'ButtonLight' : "#dfdfdf", + 'ButtonShadow': "#808080", + 'ButtonHilight' : "#ffffff", + 'ButtonFace' : "#c0c0c0", + 'ButtonText' : "#FFFF00" + } + + new = { + 'ButtonDKShadow' : ButtonDKShadow, + 'ButtonLight' : ButtonLight, + 'ButtonShadow': ButtonShadow, + 'ButtonHilight' : ButtonHilight, + 'ButtonFace' : ButtonFace, + 'ButtonText' : ButtonText + } + + + for i in ['gtk-3.0/assets/','gtk-3.24/assets/','gtk-3.0/scrollbar/','gtk-3.24/scrollbar/']: + + folder = path + i + for asset in os.listdir(folder): + if not asset.startswith("status") and not asset.startswith("branding"): + self.logger.debug("mogrifying {} ({} {} {} {} {} {})".format(asset, ButtonDKShadow, ButtonLight, ButtonShadow, ButtonHilight, ButtonFace, ButtonText)) + + args = [ + mogrify_path, + '-fill', ButtonDKShadow, '-opaque', originals['ButtonDKShadow'], + '-fill', ButtonLight, '-opaque', originals['ButtonLight'], + '-fill', ButtonShadow, '-opaque', originals['ButtonShadow'], + '-fill', ButtonHilight, '-opaque', originals['ButtonHilight'], + '-fill', ButtonFace, '-opaque', originals['ButtonFace'], + '-fill', ButtonText, '-opaque', originals['ButtonText'], + '-quiet', + folder+asset + ] + subprocess.check_call(args) + + +#### Icon/Cursor Functions + + def get_icons_size_ico(self, icons_list, size=32): + self.logger.debug("{:<21} | Getting highest quality icon of size {}".format(" ",size)) + + size_exists = False + highest_color = 0 + + for icon_data in icons_list: # First we make sure the ID exists and get the highest rating + + if int(icon_data['Width']) == int(size): + size_exists = True + + if icon_data['Colors'] >= highest_color: + highest_color = icon_data['Colors'] + + + #Return that Icon file + if size_exists: + for i in icons_list: + if "_{}x{}x{}.ico".format(size, size,highest_color) in i['filename']: + self.logger.debug("{:<21} | Found: {}".format("", i['filename'])) + return i['filename'], i['ICON'] + + else: + self.logger.debug("{:<21} | Could not find Icon of size {}".format(" ", size)) + return (False,False) + + self.logger.debug("{:<21} | Could not find Icon of size {}".format(" ", size)) + return (False,False) + + + + def get_icons_size_dll(self, icons_list, index, size=32): + self.logger.debug("{:<21} | Getting highest quality icon of size {} at index {}".format(" ", size, index)) + + id_exists = False + size_exists = False + highest_color = 0 + + for icon_data in icons_list: # First we make sure the ID exists and get the highest rating + + if int(icon_data['ID']) == int(index) and int(icon_data['Width']) == int(size): + id_exists = True + size_exists = True + + if icon_data['Colors'] >= highest_color: highest_color = icon_data['Colors'] + + + #Return that Icon file + if id_exists and size_exists: + for i in icons_list: + if "{}_{}x{}x{}.ico".format(index, size, size,highest_color) in i['filename']: + self.logger.debug("{:<21} | CFound: {}".format("",i['filename'])) + return i['filename'], i['ICON'] + else: + self.logger.debug("{:<21} | Could not find Icon with index {}".format(" ",index)) + return (False,False) + + + + def get_largest_icon_dll(self, icons_list, index): + self.logger.debug("{:<21} | Getting highest quality icon at index {}".format(" ",index)) + + highest_color = 0 + highest_size = 0 + id_exists = False + + for icon_data in icons_list: # Find the highest quality icon + if int(icon_data['ID']) == int(index): + id_exists = True + if icon_data['Colors'] >= highest_color: highest_color = icon_data['Colors'] + if icon_data['Width'] >= highest_size: highest_size = icon_data['Width'] + + #Return that Icon file + if id_exists: + for i in icons_list: + if "{}_{}x{}x{}.ico".format(index, highest_size, highest_size,highest_color) in i['filename']: + self.logger.debug("{:21} | Found: {}".format("", i['filename'])) + return i['filename'], i['ICON'] + else: + self.logger.error("{:21} | Could not find Icon with index {}".format("",index)) + return ('','') + + def get_largest_icon_ico(self, icons_list, index): + self.logger.debug("{:<21} | Getting highest quality icon".format(" ")) + + highest_color = 0 + highest_size = 0 + size_exists = False + for icon_data in icons_list: # Find the highest quality icon + if icon_data['Colors'] >= highest_color: highest_color = icon_data['Colors'] + if icon_data['Width'] >= highest_size: highest_size = icon_data['Width'] + + #Return that Icon file + + for i in icons_list: + if "{}x{}x{}.ico".format(highest_size, highest_size,highest_color) in i['filename']: + self.logger.debug("{:21} | Found: {}".format("", i['filename'])) + return i['filename'], i['ICON'] + else: + self.logger.error("{:<21} | Could not find Icon with index {}".format(" ", index)) + return ('','') + + + def extract_icons_from_dll(self, dll_file_path, dump=False, folder="./"): + # This function extracts icons from DLL files (and ICL files) + # Returns a list of dicts with the file name containing the name, index, width, height, colors and the icon itself + # This is kludgy as hell but it works + + # Mostly built off of: + # https://www.codeproject.com/Articles/16178/IconLib-Icons-Unfolded-MultiIcon-and-Windows-Vista + # https://hwiegman.home.xs4all.nl/fileformats/exe/WINHDR.TXT + + group_type = { 3: 'RT_ICON', 14 :'RT_GROUP_ICON' } + ICONS = [] + + f = open(dll_file_path,'rb') + dll_file = f.read() + f.close() + dll_bytes = bytearray(dll_file) + + self.logger.debug("{:<21} | Parsing DLL file {} with: dump={}, folder={}".format('',dll_file_path, dump, folder)) + self.logger.debug("{:<21} | Parsing NE DLL/ICL".format('')) + e_lfanew = struct.unpack(' 24: + continue # At the end of the type info array exit + rtresourcecount = struct.unpack('=8bpp) + 'bReserved' : tmp_grp[3], # Reserved + 'wPlanes' : struct.unpack('=8bpp) + 'bReserved' : cur_bytes[9], # Reserved + 'wPlanes' : struct.unpack('=8bpp) + 'bReserved' : cur_bytes[loc+3], # Reserved + 'wPlanes' : struct.unpack('=8bpp) + 'bReserved' : ani_bytes[LIST_item_size+17], # Reserved + 'wPlanes' : struct.unpack(' 0: + rectangle_posn = ("{0}px".format(colcount * self.squaresize), + "{0}px".format(rowcount * self.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() + self.logger.debug("{:<21} | Prelim SVG created: {}".format("", target_folder + svg_name)) + + self.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 + + + self.logger.info("{:<21} | Inkscape will open {} times to process {}".format("", min(len(rgbs), self.max_colors), target_folder + svg_name)) + + count = 0 + for rgb in rgbs: + count = count + 1 + if len(rgbs) >= self.max_colors: + self.logger.debug("{:<21} | Max colors ({}) hit exiting conversion".format("", self.max_colors)) + break + self.logger.info("{:<21} | [{:<3} / {:<3} {:<5}] Converting {}".format("", count, len(rgbs),str(round((float(count)/float(len(rgbs))*100),0)), rgb )) + self.fix_with_inkscape( rgbs[rgb] , tmp_file ) + + shutil.move(tmp_file, svgdoc.filename) + return(svgdoc.filename) + +## Image functions + + def convert_to_proper_svg_with_inkscape(self, svg_out, svg_in): + self.logger.debug("{:<21} | Converting {} to {} with Inkscape".format("",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 fix_with_inkscape(self, color, tmpfile): + self.logger.debug("{:<21} | Combining {} in {}".format("",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(self, svg_in, size, png_out): + self.logger.debug("{:<21} | Converting {} to {} of size {}".format("", svg_in, png_out, size)) + 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_ico_files(self, icon_filename, output_file_name): + self.logger.debug("{:<21} | Converting {} to {}".format("", icon_filename, output_file_name)) + convert_path = subprocess.check_output(["which", "convert"]).strip() + #self.logger.info("{:<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) + + + def convert_cur_files(self, cursor_filename, output_file_name): + self.logger.debug("{:<21} | Converting {} to {}".format("", cursor_filename, output_file_name)) + convert_path = subprocess.check_output(["which", "convert"]).strip() + self.logger.debug("{:<21} | {}".format(os.path.split(cursor_filename)[1], os.path.split(output_file_name)[1])) + args = [ + convert_path, + cursor_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 build_cursors(self, cursor_folder): + self.logger.debug("Generating x11 cursor in {}".format(cursor_folder + "cursors/")) + #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" + } + + + 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] + self.logger.debug("Building {:<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) +#### Helper functions + + def font_name( self, font ): + # From http://www.starrhorne.com/2012/01/18/how-to-extract-font-names-from-ttf-files-using-python-and-our-old-friend-the-command-line.html + self.logger.debug("Getting font names") + # Get font name and family + FONT_SPECIFIER_NAME_ID = 4 + FONT_SPECIFIER_FAMILY_ID = 1 + name = "" + family = "" + for record in font['name'].names: + self.logger.debug("Font record: {} ({})".format(record, record.nameID)) + if b'\x00' in record.string: + try: + name_str = record.string.decode('utf-16-be') + except UnicodeDecodeError: + name_str = record.string.decode('latin-1') + else: + try: + name_str = record.string.decode('utf-8') + except UnicodeDecodeError: + name_str = record.string.decode('latin-1') + + if record.nameID == FONT_SPECIFIER_NAME_ID and not name: + name = name_str + elif record.nameID == FONT_SPECIFIER_FAMILY_ID and not family: + family = name_str + if name and family: break + return name, family + + + def splitall(self, path): + allparts = [] + while 1: + parts = os.path.split(path) + if parts[0] == path: # sentinel for absolute paths + allparts.insert(0, parts[0]) + break + elif parts[1] == path: # sentinel for relative paths + allparts.insert(0, parts[1]) + break + else: + path = parts[0] + allparts.insert(0, parts[1]) + return allparts + + def get_icon_file_name(self, section, key, ignore_windir = False): + #input: + # section = the section in the config file + # key = key in theme file + # Returns: filename + + reg_key = "Software\\Classes\\" + file_name = '' + icon_number = 0 + if section in self.config and key in self.config[section]: + if isinstance(self.config[section][key], list): + file_name = self.config[section][key][0].lower() + else: + file_name = self.config[section][key].lower() + elif reg_key+section in self.config and key in self.config[reg_key+section]: + file_name = self.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 and not ignore_windir: + # 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: + if len(file_name.split(",")) >= 3: + loc = file_name.find(",", file_name.find(",")+1) + icon_number = file_name[loc:] + file_name = file_name[:loc] + else: + file_name, icon_number = file_name.split(",") + file_name = file_name.split("\\")[-1] + self.logger.debug("section {}, key: {}, filename: {}".format(section, key, file_name)) + + return (file_name, icon_number) + + def get_file_name(self, section, key, ignore_windir = False): + #input: + # section = the section in the config file + # key = key in theme file + # Returns: filename + + reg_key = "Software\\Classes\\" + file_name = '' + icon_number = 0 + if section in self.config and key in self.config[section]: + if isinstance(self.config[section][key], list): + file_name = self.config[section][key][0].lower() + else: + file_name = self.config[section][key].lower() + elif reg_key+section in self.config and key in self.config[reg_key+section]: + file_name = self.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 and not ignore_windir: + # we dont bother changing system icons + return False + else: + file_name = file_name.replace("%windir%".lower(),'') + + if "%ThemeDir%".lower() in file_name: + file_name = file_name.replace("%ThemeDir%".lower(),'') + + file_name = file_name.split("\\")[-1] + self.logger.debug("section {}, key: {}, filename: {}".format(section, key, file_name)) + + return file_name + + def null_string(self, data): + data = bytearray(data) + try: + string = data[:data.find(0)].decode('cp1252') + except UnicodeDecodeError: + string = data[:data.find(0)].decode('latin-1') + + return string + +## Install functions + + def install_theme(self, cursors=True, icons=True, wallpaper=True, sounds=True, colors=True, fonts=True, screensaver=True): + self.logger.info("Installing {}".format(self.theme_name)) + if cursors: + self.install_cursors() + if icons: + self.install_icons() + if fonts: + self.install_fonts() + if wallpaper: + self.install_wallpaper() + if sounds: + self.install_sounds() + if colors: + self.install_color_theme() + if screensaver: + self.logger.info("Screensavers require manual install. See the script in {}".format(self.folder_names['screensaver'])) + + def install_cursors(self, cursor_dir=False, os_cursor_dir=str(Path.home())+"/.icons/"): + self.logger.info("Installing cursors") + if not os.path.exists(os_cursor_dir): + self.logger.error("Cursor install directory does not exists: {}".format(os_cursor_dir)) + return + + if not cursor_dir: + cursor_dir = self.folder_names['cursors'] + + install_cursors_dir = os_cursor_dir + cursor_dir.split("/")[-2] + + self.logger.debug('Installing {} cursors to {}'.format(cursor_dir,install_cursors_dir)) + + shutil.rmtree(install_cursors_dir, ignore_errors=True) + shutil.copytree(self.folder_names['cursors'],install_cursors_dir,symlinks=True,ignore_dangling_symlinks=True) + + def install_icons(self, icons_dir=False, os_icons_dir=str(Path.home())+"/.icons/"): + self.logger.info("Installing icons") + + if not os.path.exists(os_icons_dir): + self.logger.error("Icons install directory does not exists: {}".format(os_icons_dir)) + return + + if not icons_dir: + icons_dir = self.folder_names['icons'] + + install_icons_dir = os_icons_dir + icons_dir.split("/")[-2] + + self.logger.debug('Installing {} icons to {}'.format(icons_dir,install_icons_dir)) + + shutil.rmtree(install_icons_dir, ignore_errors=True) + shutil.copytree(self.folder_names['icons'],install_icons_dir,symlinks=True,ignore_dangling_symlinks=True) + + + def install_color_theme(self, color_theme_dir=False, os_theme_dir=str(Path.home())+"/.themes/"): + self.logger.info("Installing color theme") + + if not os.path.exists(os_theme_dir): + self.logger.error("Theme install directory does not exists: {}".format(os_theme_dir)) + return + + if not color_theme_dir: + color_theme_dir = self.folder_names['theme'] + + install_theme_dir = os_theme_dir + color_theme_dir.split("/")[-2] + + self.logger.debug('Installing {} colors to {}'.format(color_theme_dir,install_theme_dir)) + + shutil.rmtree(install_theme_dir, ignore_errors=True) + shutil.copytree(self.folder_names['theme'],install_theme_dir,symlinks=True,ignore_dangling_symlinks=True) + + def install_fonts(self, fonts_dir=False, os_fonts_dir=str(Path.home())+"/.fonts/"): + self.logger.info("Installing fonts") + + if not os.path.exists(os_fonts_dir): + self.logger.error("Theme install directory does not exists: {}".format(os_fonts_dir)) + return + + if not fonts_dir: + fonts_dir = self.folder_names['fonts'] + + install_theme_dir = os_fonts_dir + fonts_dir.split("/")[-2] + + self.logger.debug('Installing {} fonts to {}'.format(fonts_dir,install_theme_dir)) + + shutil.rmtree(install_theme_dir, ignore_errors=True) + shutil.copytree(self.folder_names['fonts'],install_theme_dir,symlinks=True,ignore_dangling_symlinks=True) + + def install_sounds(self, sounds_dir=False, os_sounds_dir=str(Path.home())+"/.local/share/sounds/"): + self.logger.info("Installing sounds") + + if not os.path.exists(os_sounds_dir): + self.logger.error("Theme install directory does not exists: {}".format(os_sounds_dir)) + return + + if not sounds_dir: + sounds_dir = self.folder_names['sounds'] + + install_theme_dir = os_sounds_dir + sounds_dir.split("/")[-2] + + self.logger.debug('Installing {} sounds to {}'.format(sounds_dir,install_theme_dir)) + + shutil.rmtree(install_theme_dir, ignore_errors=True) + shutil.copytree(self.folder_names['sounds'],install_theme_dir,symlinks=True,ignore_dangling_symlinks=True) + + + def install_wallpaper(self, os_wallpaper_dir=str(Path.home())+"/Pictures/"): + self.logger.info("Installing wallpaper") + + if not os.path.exists(os_wallpaper_dir): + self.logger.error("Theme install directory does not exists: {}".format(os_wallpaper_dir)) + return + + if (self.theme_config['wallpaper']['theme_wallpaper'] and + self.theme_config['wallpaper']['theme_wallpaper']['wallpaper'] and + self.theme_config['wallpaper']['theme_wallpaper']['path']): + self.logger.debug("Copying {} to {}".format(self.theme_config['wallpaper']['theme_wallpaper']['path'], os_wallpaper_dir + self.theme_config['wallpaper']['theme_wallpaper']['new_filename'])) + try: + shutil.copy(self.theme_config['wallpaper']['theme_wallpaper']['path'], os_wallpaper_dir + self.theme_config['wallpaper']['theme_wallpaper']['new_filename']) + except: + self.logger.error("Could not install wallpaper to {}".format(os_wallpaper_dir)) + + if self.theme_config['wallpaper']['extra_wallpapers']: + for wallpaper in self.theme_config['wallpaper']['extra_wallpapers']: + self.logger.debug("Copying {} to {}".format(wallpaper, os_wallpaper_dir)) + try: + shutil.copy(wallpaper, os_wallpaper_dir) + except: + self.logger.error("Could not install wallpaper to {}".format(wallpaper)) + raise + +## Enable the theme in XFCE + def enable_theme(self, cursors=True, icons=True, wallpaper=True, sounds=True, colors=True, fonts=True, screensaver=True): + + self.logger.info("Enabling {}".format(self.theme_name)) + if cursors: + self.logger.info("Enabling New Cursors") + # /Gtk/CursorThemeName + # /Gtk/CursorThemeSize TODO: Can we use this for HDPI themes? + self.xfconf_query('xsettings', '/Gtk/CursorThemeName', self.theme_name+"_Cursors") + if icons: + self.logger.info("Enabling New Icons") + # /Net/FallbackIconTheme + # /Net/IconThemeName + self.xfconf_query('xsettings', '/Net/FallbackIconTheme', 'Chicago95') + self.xfconf_query('xsettings', '/Net/IconThemeName', self.theme_name+"_Icons") + if fonts: + self.enable_fonts() + if wallpaper: + # image-style 2 == Tiled + # image-style 4 == Scaled + # image-style 3 == Streched + self.logger.info("Enabling New Wallpaper") + if self.theme_config['wallpaper']['theme_wallpaper'] and self.theme_config['wallpaper']['theme_wallpaper']['new_filename']: + self.xfconf_query('xfce4-desktop', '/backdrop/screen0/monitorVirtual1/workspace0/last-image', str(Path.home()) + "/Pictures/" + self.theme_config['wallpaper']['theme_wallpaper']['new_filename']) + + if self.theme_config['wallpaper']['theme_wallpaper']['tilewallpaper']: + self.xfconf_query('xfce4-desktop', '/backdrop/screen0/monitorVirtual1/workspace0/image-style', "2") + elif self.theme_config['wallpaper']['theme_wallpaper']['wallpaperstyle'] == 2: + self.xfconf_query('xfce4-desktop', '/backdrop/screen0/monitorVirtual1/workspace0/image-style', "3") + else: + self.xfconf_query('xfce4-desktop', '/backdrop/screen0/monitorVirtual1/workspace0/image-style', "4") + else: + self.logger.debug("Wallpaper failed to install") + + + + if sounds: + self.logger.info("Enabling New Sounds") + self.xfconf_query('xsettings', '/Net/SoundThemeName', self.theme_name+"_Sounds") + if colors: + self.logger.info("Enabling New Color Scheme") + self.xfconf_query('xfwm4', '/general/theme', self.theme_name+"_Theme") + self.xfconf_query('xsettings', '/Net/ThemeName', self.theme_name+"_Theme") + if screensaver: + self.logger.info("Screensavers require manual install. See the script in {}".format(self.folder_names['screensaver'])) + + + def enable_fonts(self): + # /Gtk/FontName "Family name and size" <- this is used for the other fonts, not titlebars + + # Typical fonts based on anaylisis of over 8,000 fonts to find most used fonts and linux equivalent + # These font are typically included with Xubuntu installs + # The check is: + # 1 - Is the font family installed? If so just use that + # 2 - If not, check for the 15 most popular fonts and Xubuntu alternatives + # 3 - Otherwise use Sans, inform the user and give a link to fontworld/check the theme folder, install the font then rerun plus + font_weights = { + "FW_DONTCARE" : "0", + "FW_THIN" : "100", + "FW_EXTRALIGHT" : "200", + "FW_ULTRALIGHT" : "200", + "FW_LIGHT" : "300", + "FW_NORMAL" : "400", + "FW_REGULAR" : "400", + "FW_MEDIUM" : "500", + "FW_SEMIBOLD" : "600", + "FW_DEMIBOLD" : "600", + "FW_BOLD" : "700", + "FW_EXTRABOLD" : "800", + "FW_ULTRABOLD" : "800", + "FW_HEAVY" : "900", + "FW_BLACK" : "1000" + } + + font_replacements = { + "MS Sans Serif" : "Sans", + "Arial" : "Liberation Sans", + "Times New Roman" : "Liberation Serif", + "Tahoma" : "Kalimati", + "News Gothic MT" : "News Cycle", + "Abadi MT Condensed Light" : "", + "Century Gothic" : "League Gothic", + "Verdana" : "Kalimati", + "Microsoft Sans Serif" : "Sans", + "Arial Narrow" : "Liberation Narrow", + "Copperplate Gothic Light" : "League Gothic", + "Impact" : "League Gothic", + "MS Serif" : "Sans", + "‚l‚r ‚oƒSƒVƒbƒN" : "Sans", + "Courier New" : "Liberation Mono", + "Georgia" : "Rekha", + "Lucida Grande" : "Garuda" + } + + self.logger.info("Enabling New Fonts") + self.get_font_list() + if 'nonclientmetrics' in self.theme_config: + + xfconf_query_path = subprocess.check_output(["which", "xfconf-query"]).strip() + self.logger.debug("Getting DPI") + args = [ + xfconf_query_path, + "-v", '-l', '-c', 'xsettings', + "-p", '/Xft/DPI' + ] + dpi = subprocess.check_output(args).split()[1] + + self.logger.debug("Getting MenuFont and CaptionFont") + for logfont in ['lfcaptionfont', 'lfMenuFont']: + if isinstance(self.theme_config['nonclientmetrics'][logfont], dict) and 'lfFaceName[32]' in self.theme_config['nonclientmetrics'][logfont]: + + font_family = self.theme_config['nonclientmetrics'][logfont]['lfFaceName[32]'] + lfheight = self.theme_config['nonclientmetrics'][logfont]['lfHeight'] + font_weight = font_weights[self.theme_config['nonclientmetrics'][logfont]['lfWeight']] + + if lfheight < 0: + font_size = abs((lfheight / int(dpi)) * 72) #Per MS LOGFONT spec + else: + font_size = lfheight + + self.logger.debug("{:<21} | font: {} weight: {}".format(logfont, font_family, font_weight)) + + installed_fonts = self.get_font_list() + if font_family in installed_fonts: + self.logger.debug("{:<21} | Font installed".format(font_family)) + font = font_family + else: + self.logger.debug("{:<21} | Font not installed searching for alternate".format(font_family)) + if font_family in font_replacements and font_replacements[font_family] in self.get_font_list(): + self.logger.debug("{:<21} | Replacement found for {}: {}".format("", font_family, font_replacements[font_family])) + font = font_replacements[font_family] + else: + font = "Sans" + self.logger.debug("{:<21} | No replacement found using Sans".format('')) + if logfont == 'lfcaptionfont': + lfcaptionfont = font + + if logfont == 'lfMenuFont' and font != lfcaptionfont and font != "Sans": + self.logger.info("Menufont: font: {} weight: {}".format(font, font_weight)) + + for gtk_menu in [ self.folder_names['theme']+"gtk-3.24/gtk-menu.css", self.folder_names['theme']+"gtk-3.0/gtk-menu.css"]: + + shutil.move( gtk_menu, gtk_menu+"~" ) + fileh = open(gtk_menu+"~","r") + nfileh = open(gtk_menu,"w") + for line in fileh: + if '.menubar' in line: + self.logger.debug("{:<21} | Adding menubar font to {}: {} weight: {}".format('', gtk_menu, font, font_weight)) + line = line.replace(line, line+"\n"+" font-family: {};\n font-weight: {};\n".format(font, font_weight)) + nfileh.write(line) + fileh.close() + nfileh.close() + + self.install_color_theme() + + elif logfont == 'lfcaptionfont': + if font == "Sans": + font = "Sans Bold" + self.logger.info("Captionfont: font: {} weight: {}".format(font, font_weight)) + self.xfconf_query('xfwm4', '/general/title_font', font + ' 8') + +## Enable Helper functions + def xfconf_query(self, channel, prop, new_value): + xfconf_query_path = subprocess.check_output(["which", "xfconf-query"]).strip() + self.logger.debug("Changing xfconf setting {}/{} to {}".format(channel, prop, new_value)) + args = [ + xfconf_query_path, + "--channel", channel, + "--property", prop, + "--set", new_value + ] + subprocess.check_call(args, stdout=subprocess.DEVNULL) + + + def get_font_list(self): + fc_list = subprocess.check_output(["which", "fc-list"]).strip() + fonts_output = subprocess.check_output([fc_list]) + + + fonts = [] + for font in fonts_output.decode().split('\n'): + if len(font.split(":")) > 1 and font.split(":")[1] not in fonts: + fonts.append(font.split(":")[1].strip()) + + return(fonts) + + def logo(self): + logo = ''' + + || Chicago95 ** + ######## ## ## **** + ## || ## ## ## $$$$$$**** + ## || ## ## ##$$ ** + ######## ## ## $$$$ + ## || ##### $$ ** + ## |||||||| $$$$$$ ** + ## +''' + return logo + + + +