You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Inkscape扩展处理多组Union操作异常:问题及修复咨询

问题描述

开发了一款Inkscape扩展,目标是对每个选中的组执行Union操作,但运行后结果异常:

  • 操作场景:选中三个矩形组
  • 异常表现:仅第一个组被正确转为Union路径,其余组出现重复且位置偏移
  • 预期效果:每个组独立完成Union,生成三个独立的Union路径

扩展代码(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()

对应的.inx文件

<?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>
解决方案

可以实现预期效果,核心问题是每次处理组时都基于整个文档操作,导致后续组处理时使用已被修改的文档,出现偏移和重复。以下是修复后的完整代码:

修复后的Python代码

import inkex
import tempfile, os, shutil
from uuid import uuid4
from inkex import Group, Circle, Ellipse, Line, PathElement, Polygon, Polyline, Rectangle, Use
from lxml import etree

def process_group_union(group):
    # 创建仅包含当前组的临时SVG文档
    temp_svg = inkex.SvgDocument()
    # 复制组到临时SVG中,保留原始属性和子元素
    group_copy = etree.fromstring(etree.tostring(group))
    temp_svg.append(group_copy)

    temp_folder = tempfile.mkdtemp()
    svg_temp_filepath = os.path.join(temp_folder, f'temp_group_{str(uuid4())}.svg')
    processed_svg_filepath = os.path.join(temp_folder, f'processed_group_{str(uuid4())}.svg')

    # 保存临时SVG
    with open(svg_temp_filepath, 'w') as f:
        f.write(temp_svg.tostring().decode('utf-8'))

    # 构建命令:选中组内所有元素,执行Union,导出
    action_string = (
        f'select-by-id:{group_copy.get_id()};select-invert;delete;'  # 只保留当前组内容
        f'select-all;path-union;select-clear;'
    )
    export_actions = f'--actions=export-filename:{processed_svg_filepath};{action_string}export-do;'
    inkex.command.inkscape(svg_temp_filepath, export_actions)

    # 加载处理后的SVG
    processed_svg = inkex.elements.load_svg(processed_svg_filepath).getroot()
    shutil.rmtree(temp_folder)

    # 提取Union后的路径元素
    union_path = None
    for elem in processed_svg.iter():
        if isinstance(elem, PathElement):
            union_path = elem
            break
    return union_path

class UnionizeEachGroupMembers(inkex.EffectExtension):
    def add_arguments(self, pars):
        pars.add_argument("--selection_type_radio", type=str, dest="selection_type_radio", default='selected')

    def effect(self):
        SELECTION_TYPE = self.options.selection_type_radio

        # 获取目标组列表
        if SELECTION_TYPE == 'all':
            groups = self.svg.findall('.//svg:g', namespaces=self.svg.nsmap)
        else:
            groups = [elem for elem in self.svg.selected.values() if isinstance(elem, Group)]

        if not groups:
            inkex.errormsg('未找到可处理的组,请至少选择一个组')
            return

        for group in groups:
            # 筛选组内可执行Union的元素
            valid_elements = [child for child in group if isinstance(child, (Circle, Ellipse, Line, PathElement, Polygon, Polyline, Rectangle, Use))]
            if len(valid_elements) < 2:
                continue  # 至少需要两个元素才能执行Union

            # 执行Union操作
            union_path = process_group_union(group)
            if not union_path:
                continue

            # 清空原组内容,添加Union后的路径
            for child in list(group):
                group.remove(child)
            # 复制Union路径的属性和内容到原组
            group.append(union_path)
            # 继承原组的变换属性
            if group.get('transform'):
                union_path.set('transform', group.get('transform'))
                group.attrib.pop('transform')

if __name__ == '__main__':
    UnionizeEachGroupMembers().run()

关键改动说明

  1. 独立处理单个组:为每个组创建单独的临时SVG,避免处理过程中影响其他组的原始状态
  2. 修正命令逻辑:在临时SVG中仅保留当前组,确保Union操作只针对该组内元素
  3. 优化组内容替换:准确提取Union后的路径元素,替换原组内容,同时继承原组的变换属性,避免位置偏移
  4. 调整选择逻辑:直接筛选目标组,减少冗余的元素遍历,同时增加元素数量判断(至少两个元素才执行Union)
  5. 修复提示文字:将英文提示改为中文,更符合使用习惯

内容的提问来源于stack exchange,提问作者Bayu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 16:47:02