I made an extension to perform union operation on each selected group, but I got a weird result.
For example, there are three selected groups of rectangles:
After using the extension on them, only the first group gets converted into a union path, the rest get duplicated and they aren't in their original positions anymore:
The expected result should be three union paths:
Here's the extension (save it as "union_operation_on_each_selected_group.py"):
import inkex
import tempfile, os, shutil
from uuid import uuid4
from inkex import Group, Circle, Ellipse, Line, PathElement, Polygon, Polyline, Rectangle, Use
from inkex.paths import Path
from inkex.command import call
from math import ceil
from lxml import etree
def process_svg(svg, action_string):
temp_folder = tempfile.mkdtemp()
# Create a random filename for svg
svg_temp_filepath = os.path.join(temp_folder, f'original_{str(uuid4())}.svg')
with open(svg_temp_filepath, 'w') as output_file:
svg_string = svg.tostring().decode('utf-8')
output_file.write(svg_string)
processed_svg_temp_filepath = os.path.join(temp_folder, f'processed_{str(uuid4())}.svg')
my_actions = '--actions='
export_action_string = my_actions + f'export-filename:{processed_svg_temp_filepath};{action_string}export-do;'
# Run the command line
cmd_selection_list = inkex.command.inkscape(svg_temp_filepath, export_action_string)
# Replace the current document with the processed document
with open(processed_svg_temp_filepath, 'rb') as processed_svg:
loaded_svg = inkex.elements.load_svg(processed_svg).getroot()
shutil.rmtree(temp_folder)
return loaded_svg
def element_list_union(svg, element_list):
group = None
action_string = ''
for element in element_list:
group = element.getparent()
action_string = action_string + f'select-by-id:{element.get_id()};'
action_string = action_string + f'path-union;select-clear;'
processed_svg = process_svg(svg, action_string)
to_remove = []
for child in group:
to_remove.append(child)
for item in to_remove:
group.remove(item)
for elem in processed_svg:
group.add(elem)
class UnionizeEachGroupMembers(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument("--selection_type_radio", type=str, dest="selection_type_radio", default='all')
def effect(self):
SELECTION_TYPE = self.options.selection_type_radio
if SELECTION_TYPE == 'all':
selection_list = self.svg.descendants()
else:
selection_list = self.svg.selected
# Filter for only shapes
selected_elements = selection_list.filter(inkex.ShapeElement)
if len(selected_elements) < 1:
inkex.errormsg('Please select at least one object / No Objects found')
return
for elem in selected_elements:
if elem.tag == inkex.addNS('g', 'svg'): # Check if the tag is 'g' in the SVG namespace
group = elem
group_id = group.get_id()
elements_to_union = [child for child in group if isinstance(child, (Circle, Ellipse, Line, PathElement, Polygon, Polyline, Rectangle, Use))]
if len(elements_to_union) > 0:
element_list_union(self.svg, elements_to_union)
if __name__ == '__main__':
UnionizeEachGroupMembers().run()
And here's the .inx file:
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Union Operation on Each Selected Group</name>
<id>union_operation_on_each_selected_group</id>
<!-- Parameters Here -->
<param name="selection_type_radio" type="optiongroup" appearance="radio" gui-text="Selection Type">
<option value="selected">Selected Objects</option>
<option value="all">All Objects</option>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<!-- <submenu name="Submenu Name"/>-->
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">union_operation_on_each_selected_group.py</command>
</script>
</inkscape-extension>
You can download the example file here:
Is it possible to get the expected result using an extension? How could I fix the extension?
Problem is because it exports all elements to processed svg and later it puts all elements in group.
I had to use select-invert
to invert selection and delete
to delete other elements, and later select-all
to select again previous elements to run path-union
. This way it exports only one object. Or rather 3 elements: group with path (create by path-union
)(I don't know why it create extra group) and two other elements but they are not displayed but I don't know what it is.
Full working code:
For test I used temporary folder with prefix inkex_
and skiped shutil.rmtree()
so I could easily check what it wrote in proccesed file.
I also first create list with all commands and later convert them to one string using
action_string = ";".join(action_commands)
. It allowed me to add new commands in easy way.
(I tested also export-id
and export-id-only
but it doesn't export page size and later objects are in wrong place)
And old elements can be removed from group using one command group.clear()
I was thinking to add elements to group.getparent()
instead of group
because currently it creates two nested groups - original group (in which were selected items) and extra group from processed file.
import inkex
import tempfile, os, shutil
from uuid import uuid4
from inkex import Group, Circle, Ellipse, Line, PathElement, Polygon, Polyline, Rectangle, Use
from inkex.paths import Path
from inkex.command import call
from math import ceil
from lxml import etree
msg = inkex.errormsg # to have shorter command to write
def process_svg(svg, action_commands):
temp_folder = tempfile.mkdtemp(prefix='inkex_')
# --- keep original ---
# Create a random filename for svg
original_svg_temp_filepath = os.path.join(temp_folder, f'original_{str(uuid4())}.svg')
msg(f"{original_svg_temp_filepath = }")
with open(original_svg_temp_filepath, 'w') as output_file:
svg_string = svg.tostring().decode('utf-8')
output_file.write(svg_string)
# --- create new ---
processed_svg_temp_filepath = os.path.join(temp_folder, f'processed_{str(uuid4())}.svg')
action_commands.append(f'export-filename:{processed_svg_temp_filepath}')
action_commands.append('export-do')
action_string = ";".join(action_commands)
my_actions = f'--actions={action_string}'
msg(f"{my_actions = }")
# Run the command line
cmd_selection_list = inkex.command.inkscape(original_svg_temp_filepath, my_actions)
# --- load new ---
# Replace the current document with the processed document
with open(processed_svg_temp_filepath, 'rb') as processed_svg:
loaded_svg = inkex.elements.load_svg(processed_svg).getroot()
#shutil.rmtree(temp_folder)
return loaded_svg
def element_list_union(svg, element_list):
msg(f"{len(element_list) = }")
group = None
action_commands = []
for element in element_list:
group = element.getparent()
#msg(f"{group = }")
action_commands.append(f'select-by-id:{element.get_id()}')
action_commands.append('select-invert')
action_commands.append('delete')
action_commands.append('select-all')
action_commands.append('path-union')
#action_commands.append('select-clear')
processed_svg = process_svg(svg, action_commands)
#msg(f"{len(group) = }")
group.clear()
#msg(f"{len(group) = }")
msg(f"{len(processed_svg) = }")
for elem in processed_svg:
#msg(f"{elem = }")
group.add(elem)
#group.getparent().add(elem)
class UnionizeEachGroupMembers(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument("--selection_type_radio", type=str, dest="selection_type_radio", default='all')
def effect(self):
SELECTION_TYPE = self.options.selection_type_radio
if SELECTION_TYPE == 'all':
selection_list = self.svg.descendants()
else:
selection_list = self.svg.selected
msg(selection_list)
# Filter for only shapes
selected_elements = selection_list.filter(inkex.ShapeElement)
if len(selected_elements) < 1:
inkex.errormsg('Please select at least one object / No Objects found')
return
for elem in selected_elements:
if elem.tag == inkex.addNS('g', 'svg'): # Check if the tag is 'g' in the SVG namespace
group = elem
group_id = group.get_id()
elements_to_union = [child for child in group if isinstance(child, (Circle, Ellipse, Line, PathElement, Polygon, Polyline, Rectangle, Use))]
if len(elements_to_union) > 0:
element_list_union(self.svg, elements_to_union)
if __name__ == '__main__':
UnionizeEachGroupMembers().run()
Extra information if someone would be interested.
List of avaliable commands: inkscape --action-list
There is shell inkscape --shell
but I didn't test it.