SVG to Painting Assistant

Thanks Takiro for taking the time to check it out, if there is an alternative way of structuring the code so things like doc.activeLayer do not fail inbetween actions that create/remove layers I’d not bother with this nested function nonsense :joy: (and timers) It’s just trying to get around that limitation in the scripter, if it isn’t an issue in plugins I would make it a plugin instead, I posted about it here.

If it disables/enables the native file dialog, it can be changed in settings(genera/miscellaneous/enable native file dialogs). I must have turned it off years ago and forgot Krita even had a custom file dialog which is why I couldn’t put stuff in the filename or press the buttons with a script.


(some hours later)
I made some changes, this is more straight forward version that only creates a .paintingassistant file from a vector layer, its saved to temp. It’s up to the user to use use convert to shape on their selection (the vector layer should be cleared first). I said earlier any vector layer will do, that’s not quite right as the naive method I picked expects svgpath that is created using convert to shape.

I’m sure each case of vector shapes could be accounted for and converted 1:1 into assistants rather than a million rulers but I dont really use vectors anyway and only wanted to make a proof of concept. I wanted to specifically convert pixel selections into curves, then into assistant to get that magnet effect.

from krita import *
from pathlib import Path
import tempfile
import os
import re
import xml.etree.ElementTree as ET
import xml.dom.minidom
from math import inf
#
def extract_svg_path_d(svg_path):
    """Extracts the value of the 'd' attribute from an SVG path string."""
    match = re.search(r'\bd="([^"]*)"', svg_path)
    return match.group(1) if match else None
#
def reduce_path_density(svg_path, skip=4):
    if skip <= 1:
        return svg_path  # No reduction needed
    
    # Split the path into segments starting with M or L
    segments = re.split('([ML])', svg_path)[1:]
    
    result = []
    i = 0  # Counter for points after each M command
    
    # Rebuild the path with reduced density
    for j in range(0, len(segments), 2):
        command = segments[j]
        coords = segments[j+1]
        
        if command == 'M':
            # Always include M commands and reset counter
            result.append(f"{command}{coords}")
            i = 0
        elif command == 'L':
            # Split L coordinates and filter based on skip value
            points = [p for p in coords.split('L') if p]  # Split and remove empty strings
            filtered_points = []
            
            for idx, point in enumerate(points):
                # Always include first and last points, or include based on skip value
                if idx == 0 or idx == len(points)-1 or i % skip == 0:
                    filtered_points.append(point)
                i += 1
            
            if filtered_points:
                result.append(f"L{'L'.join(filtered_points)}")
    
    return ''.join(result)
