added batch task extension

This commit is contained in:
Mario Voigt 2021-07-04 12:33:33 +02:00
parent fa165a5717
commit f57c056ddd
3 changed files with 524 additions and 0 deletions

View File

@ -0,0 +1,195 @@
#!/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.")
sys.exit(1)
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."""
inkex.Effect.__init__(self)
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:
args_adder(self.arg_parser)
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:
id_list.remove(element)
yield element
count -= 1
if not count:
return
@staticmethod
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
Args:
obj (any): Parent object to recurse into. Examples include root, selected, or a group.
xpath (str, optional): Defaults to '/*'.
Returns:
list: [description]
"""
BASIC_TAGS = ('circle', 'ellipse', 'line', 'polygon', 'polyline', 'rect', 'path', 'image', 'g')
SPECIAL_TAGS = {
'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': 'http://www.w3.org/2000/svg',
'inkscape': 'http://www.inkscape.org/namespaces/inkscape'})
for match in matches:
if type(match).__name__ not in ('Defs', 'NamedView', 'Metadata'):
output.append(match)
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: https://gitlab.com/inkscape/inkscape/-/issues/2487
#actions_list.append("FileSave")
#actions_list.append("FileQuit")
#extra_param = "--with-gui"
#workaround to fix it (we use export to tempfile instead processing and saving again)
actions_list.append("export-type:svg")
actions_list.append("export-filename:{}".format(tempfile))
actions_list.append("export-do")
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
try:
os.remove(tempfile)
except Exception: # pylint: disable=broad-except
pass
def call(self, child, ext_options):
"""Used to call an extension from another extension"""
old_options = self.options
parser = argparse.ArgumentParser()
child.args_adder(parser)
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

View File

