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
(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()