#
def svg_path_to_assistant_naive_rulers(svg_path_d: str) -> str:
    """
    Converts an SVG path string containing multiple shapes (separated by 'M')
    into an XML format where *every consecutive pair of points* within each
    shape is represented as a 'ruler' assistant.

    Args:
        svg_path_d: The string content of the 'd' attribute from an SVG <path> tag.

    Returns:
        A formatted XML string in the paintingassistant format with many rulers.
    """
    
    # Regex to find commands (M, L, etc.) and their coordinate sequences
    pattern = re.compile(r'([MLHVCSQTAZmlhvcsqtaz])([^MLHVCSQTAZmlhvcsqtaz]*)')
    # Regex to extract coordinate pairs (x, y) from a sequence string
    coord_pattern = re.compile(r'(-?[\d\.]+)[,\s]+(-?[\d\.]+)')

    shapes_points = []
    current_shape_points = []

    matches = pattern.findall(svg_path_d.strip())

    for i, (command, args_str) in enumerate(matches):
        args_str = args_str.strip()
        # Convert extracted strings to float coordinate pairs
        coords = [(float(x), float(y)) for x, y in coord_pattern.findall(args_str)]

        if not coords and command.upper() != 'Z': # Ignore commands without coords (unless Z)
             continue

        # --- Handle specific commands (Simplified for M and L based on input) ---
        if command == 'M': # Absolute moveto
            if current_shape_points: # Finish previous shape if it had points
                shapes_points.append(current_shape_points)

            # Start new shape with the first point
            current_pos = list(coords[0])
            current_shape_points = [list(coords[0])]

            # Handle implicit lineto if M is followed by multiple coordinate pairs
            # Treat subsequent points as destinations of L commands
            for pt in coords[1:]:
                 current_pos = list(pt)
                 current_shape_points.append(list(pt))

        elif command == 'L': # Absolute lineto
            if not current_shape_points: continue # Cannot L without a current point

            for pt in coords:
                current_pos = list(pt)
                current_shape_points.append(list(pt))

        # --- Add handling for other commands (Z, H, V, C, S, Q, T, A, relative m, l, etc.) if needed ---
        # Example: Z would typically connect current_pos back to the shape's start_pos
        # elif command.upper() == 'Z':
        #    if current_shape_points:
        #        start_pos = current_shape_points[0]
        #        if current_pos != start_pos: # Avoid duplicate point if already closed
        #             current_shape_points.append(start_pos)
        #             current_pos = start_pos # Update current position

        # Update current_pos based on the last point processed in this command segment
        if coords and command.upper() != 'Z':
             current_pos = list(coords[-1]) # Keep track of the current position

    # Add the last collected shape
    if current_shape_points:
        shapes_points.append(current_shape_points)

    # --- Build XML ---
    root = ET.Element("paintingassistant", color="255,255,255,255")
    handles_root = ET.SubElement(root, "handles")
    sidehandles_root = ET.SubElement(root, "sidehandles")
    assistants_root = ET.SubElement(sidehandles_root, "assistants")

    handle_id_counter = 0
    assistant_handle_pairs = [] # Store pairs of handle IDs for each ruler

    # Iterate through each shape defined by 'M' commands
    for shape in shapes_points:
        # Iterate through consecutive pairs of points in the shape
        # Need at least 2 points to form a line segment (ruler)
        if len(shape) < 2:
            continue

        # Use zip to easily get consecutive pairs (p1, p2), (p2, p3), ...
        for p1, p2 in zip(shape[:-1], shape[1:]):
            # Create handle for the first point of the segment
            handle1_id = str(handle_id_counter)
            ET.SubElement(handles_root, "handle",
                          id=handle1_id,
                          x=f"{p1[0]:.3f}",
                          y=f"{p1[1]:.3f}")
            handle_id_counter += 1

            # Create handle for the second point of the segment
            handle2_id = str(handle_id_counter)
            ET.SubElement(handles_root, "handle",
                          id=handle2_id,
                          x=f"{p2[0]:.3f}",
                          y=f"{p2[1]:.3f}")
            handle_id_counter += 1

            # Store the pair of handle IDs for this ruler assistant
            assistant_handle_pairs.append((handle1_id, handle2_id))

    # Create the ruler assistants, referencing the created handle pairs
    for h1_id, h2_id in assistant_handle_pairs:
        assistant = ET.SubElement(assistants_root, "assistant",
                                  type="ruler", # Use 'ruler' type as requested
                                  useCustomColor="0",
                                  customColor="176,176,176,255")
        # Add standard sub-elements for ruler type
        ET.SubElement(assistant, "subdivisions", value="0")
        ET.SubElement(assistant, "minorSubdivisions", value="0")
        ET.SubElement(assistant, "fixedLength", value="0", enabled="0", unit="px")

        assistant_handles = ET.SubElement(assistant, "handles")
        ET.SubElement(assistant_handles, "handle", ref=h1_id)
        ET.SubElement(assistant_handles, "handle", ref=h2_id)

    # --- Format and Return XML ---
    rough_string = ET.tostring(root, 'utf-8')
    reparsed = xml.dom.minidom.parseString(rough_string)
    pretty_xml = reparsed.toprettyxml(indent="    ", encoding="UTF-8").decode('utf-8')

    # Remove extra blank lines caused by minidom
    non_blank_lines = [line for line in pretty_xml.splitlines() if line.strip()]
    return "\n".join(non_blank_lines)
#
def save_to_tempfile_ext(content, extension=None):

    # Create a temporary file
    fd, path = tempfile.mkstemp()
    
    try:
        # Write the content to the temporary file
        with os.fdopen(fd, 'w') as tmp:
            tmp.write(content)
        
        # If an extension is provided, rename the file to include the extension
        if extension:
            if not extension.startswith('.'):
                extension = '.' + extension  # Ensure the extension starts with a dot
            new_path = path + extension
            os.rename(path, new_path)
            path = new_path
        
        return path
    except Exception as e:
        # Clean up the temporary file in case of an error
        os.close(fd)
        os.remove(path)
        raise e
#
def main():
    inst = Krita.instance()
    doc = inst.activeDocument()
    if not doc: print("no active document"); return
    layer = doc.activeNode()
    if layer.type() != "vectorlayer": print("not vector layer"); return
    svg = layer.toSvg()
    svg_path = extract_svg_path_d(svg)
    svg_path = reduce_path_density(svg_path,4)
    assistant = svg_path_to_assistant_naive_rulers(svg_path)
    temp_fp = save_to_tempfile_ext(assistant,".paintingassistant")
    temp_fp = Path(temp_fp)
    print(temp_fp.parent,temp_fp.name)
#
main()