added batch task extension
This commit is contained in:
parent
fa165a5717
commit
f57c056ddd
195
extensions/fablabchemnitz/batch_task/BaseExtension.py
Normal file
195
extensions/fablabchemnitz/batch_task/BaseExtension.py
Normal 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
|
177
extensions/fablabchemnitz/batch_task/batch_task.inx
Normal file
177
extensions/fablabchemnitz/batch_task/batch_task.inx
Normal 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>
|
152
extensions/fablabchemnitz/batch_task/batch_task.py
Normal file
152
extensions/fablabchemnitz/batch_task/batch_task.py
Normal 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='/*')
|
Reference in New Issue
Block a user