Skip to content

Extend Python API with vector methods and remove node fix

BUGFIX: Node removal via API then undo caused a crash

If a user adds a shape to a layer, then the API removes the layer and the user hits undo. Krita will crash. This fixes it by adding layer removal to the undo.

Python API: Add export and import SVG strings

Added the addShapesFromSvg and toSvg methods to Vector Layers.

Python API: Expose more Shape methods to the API

This patch adds the following methods:

  • setType: So the plugin can take ownership of the shape.
  • zIndex, setZIndex: So the plugin can control if the shape should be in front or back.
  • selectable, setSelectable, geometryProtected, setGeometryProtected: So the plugin can take control of managing the shapes.
  • transformation, setTransformation: So the plugin can get back matrix data and adjust them
  • remove: So the plugin can delete shapes
  • update and updateAbsolute: So shape changes can be updated (like fixes setVisible)

This patch Modifies the following:

  • toSvg: Adds 2 optional parameters. prependStles and stripTextMode this lets the plugin get the data it needs from the shapes. Being optional parameters, it retains backwards compatibility

This patch Modifies the following:

  • toSvg: Adds 2 optional parameters. prependStyles and stripTextMode this lets the plugin get the data it needs from the shapes. Being optional parameters, it retains backwards compatibility

Test Plan

Tools->scripter and run this: (It will go through all the methods on a 2 second timer for about 20 seconds. And output details into terminal)

from PyQt5 import QtCore, QtGui, QtWidgets
import re

def matrix_array(mx):
    return [mx.m11(),mx.m12(),mx.m13(),mx.m21(),mx.m22(),mx.m23(),mx.m31(),mx.m32(),mx.m33()]

