211 lines
6.9 KiB
Python
211 lines
6.9 KiB
Python
|
"""
|
||
|
Created on 09/08/2011
|
||
|
|
||
|
@author: mikel
|
||
|
"""
|
||
|
import os
|
||
|
import shutil
|
||
|
import xml.etree.ElementTree as ET
|
||
|
|
||
|
import xbmc
|
||
|
from . import SkinUtilsError, check_skin_writability, reload_skin, try_remove_file, case_file_exists, \
|
||
|
DocumentCache, get_logger
|
||
|
|
||
|
|
||
|
class FontXmlError(SkinUtilsError):
|
||
|
pass
|
||
|
|
||
|
|
||
|
class FontManager:
|
||
|
__installed_names = None
|
||
|
__installed_fonts = None
|
||
|
__doc_cache = None
|
||
|
|
||
|
def _list_skin_font_files(self):
|
||
|
font_xml_list = []
|
||
|
skin_path = xbmc.translatePath("special://skin/")
|
||
|
|
||
|
# Go into each dir. Could be 720, 1080...
|
||
|
for dir_item in os.listdir(skin_path):
|
||
|
dir_path = os.path.join(skin_path, dir_item)
|
||
|
if os.path.isdir(dir_path):
|
||
|
# Try with font.xml
|
||
|
file = os.path.join(dir_path, "font.xml")
|
||
|
if case_file_exists(file):
|
||
|
font_xml_list.append(file)
|
||
|
|
||
|
# Don't try the next step on windows, wasted time
|
||
|
file = os.path.join(dir_path, "Font.xml")
|
||
|
if case_file_exists(file):
|
||
|
font_xml_list.append(file)
|
||
|
|
||
|
return font_xml_list
|
||
|
|
||
|
def __init__(self):
|
||
|
self.__installed_names = []
|
||
|
self.__installed_fonts = []
|
||
|
self.__doc_cache = DocumentCache()
|
||
|
|
||
|
# Check if the environment is sane
|
||
|
check_skin_writability()
|
||
|
|
||
|
# Initialize the doc cache with the skin's files
|
||
|
for file in self._list_skin_font_files():
|
||
|
self.__doc_cache.add(file)
|
||
|
|
||
|
def is_name_installed(self, name):
|
||
|
return name in self.__installed_names
|
||
|
|
||
|
def is_font_installed(self, file):
|
||
|
return file in self.__installed_fonts
|
||
|
|
||
|
def _get_font_attr(self, node, name):
|
||
|
attrnode = node.find(name)
|
||
|
if attrnode is not None:
|
||
|
return attrnode.text
|
||
|
|
||
|
def _copy_font_file(self, file):
|
||
|
skin_font_path = xbmc.translatePath("special://skin/fonts/")
|
||
|
file_name = os.path.basename(file)
|
||
|
dest_file = os.path.join(skin_font_path, file_name)
|
||
|
|
||
|
# TODO: Unix systems could use symlinks
|
||
|
|
||
|
# Check if it's already there
|
||
|
if dest_file not in self.__installed_fonts:
|
||
|
self.__installed_fonts.append(dest_file)
|
||
|
|
||
|
# Overwrite if file exists
|
||
|
shutil.copyfile(file, dest_file)
|
||
|
|
||
|
def _add_font_attr(self, fontdef, name, value):
|
||
|
attr = ET.SubElement(fontdef, name)
|
||
|
attr.text = value
|
||
|
attr.tail = "\n\t\t\t"
|
||
|
return attr
|
||
|
|
||
|
def _install_font_def(self, skin_file, name, filename, size, style="", aspect="", linespacing=""):
|
||
|
# Add it to the registry
|
||
|
self.__installed_names.append(name)
|
||
|
|
||
|
# Get the parsed skin font file
|
||
|
font_doc = self.__doc_cache.read(skin_file)
|
||
|
|
||
|
# Iterate over all the fontsets on the file
|
||
|
for fontset in font_doc.getroot().findall("fontset"):
|
||
|
fontset.findall("font")[-1].tail = "\n\t\t"
|
||
|
fontdef = ET.SubElement(fontset, "font")
|
||
|
fontdef.text, fontdef.tail = "\n\t\t\t", "\n\t"
|
||
|
|
||
|
self._add_font_attr(fontdef, "name", name)
|
||
|
|
||
|
# We get the full file path to the font, so let's basename
|
||
|
self._add_font_attr(fontdef, "filename", os.path.basename(filename))
|
||
|
self._copy_font_file(filename)
|
||
|
|
||
|
last = self._add_font_attr(fontdef, "size", size)
|
||
|
|
||
|
if style:
|
||
|
if style in ["normal", "bold", "italics", "bolditalics", "lighten"]:
|
||
|
last = self._add_font_attr(fontdef, "style", style)
|
||
|
|
||
|
else:
|
||
|
raise FontXmlError(
|
||
|
"Font '%s' has an invalid style definition: %s"
|
||
|
% (name, style)
|
||
|
)
|
||
|
|
||
|
if aspect:
|
||
|
last = self._add_font_attr(fontdef, "aspect", aspect)
|
||
|
|
||
|
if linespacing:
|
||
|
last = self._add_font_attr(fontdef, "linespacing", linespacing)
|
||
|
|
||
|
last.tail = "\n\t\t"
|
||
|
|
||
|
def _install_file(self, doc_cache, user_file, skin_file, font_path):
|
||
|
user_doc = doc_cache.read(user_file)
|
||
|
|
||
|
# Handle only the first fontset
|
||
|
fontset = user_doc.getroot().find("fontset")
|
||
|
if len(fontset):
|
||
|
# Every font definition inside it
|
||
|
for item in fontset.findall("font"):
|
||
|
name = self._get_font_attr(item, "name")
|
||
|
|
||
|
# Basic check for malformed defs.
|
||
|
if name is None:
|
||
|
raise FontXmlError("Malformed XML: No name for font definition.")
|
||
|
|
||
|
# Omit already defined fonts
|
||
|
elif not self.is_name_installed(name):
|
||
|
font_file_path = os.path.join(
|
||
|
font_path, self._get_font_attr(item, "filename")
|
||
|
)
|
||
|
self._install_font_def(
|
||
|
skin_file,
|
||
|
name,
|
||
|
font_file_path,
|
||
|
self._get_font_attr(item, "size"),
|
||
|
self._get_font_attr(item, "style"),
|
||
|
self._get_font_attr(item, "aspect"),
|
||
|
self._get_font_attr(item, "linespacing")
|
||
|
)
|
||
|
|
||
|
def _get_res_folder(self, path):
|
||
|
return os.path.basename(os.path.dirname(path))
|
||
|
|
||
|
def _get_res_filename(self, res_folder, user_file):
|
||
|
path, ext = os.path.splitext(user_file)
|
||
|
return path + '-' + res_folder + ext
|
||
|
|
||
|
def install_file(self, user_file, font_path, commit=True, clear=True):
|
||
|
doc_cache = DocumentCache()
|
||
|
|
||
|
# If the file does not exist the following will fail
|
||
|
doc_cache.add(user_file)
|
||
|
|
||
|
# Install the file into every cached skin font file
|
||
|
for skin_file in self.__doc_cache.list_files():
|
||
|
res_folder = self._get_res_folder(skin_file)
|
||
|
res_file = self._get_res_filename(res_folder, user_file)
|
||
|
|
||
|
# If an specific res file exists...
|
||
|
if os.path.isfile(res_file):
|
||
|
self._install_file(doc_cache, res_file, skin_file, font_path)
|
||
|
|
||
|
# Otherwise use the dafault fallback
|
||
|
else:
|
||
|
self._install_file(doc_cache, user_file, skin_file, font_path)
|
||
|
|
||
|
# If save was requested
|
||
|
if commit:
|
||
|
self.__doc_cache.write_all()
|
||
|
|
||
|
# Clear cached docs after write (if requested)
|
||
|
if clear:
|
||
|
self.__doc_cache.clear_all()
|
||
|
|
||
|
def remove_font(self, name):
|
||
|
pass
|
||
|
|
||
|
def remove_installed_names(self):
|
||
|
self.__doc_cache.rollback_all()
|
||
|
|
||
|
def remove_installed_fonts(self):
|
||
|
for item in self.__installed_fonts:
|
||
|
if not try_remove_file(item):
|
||
|
get_logger().error(
|
||
|
'Failed removing font file "%s". XBMC may still be using it.' % item
|
||
|
)
|
||
|
|
||
|
def cleanup(self):
|
||
|
self.remove_installed_names()
|
||
|
|
||
|
# Reload skin so font files are no longer in use, and then delete them
|
||
|
reload_skin()
|
||
|
self.remove_installed_fonts()
|
||
|
|
||
|
def __del__(self):
|
||
|
self.cleanup()
|