@ -0,0 +1,177 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Batch Task</name>
<id>fablabchemnitz.de.batch_task</id>
<param name="tab_main" type="notebook">
<page name="Options" gui-text="Options">
<param name="target" type="optiongroup" appearance="radio" gui-text="XPath upon:">
<option value="root">Entire document</option>
<option value="selected">Only selected objects</option>
</param>
<param name="xpath" type="string" gui-text="XPath:" />
<param name="tab_effect" type="notebook">
<page name="Preset" gui-text="Presets">
<param name="effect_preset1" type="optiongroup" appearance="combo" gui-text="Preset effect 1:">
<option value="">Do nothing</option>
<option value="EditDuplicate">EditDuplicate</option>
<option value="EditDelete">EditDelete</option>
<option value="SelectionGroup">SelectionGroup</option>
<option value="SelectionUnGroup">SelectionUnGroup</option>
<option value="SelectionRaise">SelectionRaise</option>
<option value="SelectionLower">SelectionLower</option>
<option value="SelectionToFront">SelectionToFront</option>
<option value="SelectionToBack">SelectionToBack</option>
<option value="org.inkscape.color.brighter">Brighter</option>
<option value="org.inkscape.color.darker">Darker</option>
</param>
<param name="effect_preset2" type="optiongroup" appearance="combo" gui-text="Preset effect 2:">
<option value="">Do nothing</option>
<option value="EditDuplicate">EditDuplicate</option>
<option value="EditDelete">EditDelete</option>
<option value="SelectionGroup">SelectionGroup</option>
<option value="SelectionUnGroup">SelectionUnGroup</option>
<option value="SelectionRaise">SelectionRaise</option>
<option value="SelectionLower">SelectionLower</option>
<option value="SelectionToFront">SelectionToFront</option>
<option value="SelectionToBack">SelectionToBack</option>
<option value="org.inkscape.color.brighter">Brighter</option>
<option value="org.inkscape.color.darker">Darker</option>
</param>
<param name="effect_preset3" type="optiongroup" appearance="combo" gui-text="Preset effect 3:">
<option value="">Do nothing</option>
<option value="EditDuplicate">EditDuplicate</option>
<option value="EditDelete">EditDelete</option>
<option value="SelectionGroup">SelectionGroup</option>
<option value="SelectionUnGroup">SelectionUnGroup</option>
<option value="SelectionRaise">SelectionRaise</option>
<option value="SelectionLower">SelectionLower</option>
<option value="SelectionToFront">SelectionToFront</option>
<option value="SelectionToBack">SelectionToBack</option>
<option value="org.inkscape.color.brighter">Brighter</option>
<option value="org.inkscape.color.darker">Darker</option>
</param>
</page>
<page name="Simple" gui-text="Simple">
<param name="effect_simple1" type="string" gui-text="Effect 1:" />
<param name="effect_simple2" type="string" gui-text="Effect 2:" />
<param name="effect_simple3" type="string" gui-text="Effect 3:" />
</page>
<page name="Multi" gui-text="Multi">
<param name="effect_multi" type="string" gui-text="Effects:" appearance="multiline" />
</page>
</param>
<!-- <param type="string" name="varname" gui-text="label" indent="1" max-length="5" appearance="multiline">some text</param> -->
<!-- <param name="param_str2" type="string" gui-text="Effects:" [max-length="5" | appearance="multiline"]></param> -->
<!-- <param type="string" name="varname" gui-text="label" [indent="1"] [max-length="5" | appearance="multiline"]>some text</param> -->
<param name="mode" type="optiongroup" appearance="radio" gui-text="Apply effects to:">
<option value="all">Entire selection</option>
<option value="indiv">Each object in selection</option>
</param>
</page>
<page name="Help" gui-text="Help">
<label xml:space="preserve">
This template provides extension writers with a basis to write their python based Inkscape extensions quickly and properly.
This testing help text can be changed to help users of the extension.
</label>
</page>
<page name="null_Reference" gui-text="Reference">
<!--REFERENCE START -->
<param name="null_notebook" type="notebook">
<page name="null_edit" gui-text="Edit">
<label xml:space="preserve">
EditCut
EditCopy
EditPaste</label>
<separator />
<label xml:space="preserve">EditDuplicate
EditClone
SelectionCreateBitmap</label>
<separator />
<label xml:space="preserve">EditDelete</label>
</page>
<page name="null_layer" gui-text="Layer">
<label xml:space="preserve">
LayerNew
LayerRename</label>
</page>
<page name="null_objects" gui-text="Objects">
<label xml:space="preserve">
SelectionGroup
SelectionUnGroup</label>
<separator />
<label xml:space="preserve">ObjectSetClipPath
ObjectUnSetClipPath
ObjectSetMask
ObjectUnSetMask</label>
<separator />
<label xml:space="preserve">SelectionRaise
SelectionLower
SelectionToFront
SelectionToBack</label>
</page>
<page name="null_objects_2" gui-text="Objects 2">
<label xml:space="preserve">
ObjectRotate90
ObjectRotate90CCW
ObjectFlipHorizontally
ObjectFlipVertically</label>
<separator />
<label xml:space="preserve">UnhideAll
UnhideAllInAllLayers
UnlockAll
UnlockAllInAllLayers</label>
</page>
<page name="null_path" gui-text="Path">
<label xml:space="preserve">
SelectionUnion
SelectionDiff
SelectionIntersect</label>
<separator />
<label xml:space="preserve">SelectionSymDiff
SelectionDivide
SelectionCutPath</label>
<separator />
<label xml:space="preserve">SelectionCombine
SelectionBreakApart</label>
<separator />
<label xml:space="preserve">SelectionInset
SelectionOffset
SelectionReverse</label>
</page>
<page name="null_text" gui-text="Text">
<label xml:space="preserve">
SelectionTextToPath
SelectionTextFromPath
SelectionTextRemoveKerns</label>
</page>
<page name="null_filters" gui-text="Filters">
<label xml:space="preserve">
Color
org.inkscape.color.brighter
org.inkscape.color.darker
org.inkscape.color.grayscale
org.inkscape.color.black_and_white</label>
</page>
<page name="null_extensions" gui-text="Extensions">
<label xml:space="preserve">
EffectLast</label>
</page>
</param>
<!-- REFERENCE END -->
</page>
</param>
<param name="dry_run" type="bool" gui-text="Dry run">true</param>
<effect>
<object-type>all</object-type>
<!--object-type>path</object-type-->
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Various"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">batch_task.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,152 @@
#!/usr/bin/env python3
import os
import sys
import re
import subprocess
from BaseExtension import BaseExtension
# For linting purposes
from argparse import ArgumentParser
"""If syntax error occurs here, change inkscape interpreter to python3"""
"""I have yet to find a way for an extension to call another extension with parameters,
without GUI. This extension can be run as part of a standalone extension (using BaseExtension)
or imported for use by another extension. This workaround is done via the 'option' arg in
the 'custom_effect' function"""
def custom_effect(self: BaseExtension):
"""Note: The init of the BaseExtension class will set its 'custom_effect' attr
to this function. Hence, the self arg is of type BaseExtension."""
selected = self.svg.selected
root = self.document.getroot()
actions_list = []
proc = subprocess.run("inkscape --verb-list | grep -oP '^.+?(?=:)'",
shell=True, capture_output=True)
valid_actions_and_verbs = proc.stdout.decode().splitlines()
proc = subprocess.run("inkscape --action-list | grep -oP '^.+?(?= *:)'",
shell=True, capture_output=True)
valid_actions_and_verbs += proc.stdout.decode().splitlines()
self.options.dry_run = self.options.dry_run == 'true'
def verify_action(action):
if ':' in action:
action = action.split(':')[0]
if action not in valid_actions_and_verbs:
raise ValueError(action)
def select_do_individually(objs, actions):
for obj in objs:
actions_list.append("EditDeselect")
actions_list.append("select-by-id:" + obj.get_id())
if isinstance(actions, str):
actions = [actions]
for action in actions:
verify_action(action)
actions_list.append(action)
def select_do_on_all(objs, actions):
for obj in objs:
actions_list.append("select-by-id:" + obj.get_id())
if isinstance(actions, str):
actions = [actions]
for action in actions:
verify_action(action)
actions_list.append(action)
effects = []
try:
if self.options.tab_effect is None:
if self.options.effects is not None:
self.options.tab_effect = 'Multi'
elif self.options.effect1 is not None:
self.options.tab_effect = 'Simple'
elif self.options.tab_effect in ('Preset', 'Simple'):
for attr in ('effect_' + self.options.tab_effect.lower() + str(i) for i in range(1, 4)):
e = getattr(self.options, attr)
if e != None:
effects += [e.strip()]
if effects == []:
raise ValueError
elif self.options.tab_effect == 'Multi':
if self.options.effects is None:
raise ValueError
for line in self.options.effects.split('\\n'):
effects += [e.strip() for e in line.split(';') if e != '']
except ValueError:
self.msg("No effects inputted! Quitting...")
sys.exit(0)
if self.options.target == 'root':
objects = self.find(root, '/svg:svg' + self.options.xpath)
elif self.options.target == 'selected':
objects = self.find(selected, self.options.xpath)
if objects == []:
self.msg(f"No objects satisfies XPath: '{self.options.xpath}'.")
self.msg("Root:", self.show(root))
self.msg("Selected:", self.show(selected))
sys.exit(0)
try:
if self.options.mode == 'all':
select_do_on_all(objects, effects)
elif self.options.mode == 'indiv':
select_do_individually(objects, effects)
except ValueError as e:
self.msg(f"'{e.args[0]}' is not a valid action or verb in inkscape.")
sys.exit(1)
if self.options.dry_run:
self.msg(f"{'DRY RUN':=^40}")
self.msg("Root:", self.show(self.find(root, '/*')))
self.msg("Selected:", self.show(selected))
self.msg()
self.msg("XPath:", self.show(objects))
self.msg()
self.msg("Actions:", actions_list)
sys.exit(0)
return actions_list
def args_adder(arg_parser: ArgumentParser):
arg_parser.add_argument("--target", default='root', help="Object to apply xpath find on")
arg_parser.add_argument("--xpath", default='/*', help="For selection of objects")
arg_parser.add_argument("--tab_main", default=None)
arg_parser.add_argument("--Simple", default=None)
arg_parser.add_argument("--Multi", default=None)
arg_parser.add_argument("--mode", default="all", help="Mode to apply effects on objects")
arg_parser.add_argument("--tab_effect", default=None)
for arg in (*(x + str(y) for x in ('effect_preset', 'effect_simple') for y in range(1, 4)), 'effects'):
arg_parser.add_argument(f"--{arg}", default=None, help="Inkscape verb for path op")
arg_parser.add_argument("--dry_run", default='false')
arg_parser.add_argument("--null_notebook", default='false')
#import inkex
#for key, value in arg_parser.parse_args()._get_kwargs():
# if value is not None:
# inkex.utils.debug("{}={}".format(key, value))
BatchTask = BaseExtension(custom_effect, args_adder=args_adder)
if __name__ == '__main__':
BatchTask.run()
# Namespace(Multi='SelectionDiff', Simple='SelectionDiff', dry_run='false', effect1='SelectionBreakApart', effect2=None, effect3=None, effects=None, ids=['image25'], input_file='/tmp/ink_ext_XXXXXX.svgIDCKU0', mode='all', null='null', output=<_io.BufferedWriter name='<stdout>'>, selected_nodes=[], tab_effect='Simple', tab_main='Options', target='root', xpath='/*')
# Namespace(Multi=None, Simple=None, dry_run='false', effect1='SelectionDelete', effect2=None, effect3=None, effects=None, mode='all', null='false', tab_effect=None, tab_main=None, target='root', xpath='/*')