This repository has been archived on 2023-03-25. You can view files and clone it, but cannot push or open issues or pull requests.

196 lines
6.4 KiB

#!/usr/bin/env python3
# pylint: disable=too-many-ancestors
# standard library
import os
import sys
import re
import argparse
from shutil import copy2
# from subprocess import Popen, PIPE
# import time
# from lxml import etree
# local library
import inkex
from inkex.command import inkscape
from inkex.elements import _selected as selection
MIN_PYTHON_VERSION = (3, 6) # Mainly for f-strings
if (sys.version_info.major, sys.version_info.minor) < (3, 6):
inkex.Effect.msg(f"Python {MIN_PYTHON_VERSION[0]}.{MIN_PYTHON_VERSION[1]} or later required.")
class BaseExtension(inkex.Effect):
"""Custom class that makes creation of extensions easier.
Users of this class need not worry about boilerplates, such as how to
call inkscape via shell, and the management of tempfiles. Useful functions
are also provided."""
def __init__(self, custom_effect, args_adder=None):
"""Init base class.
In a typical Inkscape extension that does not make use of BaseExtension,
the effect is determined by the "effect" method of the extension class.
This init function will take in a method, and run it in the "effect" method
together with the other boilerplate.
This init method takes in a function under the custom_effect argument.
This function will handle the user's effects, minus the boilerplate. It
has to return a list[str] object, with each str being a verb that inkscape
can execute."""
self.custom_effect = custom_effect
self._msg = self.msg # The old msg function provided by inkex (only accepts strings)
def msg(*args, sep=' '):
"""Improved msg method, similar to Python's print"""
self._msg(sep.join([str(arg) for arg in args]))
self.msg = msg
if args_adder is not None:
self.args_adder = args_adder
def z_sort(self, alist):
"""Return new list sorted in document order (depth-first traversal)."""
return list(self.z_iter(alist))
def z_iter(self, alist):
"""Return iterator over ids in document order (depth-first traversal)."""
id_list = list(alist)
count = len(id_list)
for element in self.document.getroot().iter():
# element_id = element.get('id')
# if element_id is not None and element_id in id_list:
if element in alist:
yield element
count -= 1
if not count:
def show(obj):
"""Returns a str representation of object"""
def rep(obj):
if hasattr(obj, 'get_id'):
return f"{type(obj).__name__}({obj.get_id()})"
return f"{type(obj).__name__}"
if type(obj).__name__ == 'ElementList':
return ('ElementList(' +
', '.join([rep(child) for child in obj.values()]) +
if isinstance(obj, list):
return '[' + ', '.join(rep(child) for child in obj) + ']'
return rep(obj)
def find(self, obj: any, xpath='/*') -> list:
"""Returns a list of objects which satisfies XPath
obj (any): Parent object to recurse into. Examples include root, selected, or a group.
xpath (str, optional): Defaults to '/*'.
list: [description]
BASIC_TAGS = ('circle', 'ellipse', 'line', 'polygon', 'polyline', 'rect', 'path', 'image', 'g')
'l': "svg:g[@inkscape:groupmode='layer']",
'p': 'svg:path'
xpath = re.sub(r'((?<=/)(' + '|'.join(BASIC_TAGS) + r')\b)', r'svg:\1', xpath)
for k, v in SPECIAL_TAGS.items():
xpath = re.sub('(?<=/)' + k + r'\b', v, xpath)
xpath = re.sub(r'(?<=\[)(\d+):(\d+)(?=\])', r'position()>=\1 and position()<\2', xpath)
if type(obj).__name__ != 'ElementList':
obj = [obj]
output = []
for child in obj:
matches = child.xpath(xpath, namespaces={
'svg': '',
'inkscape': ''})
for match in matches:
if type(match).__name__ not in ('Defs', 'NamedView', 'Metadata'):
return output
def effect(self):
"""Main entry point to process current document. Not to be called externally."""
actions_list = self.custom_effect(self)
if actions_list is None or actions_list == []:
self.msg("No actions received. Perhaps you are calling inkex object methods?")
elif isinstance(actions_list, list):
tempfile = self.options.input_file + "-BaseExtension.svg"
# prepare
copy2(self.options.input_file, tempfile)
#disabled because it leads to crash Inkscape:
#extra_param = "--with-gui"
#workaround to fix it (we use export to tempfile instead processing and saving again)
extra_param = "--batch-process"
actions = ";".join(actions_list)
inkscape(tempfile, extra_param, actions=actions)
# finish up
# replace current document with content of temp copy file
self.document = inkex.load_svg(tempfile)
# update self.svg
self.svg = self.document.getroot()
# Clean up tempfile
except Exception: # pylint: disable=broad-except
def call(self, child, ext_options):
"""Used to call an extension from another extension"""
old_options = self.options
parser = argparse.ArgumentParser()
self.options = parser.parse_args([])
for k, v in ext_options.items():
setattr(self.options, k, v)
output = child.custom_effect(self)
self.options = old_options
return output