diff --git a/README.md b/README.md index c6d9c64..b7b8c0d 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ the syntax: You can specify a background image with `--image/-im`, the supported image formats/extensions are PNG, JPG, JPEG, and TGA. This feature is considered *unfinished* because it does not yet work as well as it could *(see -[#57](https://github.com/mateosss/matter/pull/57))* +[#58](https://github.com/mateosss/matter/issues/58))* Here is an example of the syntax: diff --git a/matter.py b/matter.py index 6c5b409..f8ec6b2 100755 --- a/matter.py +++ b/matter.py @@ -1,5 +1,6 @@ #!/usr/bin/python3 +# Standard library modules import sys import os import re @@ -11,6 +12,9 @@ from subprocess import run, check_call, PIPE from shutil import which, rmtree, copytree, copyfile +# Local Matter modules +from svg2png import inkscape_convert_svg2png, magick_convert_svg2png + # Configuration constants MIN_PYTHON_VERSION = (3, 6) # Mainly for f-strings @@ -234,13 +238,17 @@ def is_icon_downloaded(icon_name): def convert_icon_svg2png(icon_name): - if not has_command("convert"): - error( - "Stop. The `convert` command from imagemagick was not found", - "Also consider installing `inkscape` for the best results", - ) if not has_command("inkscape"): - warning("Resulting icons could look a bit off, consider installing inkscape") + if not has_command("convert"): + error( + "Stop. Both `inkscape` and `convert` command from imagemagick was not found", + "Consider installing `inkscape` for the best results", + ) + else: + command = "convert" + else: + command = "inkscape" + color = ( parse_color(user_args.iconcolor) if user_args.iconcolor @@ -248,15 +256,16 @@ def convert_icon_svg2png(icon_name): ) src_path = ICON_SVG_PATHF.format(icon_name) dst_path = ICON_PNG_PATHF.format(icon_name) - command = ( - r"convert -trim -scale 36x36 -extent 72x72 -gravity center " - r"-define png:color-type=6 -background none -colorspace sRGB -channel RGB " - rf"-threshold -1 -density 300 -fill \{color} +opaque none " - rf"{src_path} {dst_path}" - ) - exit_code = sh(command) + + if command == "convert": + warning("Resulting icons could look a bit off, consider installing inkscape") + converter = magick_convert_svg2png + elif command == "inkscape": + converter = inkscape_convert_svg2png + + exit_code = converter(color, src_path, dst_path) if exit_code != 0: - error("Stop. The convert command returned an error") + error(f"Stop. The `{command}` command returned an error") def get_available_fonts(): @@ -551,7 +560,7 @@ def do_uninstall(): clean_hookcheck() clean_install_dir() update_grub_cfg() - info(f"{THEME_NAME} succesfully uninstalled") + info(f"{THEME_NAME} successfully uninstalled") def do_list_grub_cfg_entries(): @@ -605,7 +614,7 @@ def do_patch_grub_cfg_icons(): with open(GRUB_CFG_PATH, "w") as f: f.write(new_grub_cfg) - info(f"{len(icons)} icons succesfully patched onto {GRUB_CFG_PATH}") + info(f"{len(icons)} icons successfully patched onto {GRUB_CFG_PATH}") def do_set_icons(): @@ -628,7 +637,7 @@ def do_set_icons(): f.write(new_grub_mkconfig) info( - f"{GRUB_MKCONFIG_PATH} succesfully patched, icons will now persist between grub updates." + f"{GRUB_MKCONFIG_PATH} successfully patched, icons will now persist between grub updates." ) diff --git a/svg2png.py b/svg2png.py new file mode 100755 index 0000000..25b8d85 --- /dev/null +++ b/svg2png.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 + +import os +import re +import subprocess +import xml.etree.ElementTree as ET +import xml.dom.minidom + +def inkscape_convert_svg2png(color, src_path, dst_path): + SVG_URI = 'http://www.w3.org/2000/svg' + FRAC = 0.7 + TEMPFILE = 'temp.svg' + + def parse_with_map(source): + """Parses file, returns a tuple containing the parsed ElementTree and a namespace map (dict). + + The ElementTree object returned is the same as if parsed using xml.etree.ElementTree.parse. For + some reason, ElementTree objects by the xml package will not provide a namespace map, unlike the + lxml package. + """ + + root = None + ns_map = [] + for event, node in ET.iterparse(source, events=['start-ns', 'start']): + if event == 'start-ns': + ns_map.append(node) + elif event == 'start': + if root is None: + root = node + return (ET.ElementTree(root), dict(ns_map)) + + def int_ignore_units(s): + return int(''.join(ch for ch in s if ch.isdigit())) + + def prettify(xml_string): + return '\n'.join(line for line in xml.dom.minidom.parseString(xml_string).toprettyxml( + indent=' ').splitlines() if not line.isspace() and line != '') + + # Fixes undefined namespace tags in output xml (not a big issue) + dom, ns_map = parse_with_map(src_path) + for key, value in ns_map.items(): + ET.register_namespace(key, value) + + root = dom.getroot() + width, height = int_ignore_units(root.attrib['width']), int_ignore_units(root.attrib['height']) + width_gap, height_gap = (1-FRAC)*width/2, (1-FRAC)*height/2 + + # Group all elements that are children of while changing their 'style' attributes + elements = root.findall('svg:*', namespaces={'svg': SVG_URI}) + group = ET.SubElement(root, 'g') + for element in elements: + if any(map(element.tag.endswith, ['defs', 'metadata'])): # Don't group these special tags + continue + root.remove(element) + for child in element.iter(): # Changes all decendents (.iter will also include itself) + if 'style' in child.attrib: + child.attrib['style'] = re.sub(r'(?<=fill:)\S+?(?=;)', color, child.attrib['style']) + else: + child.attrib['style'] = f'fill:{color};' + group.append(element) + # Shrink the svg by a factor of FRAC for padding around icon + group.attrib['transform'] = f"matrix({FRAC},0,0,{FRAC},{width_gap},{height_gap})" + + xml_string = ET.tostring(root).decode() + xml_string = prettify(xml_string) + with open(TEMPFILE, 'w') as f: + f.write(xml_string) + + + # Check inkscape version + version_string = subprocess.run('inkscape --version 2>/dev/null', + shell=True, capture_output=True).stdout.decode() + version = re.findall(r'(?<=Inkscape )[.\d]+', version_string)[0] + if version[0] == '1': + arguments = [f'--export-filename={dst_path}'] + elif version[0] == '0': + arguments = ['--without-gui', f'--export-png={dst_path}'] + arguments.extend([TEMPFILE, '-w', '72']) + + inkscape_proc = subprocess.Popen(['inkscape', *arguments], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + subprocess.run(['sed', 's/^/inkscape: /'], stdin=inkscape_proc.stdout) + inkscape_proc.wait() + exit_code = inkscape_proc.returncode + # Alternative - Using bash to pipe + # cmd = f"""/bin/bash -c 'inkscape {' '.join(arguments)} 2>&1 | sed "s/^/inkscape: /"; exit ${{PIPESTATUS[0]}}'""" + # exit_code = os.system(cmd) + + os.remove(TEMPFILE) + return exit_code + +def magick_convert_svg2png(color, src_path, dst_path): + cmd = ( + r"convert -trim -scale 36x36 -extent 72x72 -gravity center " + r"-define png:color-type=6 -background none -colorspace sRGB -channel RGB " + rf"-threshold -1 -density 300 -fill \{color} +opaque none " + rf"{src_path} {dst_path}" + ) + return os.system(cmd) + +# For demostration purposes +if __name__ == '__main__': + svg2png = inkscape_convert_svg2png + # svg2png = magick_convert_svg2png + for file in os.listdir('./icons'): + basename, ext = os.path.splitext(file) + if ext == '.svg': + svg2png('#FFFFFF', f'icons/{basename}.svg', f'icons/{basename}.png') diff --git a/theme.txt.template b/theme.txt.template index 1eb932d..fc0d6e4 100644 --- a/theme.txt.template +++ b/theme.txt.template @@ -2,7 +2,7 @@ # already parsed and the comments below could not make too much sense. # theme.txt.template represents a python string that gets format()-ed -# Note: for scaping literal curly braces, double them like so: {{ or }} +# Note: for escaping literal curly braces, double them like so: {{ or }} # {theme_name} Theme File # Designed for any resolution