def stepper(i):
    print ("STEP", i)
    doc = Krita.instance().activeDocument()
    node = doc.activeNode()
    shapes = [None, None, None, None]
    if node.type() == 'vectorlayer':
        tempShapes = node.shapes()
        
        for shape in tempShapes:
            shapes[int(re.match(r'shape(\d+)', shape.name()).group(1))]=shape
    
    if i == 0:
        print ("Creating Vector Layer")
        newLayer = doc.createVectorLayer('VLayer')
        
        doc.rootNode().addChildNode(newLayer,None)
        doc.setActiveNode(newLayer)

    
    if i == 1:
        print ("TEST: import SVG Test")
        newShapes=node.addShapesFromSvg('''<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"><!-- Created using Krita: https://krita.org --><svg xmlns="http://www.w3.org/2000/svg"                xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:krita="http://krita.org/namespaces/svg/krita" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" width="307.2pt" height="307.2pt" viewBox="0 0 307.2 307.2"><defs><linearGradient id="gradient0" gradientUnits="objectBoundingBox" x1="0" y1="0" x2="1" y2="1" spreadMethod="pad"><stop stop-color="#9f6c00" offset="0" stop-opacity="1"/><stop stop-color="#6b6a7b" offset="1" stop-opacity="1"/></linearGradient></defs><path id="shape0" transform="translate(21.5999992104248, 215.099992137147)" fill="#2b0000" fill-rule="evenodd" stroke="#d00000" stroke-width="6" stroke-linecap="square" stroke-linejoin="bevel" d="M76.5 64.8L0 64.8L40.5 0Z" sodipodi:nodetypes="cccc"/><rect id="shape1" transform="translate(200.100000230293, 16.799999934202)" fill="url(#gradient0)" fill-rule="evenodd" stroke="#ffb100" stroke-width="24" stroke-linecap="square" stroke-linejoin="bevel" width="90" height="66"/><text id="shape2" krita:useRichText="true" krita:textVersion="2" transform="translate(8.9999996710103, 31.4999995065155)" fill="#0026ff" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" font-size-adjust="0.372962" font-stretch="normal" letter-spacing="0" word-spacing="0"><tspan x="0">Top Shape2</tspan></text><text id="shape3" krita:useRichText="true" krita:textVersion="2" transform="translate(211.499992268743, 273.599990656694)" fill="#0a0a00" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" font-size="14" font-size-adjust="0.372962" font-stretch="normal" font-weight="700" letter-spacing="0" word-spacing="0"><tspan x="0">Bottom Shape3</tspan></text></svg>''')
        
        for shape in newShapes:
            print ("ADDED SHAPE:", shape.name() )
        
        print ("TEST: Complete")
    elif i == 2:
        print ("TEST: Remove Layer")
        print ("RESPONSE:", node.remove() )
        print ("TEST: Complete")
    elif i == 3:
        print ("TEST: Undo layer removal")
        Krita.instance().action('edit_undo').trigger()
        print ("TEST: Complete")
    elif i == 4:
        print ("TEST: Get Shape properties")
        for si in range(len(shapes)):
            print ("SHAPE #"+str(si), 
            "type:", shapes[si].type(), 
            "visible:", shapes[si].visible(), 
            "zindex:", shapes[si].zIndex(), 
            "transformation:", matrix_array(shapes[si].transformation()),
            "selectable:", shapes[si].selectable(),
            "geometryprotected:", shapes[si].geometryProtected()
                                  
            )
        print ("TEST: Complete")
    elif i == 5:
        print ("TEST: Change Shape properties")

        for si in range(len(shapes)):
            if si == 0:
                print ("Make Shape 0 invisible")
                shapes[0].setVisible(False)
                shapes[0].update()
            elif si == 1:
                print ("Transform Shape 1")
                rect = shapes[1].boundingBox()   
                tran = shapes[1].transformation()
                tran.rotate(45)
                shapes[1].setTransformation(tran)
                rect |= shapes[1].boundingBox() 
                shapes[1].updateAbsolute(rect)
            elif si == 2:
                print ("Move Shape2 to Shape 1")
                rect = shapes[2].boundingBox()
                shapes[2].setPosition(shapes[1].position())
                rect |= shapes[2].boundingBox() 
                shapes[2].updateAbsolute(rect)
            elif si == 3:
                print ("Delete Shape 3")
                print ("Deleted:", shapes[3].remove())
                break
            

            print ("SHAPE #"+str(si), 
                "visible:", shapes[si].visible(), 
                "transformation:", matrix_array(shapes[si].transformation()),
            )
        print ("TEST: Complete")
        
    elif i == 6:
        print ("TEST: Undo delete shape 3 and restore visible shape 0")
        Krita.instance().action('edit_undo').trigger()
        shapes[0].setVisible(True)
        shapes[0].update()
        print ("TEST: Complete")      
        
    elif i == 7:
        print ("TEST: Make Shape 2 go behind Shape 1")
        shapes[2].setZIndex(-1)
        shapes[2].update()
  

        print ("TEST: Complete")             
    elif i == 8:
        print ("TEST: Block Shapes from user")
        
        print ("Make Shape 0 unselectable")
        shapes[0].setSelectable(False)
        print ("Make Shape 1 geometry locked")
        shapes[1].setGeometryProtected(True)

        print ("TEST: Complete")    
    elif i == 9:
        print ("TEST: Output Shape data")
        for si in range(len(shapes)):
            print ("SVG SHAPE #"+str(si), 
                                 shapes[si].toSvg(True, False) );
        print ( "SVG LAYER", node.toSvg() )

        print ("TEST: Complete")
        print ("DONE!")  
                        
    if i < 10: QtCore.QTimer.singleShot(2000, lambda: stepper(i+1) )
        
print ("Starting Test")  
newdoc = Krita.instance().createDocument(512, 512, "Python test document", "RGBA", "U8", "", 120.0)
Krita.instance().activeWindow().addView(newdoc)
stepper(0)

Formalities Checklist

  • I confirmed this builds.
  • I confirmed Krita ran and the relevant functions work.
  • I tested the relevant unit tests and can confirm they are not broken. (If not possible, don't hesitate to ask for help!)
  • I made sure my commits build individually and have good descriptions as per KDE guidelines.
  • I made sure my code conforms to the standards set in the HACKING file.
  • I can confirm the code is licensed and attributed appropriately, and that unattributed code is mine, as per KDE Licensing Policy.
Edited by Know Zero

Merge request reports