Skip to content

Python API: Add Preset plus vector and transform methods

Know Zero requested to merge knowzero/krita:KnowZero/Python-API-Features into master

Python API: Add Preset plus vector and transform methods

This patch includes expansion to the Python API to make it easier to work with vectors and overlays, plus minimal support for brush preset control. It also includes missing sip bindings for some methods and a few fixes.

Document

  • createTransparencyMask : Creates a transparency mask node
  • createColorizeMask : Creates a colorize mask node

ColorizeMask

  • Ability to modify and generate colorize masks

TransparencyMask

  • Ability to modify transaprency masks and prevent them from crashing

TransformMask

  • toXML : Output TransformMask as XML string
  • fromXML : Set transform from XML string

CloneLayer

  • sourceNode : Show the original node CloneLayer is based on
  • setSourceNode : Set the node CloneLayer should be based on

Shape

  • == and != - Compare if the shape is the same shape
  • parentShape : Find the parent of the current shape
  • absoluteTransformation : The absolute transformation of a shape including all grandparents

VectorLayer

  • shapeAtPosition : Gets shape at a certain point
  • shapesInRect : Returns all shapes within a rectangle
  • createGroupShape : Groups top level shapes into a GroupShape

Node

  • findChildNodes : finds all child nodes directly or recursively that match the given criteria within a node. Search by name partially or fully, by type or by color label.

Preset

  • toXML : dump all brush preset preferences
  • fromXML : change brush preset preferences

View

  • flakeToDocumentTransform
  • flakeToCanvasTransform
  • flakeToImageTransform

Window

  • dockers : get a list of dockers for the set window

Krita.instance().dockers() fixed to not crash if declared before mainwindow is loaded.

Modified Tests:

  • TestDocument
  • TestNode

New Tests:

  • TestShape
  • TestVectorLayer

Test Plan

from krita import *
import re



def nodeNames(nlist):
    rlist = []
    for n in nlist:
        rlist.append(n.name())
    return rlist
    
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()

    
    if i == 0:
        print ("Creating Vector Layer")
        newLayer = doc.createVectorLayer('VLayer1')
        
        
        doc.rootNode().addChildNode(newLayer,None)
        print ("Done")

    elif i == 1:
        print ("Create Shape")
        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/>
