Chicago95/Plus/pluslib.py

3193 lines
124 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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
running_folder = os.path.dirname(os.path.abspath(__file__))
share_dir = running_folder
libexec_dir = running_folder
work_dir = running_folder
if not os.path.exists(work_dir):
os.makedirs(work_dir)
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
# Placeholder for inkscape version and path information. It will be populated with actual information after it is confirmed that the user has Inkscape
self.inkscape_info = inkscape_info("void", [0,0,0])
# 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()
#Assuming the previous portion doesn't return an exception, the placeholder information is replaced
self.get_inkscape_info()
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
}
#if logging.getLogger(__name__).getEffectiveLevel() == logging.DEBUG:
# self.logger.debug("Printing current theme config")
# self.print_theme_config()
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/<files> 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/"
self.logger.debug("Path to theme: {}, theme file name: {}".format(self.path_to_theme, self.theme_file_name))
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])) + "/"
if self.path_to_theme[0:2] == "//":
self.path_to_theme = self.path_to_theme[1:]
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", signed=True),
"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']+icon_filename,"wb")
f.write(icon_file)
f.close()
else:
shutil.copyfile(path, self.folder_names['icons']+icon_filename)
svg_icon_file = self.convert_icon(self.folder_names['icons'], self.folder_names['icons']+icon_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()
try:
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()
except:
self.logger.critical("Error converting {}. Cursor file corrupt".format(cursor_src_folder+pointers[current_cursor]+".cur"))
# 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/']:
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 = []
if not dll_file_path:
return False
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('<I',dll_bytes[60:64])[0]
ne_header_char = dll_bytes[e_lfanew:e_lfanew+2].decode()
if ne_header_char == 'NE':
self.logger.debug("{:<21} | NE Header: {}".format('', ne_header_char))
ne_rsrctab = struct.unpack('<H',dll_bytes[e_lfanew+36:e_lfanew+36+2] )[0] + e_lfanew
rscAlignShift = struct.unpack('<H',dll_bytes[ne_rsrctab:ne_rsrctab+2] )[0]
resource_table = {'rscAlignShift':rscAlignShift, 'rscTypes': [], 'rscEndTypes' : 0, 'rscResourceNames': [], 'rscEndNames': 0}
self.logger.debug("{:<21} | Offset from 0 to NE header (e_lfanew): {}".format('',e_lfanew))
self.logger.debug("{:<21} | Parsing Resource Tables (ne_rsrctab) at {} ({})".format('',ne_rsrctab, hex(ne_rsrctab)))
TNAMEINFO = []
ptr = ne_rsrctab+2 #Advance ptr to TYPEINFO
rttypeid = 1
while rttypeid != 0 and rttypeid < 24:
tmp_ba = dll_bytes[ptr:]
rttypeid = struct.unpack('<H',tmp_ba[0:2] )[0] & 0x7FFF
if rttypeid == 0 or rttypeid > 24:
continue # At the end of the type info array exit
rtresourcecount = struct.unpack('<H',tmp_ba[2:4] )[0]
tmp_ba = dll_bytes[ptr+8:]
self.logger.debug("{:<21} | Type ID {} has {} records".format('',group_type[rttypeid], rtresourcecount, ptr+8, hex(ptr+8)))
size = 0
for x in range(0, rtresourcecount):
TNAMEINFO.append( {
'rttypeid' : rttypeid,
'rnOffset' : struct.unpack('<H',tmp_ba[size+ 0:size+2])[0] << rscAlignShift,
'rnLength' : struct.unpack('<H',tmp_ba[size+ 2:size+4])[0],
'rnFlags' : struct.unpack('<H',tmp_ba[size+ 4:size+6])[0],
'rnID' : struct.unpack('<H',tmp_ba[size+ 6:size+8])[0] & 0x7FFF,
'rnHandle' : struct.unpack('<H',tmp_ba[size+ 8:size+10])[0],
'rnUsage' : struct.unpack('<H',tmp_ba[size+ 10:size+12])[0]
} )
size = size + 12 #Skip ahead these entries
ptr = ptr + size + 8 # Skip to the next TYPEINFO
ptr = ptr + 2 # rscEndTypes
tmp_ba = dll_bytes[ptr:]
names = 0
length = 1
#Resource Names
RESOURCENAMES = []
while length != 0:
length = tmp_ba[names]
RESOURCENAMES.append(tmp_ba[names+1:names+1+length].decode())
names = names + tmp_ba[names] + 1
resource_table['rscResourceNames'].extend(RESOURCENAMES)
resource_table['rscTypes'].extend(TNAMEINFO)
for GRPICONDIRENTRY in resource_table['rscTypes']:
if GRPICONDIRENTRY['rttypeid'] == 14: #RT_GROUP_ICON
try:
name = RESOURCENAMES[GRPICONDIRENTRY['rnID']]
except (KeyError, IndexError):
name = os.path.splitext(dll_file_path.split("/")[-1])[0]
pass
idReserved = struct.unpack('<H',dll_bytes[GRPICONDIRENTRY['rnOffset']+0:GRPICONDIRENTRY['rnOffset']+2])[0]
idType = struct.unpack('<H',dll_bytes[GRPICONDIRENTRY['rnOffset']+2:GRPICONDIRENTRY['rnOffset']+4])[0]
idCount = struct.unpack('<H',dll_bytes[GRPICONDIRENTRY['rnOffset']+4:GRPICONDIRENTRY['rnOffset']+6])[0]
tmp_grp = dll_bytes[GRPICONDIRENTRY['rnOffset']+6:]
for x in range(0, idCount):
rtIcon = {
'bWidth' : tmp_grp[0], # Width, in pixels, of the image
'bHeight' : tmp_grp[1], # Height, in pixels, of the image
'bColorCount' : tmp_grp[2], # Number of colors in image (0 if >=8bpp)
'bReserved' : tmp_grp[3], # Reserved
'wPlanes' : struct.unpack('<H',tmp_grp[4:6])[0], # Color Planes
'wBitCount' : struct.unpack('<H',tmp_grp[6:8])[0], # Bits per pixel
'dwBytesInRes' : struct.unpack('<L',tmp_grp[8:12])[0], # how many bytes in this resource?
'nId' : struct.unpack('<H',tmp_grp[12:14])[0] # RT_ICON rnID
}
for RT_ICON in resource_table['rscTypes']:
if RT_ICON['rttypeid'] == 3 and RT_ICON['rnID'] == rtIcon['nId']:
icon_file = bytearray(2) + struct.pack('<H',1) + struct.pack('<H',1)
ICONENTRY = tmp_grp[0:12] + struct.pack('<L', 22)
icon_bitmap = dll_bytes[RT_ICON['rnOffset']:RT_ICON['rnOffset']+rtIcon['dwBytesInRes']]
#print(ICONENTRY)
if rtIcon['bColorCount'] == 0: rtIcon['bColorCount'] = 256
filename = "{}_{}_{}x{}x{}.ico".format(name, GRPICONDIRENTRY['rnID'], rtIcon['bWidth'], rtIcon['bHeight'], rtIcon['bColorCount'])
if dump:
self.logger.info("{:<21} | Creating: {}".format('', folder + filename))
f = open(folder + filename,"wb")
f.write(icon_file+ICONENTRY+icon_bitmap)
f.close()
ICONS.append({
'filename': filename,
'ID' : GRPICONDIRENTRY['rnID'],
'Width' : rtIcon['bWidth'],
'Height' : rtIcon['bHeight'],
'Colors' : rtIcon['bColorCount'],
'ICON': icon_file+ICONENTRY+icon_bitmap})
tmp_grp = tmp_grp[14:]
return ICONS
def extract_cur(self, file_name):
self.logger.debug("{:21} | Parsing cursor file {}".format("", file_name))
# input: .cur file location/name
# output: dict with cursor information
f = open(file_name,'rb')
cur_file = f.read()
f.close()
cur_bytes = bytearray(cur_file)
rtIconDir = False
rtIconDirEntry = False
INFO = False
icon = []
icon.append({
'rtIconDir' : {
'res' : struct.unpack('<H',cur_bytes[0:2])[0],
'ico_type' : struct.unpack('<H',cur_bytes[2:4])[0],
'ico_num_images' : struct.unpack('<H',cur_bytes[4:6])[0]
},
'ico_file' : cur_bytes,
#ICONDIRENTRY
# TODO Add multiple cursors here if needed like icons
'rtIconDirEntry' : {
'bWidth' : cur_bytes[6], # Width, in pixels, of the image
'bHeight' : cur_bytes[7], # Height, in pixels, of the image
'bColorCount' : cur_bytes[8], # Number of colors in image (0 if >=8bpp)
'bReserved' : cur_bytes[9], # Reserved
'wPlanes' : struct.unpack('<H',cur_bytes[10:12])[0], # Color Planes
'wBitCount' : struct.unpack('<H',cur_bytes[12:14])[0], # Bits per pixel
'dwBytesInRes' : struct.unpack('<L',cur_bytes[14:18])[0], # how many bytes in this resource?
'dwDIBOffset' : struct.unpack('<H',cur_bytes[18:20])[0] # RT_ICON rnID
}
})
cursor = {
'icon' : icon
}
return cursor
def extract_ico(self, file_name, dump=False, folder='./'):
self.logger.debug("{:21} | Parsing ICO file {} with: dump={}, folder={}".format("", file_name, dump, folder))
if not file_name:
self.logger.error("ICO file does not exist. Enable debug for more information")
return False
# input: .ico file location/name
# output: dict with icon information
f = open(file_name,'rb')
cur_file = f.read()
f.close()
cur_bytes = bytearray(cur_file)
if cur_bytes[0:2].decode() == "BM":
return "bmp"
rtIconDir = False
rtIconDirEntry = False
ICONS = []
idReserved = struct.unpack('<H',cur_bytes[0:2])[0]
idType = struct.unpack('<H',cur_bytes[2:4])[0]
idCount = struct.unpack('<H',cur_bytes[4:6])[0]
loc = 6
if idType == 1: # ICONS ONLY NO CURSORS
name = os.path.splitext(os.path.basename(file_name))[0]
for i in range(0,idCount):
#ICONDIRENTRY
rtIconDirEntry = {
'bWidth' : cur_bytes[loc], # Width, in pixels, of the image
'bHeight' : cur_bytes[loc+1], # Height, in pixels, of the image
'bColorCount' : cur_bytes[loc+2], # Number of colors in image (0 if >=8bpp)
'bReserved' : cur_bytes[loc+3], # Reserved
'wPlanes' : struct.unpack('<H',cur_bytes[loc+4:loc+6])[0], # Color Planes
'wBitCount' : struct.unpack('<H',cur_bytes[loc+6:loc+8])[0], # Bits per pixel
'dwBytesInRes' : struct.unpack('<L',cur_bytes[loc+8:loc+12])[0], # how many bytes in this resource?
'dwImageOffset' : struct.unpack('<L',cur_bytes[loc+12:loc+16])[0] # RT_ICON rnID
}
ICONHEADER = bytearray(2) + struct.pack('<H',1) + struct.pack('<H',1)
IconDirectoryEntry = cur_bytes[loc:loc+12] + struct.pack('<L', 22)
img = cur_bytes[rtIconDirEntry['dwImageOffset']:rtIconDirEntry['dwImageOffset']+rtIconDirEntry['dwBytesInRes']]
if rtIconDirEntry['bColorCount'] == 0: rtIconDirEntry['bColorCount'] = 256
filename = "{}_{}_{}x{}x{}.ico".format(name, i, rtIconDirEntry['bWidth'], rtIconDirEntry['bHeight'], rtIconDirEntry['bColorCount'])
if dump:
self.logger.info("Creating:", folder + filename)
f = open(folder + filename,"wb")
f.write(ICONHEADER+IconDirectoryEntry+img)
f.close()
ICONS.append({
'filename': filename,
'ID' : i,
'Width' : rtIconDirEntry['bWidth'],
'Height' : rtIconDirEntry['bHeight'],
'Colors' : rtIconDirEntry['bColorCount'],
'ICON': ICONHEADER+IconDirectoryEntry+img
})
loc += 16
return ICONS
def extract_ani(self, file_name):
# convert an ani a dict with Icon information
# input: .ani file location/name
# output: dict with cursor information
# from http://www.toolsandtips.de/Tutorial/Aufbau-Animierte-Cursor.htm
#1. 0000. First RIFF : then the size of the file as DWORD, a total of 8 bytes. Note the length of the file can be different!
# A: the actual length of the file.
# B: the length of the file minus the 8 bytes for RIFF and the DWORD for the length specification.
#2. 0008. ACON : This part may contain the following. Optional!
# LIST : Length as DWORD to "anih" without the 4 bytes of the length specification from LIST . The data can be: Optional!
# INAM: Size of the title as DWORD without the 4 bytes of the length specification, then data. Note there can be "INFO" in front of it! Optional!
# IART: Length from the author as DWORD without the 4 bytes of the length specification, then data.
# anih : size of the Ani header structure as DWORD maximum 36 bytes, then the structure with 36 bytes. The first value of the DWORD is with the size of the structure = 36 bytes. (See Ani header structure).
# rate : size of the rate as DWORD. Data in DWORDs. The specification of "rate" can also be optional !
# With which the speed of the image change can be adjusted more finely, does not have to be.
# Since a standard value for the speed is already entered in the Ani header structure (ANIHEADER.iDispRate) !
# According to the length specification. So with Hex 10 = 16 bytes, 4 DWORDs = 16 bytes follow.
# Example: The Ani has 4 pictures, then a " DWORD " followed by a DWORD with a hex number 10 = 16 bytes, the length as DWORD!
# This DWORD is followed by a DWORD for each picture, which indicates the speed of the picture change. That can be different.
# Eg. For 4 pictures 0000 0011, 0000 0011, 0000 0011, 0000 0011.or: for 4 pictures 0000 0011, 0000 0030, 0000 0050, 0000 0018.
# seq : Size of the sequence block as DWORD, data in DWORDs. The specification of "seq" can also be optional !
# What the order of the images is in the animation before the animation is repeated.
# Eg. 5 pictures are actually in the file (ANIHEADER.nFrames = 5) in ANIHEADER.nSteps = 8 there are 8 pictures.
# The length of "seq" would then be Hex 20 = 32 bytes = 8 DWORD.
# The arrangement of the pictures could be picture 1, picture 1, picture 2, picture 3, picture 4, picture 1, picture 2, picture 5 then the series is repeated.
# If the order of the pictures were 1,2,3,4,5, then you don't actually need a "seq" block.
# If there are more pictures in the ANIHEADER.nSteps than in the ANIHEADER.nFrames then the "seq" block is mandatory!
# Which of course must then correspond to the size of the number of images, e.g. 8 images * 4 bytes (= 1DWORD) = 32 bytes = hex 20 = 8 DWORDs.
# Furthermore, if the "rate" block is to be used, the "rate" block must have the same size as the "seq" block, ie also 32 bytes = 8 DWORDs! One DWORD for each image to be displayed.
#
# 3. LIST : Length of the rest of the file, as DWORD, from this length specification, that is after this DWORD.
# fram:
# icon : Size of the image data after this DWORD, then data. (First picture)
# Note! In the size specification, the sizes of the two cursor structures, the BITMAPINFOHEADER structure, the color table and the XOR and the AND image are added together.
# So from here to the next "icon" frame, or the end of the file if it is the last picture. The icon block contains according to the size specification.
# A: The structure CURHEADER (or iconheader) with the size is 6 bytes. (see CURHEADER structure).
# B: The structure CURSORDIEENTRY (or icondir) with the size is 16 bytes. (see CURSORDIEENTRYR structure)
# C: the structure BITMAPINFOHEADER with the size is 40 bytes. (see BITMAPINFOHEADER structure)
# D: color table (number of colors * 4 bytes)
# E: image data of the cursor first the XOR image then the AND image ........ ........ (etc.) . .......
#
# icon : size of the image data after this DWORD, then data. (Last picture)
# Note! In the size specification, the sizes of the two cursor structures, the BITMAPINFOHEADER structure, the color table and the XOR and the AND image are added together.
# So from here to the end of the file. The icon block contains according to the size specification.
# A: The structure CURHEADER with the size is 6 bytes. (see CURHEADER structure)
# B: the structure CURSORDIEENTRY with the size is 16 bytes. (see CURSORDIEENTRYR structure)
# C: the structure BITMAPINFOHEADER with the size is 40 bytes. (see BITMAPINFOHEADER structure)
# D: color table (number of colors * 4 bytes)
# E: image data of the cursor first the XOR image then the AND image
#
f = open(file_name,'rb')
ani_file = f.read()
f.close()
ani_bytes = bytearray(ani_file)
rate = False
seq = False
rtIconDir = False
rtIconDirEntry = False
INFO = False
anih = False
icon = []
icon_count = 0
ckID = ani_bytes[0:4].decode()
ckSize = struct.unpack('<L',ani_bytes[4:8])[0]
ckForm = ani_bytes[8:12].decode()
total_size = 0
self.logger.debug("{:<21} | Extracting cursors/icons from ani file: {}".format("", file_name))
# ANI files are just RIFF files
if ckID == 'RIFF':
self.logger.debug("{:<21} | {} ckSize :{}".format("","RIFF detected", ckSize))
if ckForm == 'ACON': #ACON is optional
#self.logger.debug("ACON detected (optional)")
total_size = 12 # RIFF Header with ACON
else:
total_size = 8 # RIFF Header without ACON
if ckSize == len(ani_bytes) - 8:
ckSize = ckSize + 8 # Sometimes, but not always, the header isn't included in ckSize
self.logger.debug("{:<21} | Adjusting ckSize to actual file size: {}".format("",ckSize))
while total_size < ckSize:
section = ani_bytes[total_size:total_size+4].decode()
total_size = total_size + 4
chunk_size = struct.unpack('<L',ani_bytes[total_size:total_size+4])[0]
total_size = total_size + 4
#print("Chunk {}, Size: {}".format(section, chunk_size))
#print(ani_bytes[total_size:total_size+36])
if section == 'anih': #ANI Header
self.logger.debug("{:<21} | Chunk: anih".format(""))
anih = {
'cbSize': struct.unpack('<L',ani_bytes[total_size:total_size+4])[0],
'nFrames': struct.unpack('<L',ani_bytes[total_size+4:total_size+8])[0],
'nSteps' : struct.unpack('<L',ani_bytes[total_size+8:total_size+12])[0],
'iWidth' : struct.unpack('<L',ani_bytes[total_size+12:total_size+16])[0],
'iHeight' : struct.unpack('<L',ani_bytes[total_size+16:total_size+20])[0],
'iBitCount' : struct.unpack('<L',ani_bytes[total_size+20:total_size+24])[0],
'nPlanes' : struct.unpack('<L',ani_bytes[total_size+24:total_size+28])[0],
'iDispRate' : struct.unpack('<L',ani_bytes[total_size+28:total_size+32])[0], # The value is expressed in 1/60th-of-a-second units, which are known as jiffie, ignored if seq exists
'bfAttributes' : struct.unpack('<L',ani_bytes[total_size+32:total_size+36])[0]
}
elif section == 'rate':
self.logger.debug("{:<21} | Chunk: rate, size: {}".format("", chunk_size))
rate = []
for jiffie in range(0,chunk_size,4):
rate.append(struct.unpack('<L',ani_bytes[total_size+jiffie:total_size+jiffie+4])[0])
elif section == 'seq ':
self.logger.debug("{:<21} | Chunk: seq, size: {}".format("",chunk_size))
seq = []
for sequence in range(0,chunk_size,4):
seq.append(struct.unpack('<L',ani_bytes[total_size+sequence:total_size+sequence+4])[0])
# bfAttributes: 1 == CUR or ICO, 0 == BMP, 3 == 'seq' block is present
elif section == 'LIST':
chunk_type = ani_bytes[total_size:total_size+4].decode()
LIST_item_size = total_size + 4
self.logger.debug("{:<21} | Chunk: {}, size: {}".format("",chunk_type, chunk_size))
if chunk_type == 'INFO':
INFO = {}
while LIST_item_size <= chunk_size:
try:
info_section = ani_bytes[LIST_item_size:LIST_item_size+4].decode()
list_chunk_size = struct.unpack('<L',ani_bytes[LIST_item_size+4:LIST_item_size+8])[0]
INFO[info_section] = ani_bytes[LIST_item_size+8:LIST_item_size+8+list_chunk_size].decode()
except UnicodeDecodeError:
info_section = ani_bytes[LIST_item_size:LIST_item_size+4].decode('latin-1')
list_chunk_size = struct.unpack('<L',ani_bytes[LIST_item_size+4:LIST_item_size+8])[0]
INFO[info_section] = ani_bytes[LIST_item_size+8:LIST_item_size+8+list_chunk_size].decode('latin-1')
if (list_chunk_size % 2) != 0: # Yay DWORD boundaries
list_chunk_size = list_chunk_size + 1
LIST_item_size = LIST_item_size + list_chunk_size + 8
elif chunk_type == 'fram':
info_section = ani_bytes[LIST_item_size:LIST_item_size+4].decode()
while LIST_item_size < chunk_size:
self.logger.debug("{:<21} | Chunk: {}, size: {}".format("",info_section, LIST_item_size))
info_section = ani_bytes[LIST_item_size:LIST_item_size+4].decode()
list_chunk_size = struct.unpack('<L',ani_bytes[LIST_item_size+4:LIST_item_size+8])[0]
if info_section == 'icon':
icon.append({
'index' : icon_count,
#ICONDIR
'rtIconDir' : {
'res' : struct.unpack('<H',ani_bytes[LIST_item_size+8:LIST_item_size+10])[0],
'ico_type' : struct.unpack('<H',ani_bytes[LIST_item_size+10:LIST_item_size+12])[0],
'ico_num_images' : struct.unpack('<H',ani_bytes[LIST_item_size+12:LIST_item_size+14])[0]
},
#ICONDIRENTRY
'rtIconDirEntry' : {
'bWidth' : ani_bytes[LIST_item_size+14], # Width, in pixels, of the image
'bHeight' : ani_bytes[LIST_item_size+15], # Height, in pixels, of the image
'bColorCount' : ani_bytes[LIST_item_size+16], # Number of colors in image (0 if >=8bpp)
'bReserved' : ani_bytes[LIST_item_size+17], # Reserved
'wPlanes' : struct.unpack('<H',ani_bytes[LIST_item_size+18:LIST_item_size+20])[0], # Color Planes (or hotspot X coords for cur)
'wBitCount' : struct.unpack('<H',ani_bytes[LIST_item_size+20:LIST_item_size+22])[0], # Bits per pixel
'dwBytesInRes' : struct.unpack('<L',ani_bytes[LIST_item_size+22:LIST_item_size+26])[0], # how many bytes in this resource?
'dwDIBOffset' : struct.unpack('<H',ani_bytes[LIST_item_size+26:LIST_item_size+28])[0] # RT_ICON rnID
},
'ico_file' : ani_bytes[LIST_item_size+8:LIST_item_size + list_chunk_size + 8]
})
icon_count += 1
#print(info_section, hex(LIST_item_size), list_chunk_size)
if (list_chunk_size % 2) != 0: # Yay DWORD boundaries
list_chunk_size = list_chunk_size + 1
LIST_item_size = LIST_item_size + list_chunk_size + 8
total_size = total_size + chunk_size # The 8 accounts for the chunk id and size which is not included in the size
else:
self.logger.error("No RIFF ID, is {} an ANI file?".format(file_name))
self.logger.debug("{:<21} | RIFF ID: {}, Form: {}".format("", ckID, ckForm))
if INFO:
for i in INFO:
self.logger.debug("{:<21} | {:<21} | {}".format("",i, INFO[i]))
if anih:
for i in anih:
self.logger.debug("{:<21} | {:<21} | {}".format("",i, anih[i]))
for section in icon:
if section['index']:
self.logger.debug("{:<21} | Index: {}".format("",section['index']))
self.logger.debug("{:<21} | rtIconDir".format(""))
for j in section['rtIconDir']:
self.logger.debug("{:<21} | {:<21} | {}".format("",j, section['rtIconDir'][j]))
self.logger.debug("{:<21} | rtIconDirEntry".format(""))
for j in section['rtIconDirEntry']:
self.logger.debug("{:<21} | {:<21} | {}".format("",j, section['rtIconDirEntry'][j]))
cursor = {
'INFO' : INFO,
'anih' : anih,
'seq' : seq,
'rate' : rate,
'icon' : icon
}
return cursor
def convert_icon(self, target_folder, icon_file_path, tmp_file=work_dir + "/chicago95_tmp_file.svg"):
## Converts Icons to PNG
# Input:
# folder: svg file destination folder
# icon_file_path: theme icon file to be processed
# tmp_file: tmp working file for inkscape
# Lots of code lifted from pixel2svg
path_to_icon, icon_file_name = os.path.split(icon_file_path)
icon_name, icon_ext = os.path.splitext(icon_file_name)
svg_name = icon_name+".svg"
if not os.path.exists(icon_file_path):
# get the actual filename
icon_file_path = [self.theme_files[i] for i in self.theme_files if icon_file_path in i][0]
self.logger.debug("{:<21} | Converting {} to {} using pixel2svg".format("", icon_file_path, svg_name))
# Open the icon file
try:
image = Image.open(icon_file_path)
except IOError:
self.logger.debug("{:<21} | Image BMP compression not support, converting".format(""))
self.convert_ico_files(icon_file_path,"tmpimage.png")
image = Image.open("tmpimage.png")
os.remove("tmpimage.png")
image = image.convert("RGBA")
(width, height) = image.size
rgb_values = list(image.getdata())
rgb_values = list(image.getdata())
svgdoc = svgwrite.Drawing(filename = target_folder + svg_name,
size = ("{0}px".format(width * self.squaresize),
"{0}px".format(height * self.squaresize)))
rectangle_size = ("{0}px".format(self.squaresize + self.overlap),
"{0}px".format(self.squaresize + self.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 * self.squaresize),
"{0}px".format(rowcount * self.squaresize))
rectangle_fill = svgwrite.rgb(rgb_tuple[0], rgb_tuple[1], rgb_tuple[2])
# Use CSS classes as a workaround for missing selectSameFillColor in Inkscape 1.2 and above
rectangle_class = "r" + str(rgb_tuple[0]) + "-" + str(rgb_tuple[1]) + "-" + str(rgb_tuple[2]) + "-" + str(rgb_tuple[3])
alpha = rgb_tuple[3]
if alpha == 255:
svgdoc.add(svgdoc.rect(insert = rectangle_posn,
size = rectangle_size,
fill = rectangle_fill,
class_ = rectangle_class))
else:
svgdoc.add(svgdoc.rect(insert = rectangle_posn,
size = rectangle_size,
fill = rectangle_fill,
opacity = alpha/float(255,
class_ = rectangle_class)))
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']
rect_class = rect.attrib['class']
# Add both the rectangle ID and and rectangle class in an array case of Inkscape version issues
if rgb not in rgbs:
rgbs[rgb] = [rect_id, rect_class]
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))
# this is a bit of a hack to support both version of inkscape
if int(self.inkscape_info.version[0]) < 1:
self.logger.debug("{:<21} | Using Inkscape v0.9x command".format(''))
# Works with version 0.9x
args = [
self.inkscape_info.path,
"-l", svg_out, svg_in
]
else:
self.logger.debug("{:<21} | Using Inkscape v1.0 command".format(''))
#works with version 1.0
args = [
self.inkscape_info.path,
"-l", "-o", svg_out, svg_in
]
subprocess.check_call(args, stderr=subprocess.DEVNULL ,stdout=subprocess.DEVNULL)
def fix_with_inkscape(self, color, tmpfile):
self.logger.debug("{:<21} | Combining {} in {}".format("",color, tmpfile))
if int(self.inkscape_info.version[0]) > 0:
#The --verb option was removed from Inkscape 1.2, so versions newer than 1.1 must use the --actions command instead
if int(self.inkscape_info.version[1]) > 1:
args = [
self.inkscape_info.path,
"--batch-process",
"--actions",
"select-by-selector:."+color[1]+";object-to-path;path-union;export-overwrite:1;export-plain-svg:1;export-filename:"+tmpfile+";export-do;",
tmpfile
]
print(" ".join(args))
else:
args = [
self.inkscape_info.path,
"-g",
"--select="+color[0],
"--verb", "EditSelectSameFillColor;SelectionCombine;SelectionUnion;FileSave;FileQuit",
tmpfile
]
else:
args = [
self.inkscape_info.path,
"--select="+color[0],
"--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))
size = str(size)
if int(self.inkscape_info.version[0]) < 1:
args = [
self.inkscape_info.path,
"--without-gui",
"-f", svg_in,
"--export-area-page",
"-w", size,
"-h", size,
"--export-png=" + png_out
]
else:
args = [
self.inkscape_info.path,
"--export-area-page",
"--export-type=png",
"-w", size,
"-h", size,
"-o", png_out,
svg_in
]
subprocess.check_call(args, stderr=subprocess.DEVNULL ,stdout=subprocess.DEVNULL)
def get_inkscape_info(self):
inkscape_path = subprocess.check_output(["which", "inkscape"]).strip().decode()
inkscape_version_cmd = subprocess.check_output([inkscape_path, "--version"])
inkscape_version = inkscape_version_cmd.splitlines()[0].split()[1].decode().split(".")[0:2]
self.inkscape_info = inkscape_info(inkscape_path, inkscape_version)
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']:
try:
# If we're using a VM the wallpaper is different
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")
except:
self.xfconf_query('xfce4-desktop', '/backdrop/screen0/monitor0/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/monitor0/workspace0/image-style', "2")
elif self.theme_config['wallpaper']['theme_wallpaper']['wallpaperstyle'] == 2:
self.xfconf_query('xfce4-desktop', '/backdrop/screen0/monitor0/workspace0/image-style', "3")
else:
self.xfconf_query('xfce4-desktop', '/backdrop/screen0/monitor0/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",
"lr 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:
try:
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]
except subprocess.CalledProcessError:
self.logger.info("xfconf not installed, enable theme manually")
return
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):
try:
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)
except subprocess.CalledProcessError:
self.logger.info("xfconf not installed, enable theme manually")
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
class inkscape_info:
def __init__(self, path, version):
self.path = path
self.version = version