<text id="shape0" krita:useRichText="true" krita:textVersion="2" transform="matrix(0.999999960263572 0 0 0.999999960263572 12.921701441042 34.9859557016265)" fill="#0026ff" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" font-stretch="normal" letter-spacing="0" word-spacing="0"><tspan x="0">Top Shape0</tspan></text>
<text id="shape1" krita:useRichText="true" krita:textVersion="2" transform="matrix(0.999999867428964 0 0 0.999999867428964 239.90986885015 35.1714852651231)" fill="#0026ff" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" font-stretch="normal" letter-spacing="0" word-spacing="0"><tspan x="0">Top Shape1</tspan></text>
<text id="shape2" krita:useRichText="true" krita:textVersion="2" transform="matrix(0.999999762283568 0 0 0.999999762283568 36.7133523692404 203.118701819228)" fill="#0026ff" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" font-stretch="normal" letter-spacing="0" word-spacing="0"><tspan x="0">Bottom Shape2</tspan></text>
<text id="shape3" krita:useRichText="true" krita:textVersion="2" transform="matrix(0.999999698132009 0 0 0.999999698132009 214.531891414957 201.125497598433)" fill="#0026ff" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" font-stretch="normal" letter-spacing="0" word-spacing="0"><tspan x="0">Bottom Shape3</tspan></text>
</svg>''')
        print ( node.shapes()[0].position() )
        print ("Done")

    elif i == 2:
        print ("TEST: VectorLayer")
        shapes = node.shapes()
        shape = node.shapeAtPosition(QPointF(20,30))
        print( "RESULT shapeAtPosition:", shape.name() )
        print( "RESULT shapeAtPosition == shape[0] (true):", shape == shapes[0] )
        print( "RESULT shapeAtPosition != shape[0] (false):", shape != shapes[0] )
        print( "RESULT shapeAtPosition == shape[1] (false):", shape == shapes[1] )
        print( "INFO: SHAPE0", shapes[0].boundingBox(), shapes[0].position(), "SHAPE2:", shapes[2].boundingBox(), shapes[2].position() )
        print( "INFO:", node.shapesInRect(QRectF(0,0,200,200), True, True), node.shapesInRect(QRectF(0,0,200,200)) )
        print( "RESULT shapesInRect (shape0 + shape2):", nodeNames(node.shapesInRect(QRectF(0,0,200,200))) )
        group = node.createGroupShape( "New Group 1", [ shapes[0], shapes[1] ] )
        group.select()
        print( "RESULT createGroupShape:", group.name() )
       
        print ("TEST: Complete")
    elif i == 3:
        print ("TEST: Shape")
        group = node.shapes()[0]
        shapes = group.children()
        
        
        print( "RESULT absoluteTransformation of shape 0:",  matrix_array(shapes[0].absoluteTransformation()) )
        print( "RESULT parentShape of shape 0(name):",  shapes[0].parentShape().name() )
        
       
        print ("TEST: Complete")
    elif i == 4:
        view = Krita.instance().activeWindow().activeView()

        print ("TEST: View Transform and dockers list")
        print( "RESULT flakeToDocumentTransform:",  matrix_array(view.flakeToDocumentTransform()) )
        print( "RESULT flakeToCanvasTransform:",  matrix_array(view.flakeToCanvasTransform()) )
        print( "RESULT flakeToImageTransform:",  matrix_array(view.flakeToImageTransform()) )
        print( "RESULT Krita.instance().activeWindow().dockers():",  Krita.instance().activeWindow().dockers() )
        
        print ("TEST: Complete")

    elif i == 5:
        print ("TEST: Document and Nodes")
        g1 = doc.createGroupLayer("G1")
        v2 = doc.createNode("VLayer2","paintlayer")
        
        c1 = doc.createCloneLayer("CLayer1",v2)
        t1 = doc.createTransparencyMask("TLayer1")
        t2 = doc.createTransformMask("TLayer2")
        g1.setColorLabel(2)
        g1.addChildNode(v2, None)
        v2.addChildNode(t1, None)
        
        
        doc.rootNode().addChildNode(g1, None)
        

        
        print( "RESULT clonelayer:",  v2.name() == c1.sourceNode().name() )
        c1.setSourceNode(g1)
        print( "RESULT clonelayer set:",  g1.name() == c1.sourceNode().name() )
        
        s1 = Selection()
        s1.select(0,0,20,20,255)
        t1.setSelection(s1)
        
        print( "RESULT transparency mask:",  s1.width() == t1.selection().width() )
        
        xml='''<!DOCTYPE transform_params>
<transform_params>
  <main id="tooltransformparams"/>
  <data mode="0">
   <free_transform>
    <transformedCenter type="pointf" x="12.3102137276208" y="11.0727768562035"/>
    <originalCenter type="pointf" x="20" y="20"/>
    <rotationCenterOffset type="pointf" x="0" y="0"/>
    <transformAroundRotationCenter value="0" type="value"/>
    <aX value="0" type="value"/>
    <aY value="0" type="value"/>
    <aZ value="0" type="value"/>
    <cameraPos z="1024" type="vector3d" x="0" y="0"/>
    <scaleX value="1" type="value"/>
    <scaleY value="1" type="value"/>
    <shearX value="0" type="value"/>
    <shearY value="0" type="value"/>
    <keepAspectRatio value="0" type="value"/>
    <flattenedPerspectiveTransform m23="0" m31="0" m32="0" type="transform" m33="1" m12="0" m13="0" m22="1" m11="1" m21="0"/>
    <filterId value="Bicubic" type="value"/>
   </free_transform>
  </data>
</transform_params>'''


        
        print( "RESULT findChildNodes - All non-recursive:",  nodeNames(doc.rootNode().findChildNodes()) )
        print( "RESULT findChildNodes - All recursive:",  nodeNames(doc.rootNode().findChildNodes('', True)) )
        print( "RESULT findChildNodes - Exact VLayer1:",  nodeNames(doc.rootNode().findChildNodes('VLayer1')) )
        print( "RESULT findChildNodes - Exact Recursive VLayer2:",  nodeNames(doc.rootNode().findChildNodes('VLayer2', True)) )
        print( "RESULT findChildNodes - Partial Recursive VLayer:",  nodeNames(doc.rootNode().findChildNodes('VLayer',True, True)) )
        print( "RESULT findChildNodes - Paint Layer Type Recursive PaintLayer:",  nodeNames(doc.rootNode().findChildNodes('',True,False,'paintlayer')) )
        print( "RESULT findChildNodes - ColorLabel Recursive Index 2:",  nodeNames(doc.rootNode().findChildNodes('',True,False,'',2)) )

        v1 = doc.rootNode().findChildNodes('VLayer1')[0]
        v1.addChildNode(t2, None)
        t2.fromXML(xml)

        
        print( "RESULT transformmask:", 'z="1024"' in xml )
        
        print ("TEST: Complete")
    elif i == 6:
        print ("TEST: Preset")
        
        view = Krita.instance().activeWindow().activeView()
        resources = Krita.instance().resources("preset")


        print ( "Setting to b) Basic-1 brush..." )

        for (k, v) in resources.items():
            if 'b) Basic-1' in v.name():
                view.setCurrentBrushPreset(v)
        
        preset = Preset(view.currentBrushPreset())
        
        pxml = preset.toXML()
        dmatch = re.search(r'\<param .*?name="diameter".*?\>(.*?)\<\/param>', pxml)
        diameter = dmatch.group(1)
        
        print ("Current Brush Preset diameter:", diameter  )

        
        newxml = re.sub(r'(\<param .*?name="diameter".*?\>).*?\<\/param\>', r'<param type="string" name="diameter"><![CDATA[35.5]]></param>', pxml )        
        
       
        
        preset.fromXML( newxml )

        print ("Setting diameter to 35.5:", re.search(r'\<param .*?name="diameter".*?\>(.+?)\</param\>', preset.toXML()).group(1) == '<![CDATA[35.5]]>' )

    elif i == 7:
        window = Krita.instance().activeWindow()
        doc = Krita.instance().createDocument(10, 3, "Test", "RGBA", "U8", "", 120.0)
        window.addView(doc)
        root = doc.rootNode();
        node = doc.createNode("layer", "paintLayer")
        root.addChildNode(node, None)
        nodeData = QByteArray.fromBase64(b"AAAAAAAAAAAAAAAAEQYMBhEGDP8RBgz/EQYMAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARBgz5EQYM/xEGDAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEQYMAhEGDAkRBgwCAAAAAAAAAAAAAAAA");
        node.setPixelData(nodeData,0,0,10,3)

        cols = [ ManagedColor('RGBA','U8',''), ManagedColor('RGBA','U8','') ]
        cols[0].setComponents([0.65490198135376, 0.345098048448563, 0.474509805440903, 1.0]);
        cols[1].setComponents([0.52549022436142, 0.666666686534882, 1.0, 1.0]);
        keys = [
                QByteArray.fromBase64(b"/48AAAAAAAAAAAAAAAAAAAAAAACmCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
                QByteArray.fromBase64(b"AAAAAAAAAACO9ocAAAAAAAAAAAAAAAAAAAAAAMD/uQAAAAAAAAAAAAAAAAAAAAAAGoMTAAAAAAAAAAAA")
                ]

        mask = doc.createColorizeMask('c1')
        node.addChildNode(mask,None)
        mask.setEditKeyStrokes(True)

        mask.setUseEdgeDetection(True)
        mask.setEdgeDetectionSize(4.0)
        mask.setCleanUpAmount(70.0)
        mask.setLimitToDeviceBounds(True)
        mask.setKeyStrokeColors(cols)

        for col,key in zip(cols,keys):
            mask.setKeyStrokePixelData(key,col,0,0,20,3)

        mask.updateMask()
        mask.setEditKeyStrokes(False);
        mask.setShowOutput(True);
    
        print ("TEST: Complete")
        print ("DONE!")  
        

                        
    if i < 7: 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.

/review

Edited by Know Zero

Merge request reports