Inkscape.org
Creating New Extensions [beginner's question] Some guidance on mapping a key to a custom Python function to do repetitive tasks
  1. #1
    Rodrigo Morales Rodrigo Morales @rodrigomorales
    *

    When I'm using Inkscape, there are some tasks which I frequently do. These are the tasks that I frequently do:

    1. Set the opacity of the selected path to 10%
    2. Export selection to PNG in 600 DPI and insert it to my clipboard

    I wish I could map a key to do those repetitive tasks. That is, I want to do (1) or (2) by pressing a single key in my keyboard.

    I have been searching for a while and I found out that some people have created Inkscape extensions using Python. I have never created an Inkscape extension, but I have used Python in the past and it seems that Inkscape can be widely controlled using Python so I'm planning to learn how to write my custom extensions, so that I could find a way to map a key to a Python function that do (1) and (2).

    So far, I have been able to execute this extension https://inkscapetutorial.org/hello-extension.html in my computer.

    I'm creating this post so that others can share their experiences mapping a key to a custom function which you created in Python. Any information on this topic is appreciated.

     

  2. #2
    Rodrigo Morales Rodrigo Morales @rodrigomorales
    *

    I managed to create an extension to create an extension for setting the opacity and I was able to map it to a key by going to "Preferences" > "Keyboard". Below is the code of the extension.

    Now, I need to learn how to export the selection to a PNG file and insert it to my clipboard.

    ~/.config/inkscape/extensions/my_set_opacity_to_10_percent/my_set_opacity_to_10_percent.inx

    <?xml version="1.0" encoding="UTF-8"?>
    <inkscape-extension
        xmlns="http://www.inkscape.org/namespace/inkscape/extension">
        <name>Set opacity of selection to 10%</name>
        <id>user.my_set_opacity_to_10_percent</id>
        <effect>
            <object-type>all</object-type>
            <effects-menu>
                <submenu name="Custom"/>
            </effects-menu>
        </effect>
        <script>
            <command location="inx" interpreter="python">my_set_opacity_to_10_percent.py</command>
        </script>
    </inkscape-extension>
    

    ~/.config/inkscape/extensions/my_set_opacity_to_10_percent/my_set_opacity_to_10_percent.py

    import inkex
    from inkex import TextElement
    
    class Hello(inkex.EffectExtension):
        def effect(self):
            for elem in self.svg.selection:
                elem.style['opacity'] = 0.1
    
    if __name__ == '__main__':
        Hello().run()
    

     

  3. #3
    inklinea inklinea @inklinea⛰️

    It's not possible to replicate the gui export to .png function directly with pure python (inkex)

    It requires a command call to the main Inkscape program to export to a file in the system tempfolder

    You can then open the .png file with PIL (pillow) which is bundled with Inkscape on Windows.

    (Please note there was a slight issue with one of the 1.4.x releases and PIL which has been reported)

    Then you would need to go from the PIL Image to the Windows clipboard.

    I requires a pypi library to be installed, there are a couple of examples if you search

    "python 3 PIL image to clipboard"

    I think some also work with Linux.

  4. #4
    Rodrigo Morales Rodrigo Morales @rodrigomorales

    I managed to create an extension that exports the selection to a PNG file using the code below. I would appreciate some feedback from more experienced users on the code that I wrote. Any suggestion/improvement to my code is appreciated.

    ~/.config/inkscape/extensions/my_export_selection_to_png/my_export_selection_to_png.inx

    <?xml version="1.0" encoding="UTF-8"?>
    <inkscape-extension
        xmlns="http://www.inkscape.org/namespace/inkscape/extension">
        <name>Export selection to PNG</name>
        <id>user.my_export_selection_to_png</id>
        <effect>
            <object-type>all</object-type>
            <effects-menu>
                <submenu name="Custom"/>
            </effects-menu>
        </effect>
        <script>
            <command location="inx" interpreter="python">my_export_selection_to_png.py</command>
        </script>
    </inkscape-extension>

    ~/.config/inkscape/extensions/my_export_selection_to_png/my_export_selection_to_png.py

    import inkex
    import subprocess
    import copy
    
    class MyExportSelectionToPng(inkex.EffectExtension):
        def effect(self):
            # Create copy of the SVG file
            document = copy.deepcopy(self.document)
            # Get the ID of elements in the selection
            selected_elements_ids = []
            for elem in self.svg.selection:
                selected_elements_ids.append(elem.eid)
            # Remove elements that are not in the selection
            g = document.xpath('/svg:svg/svg:g', namespaces=inkex.NSS)[0]
            for element in g.getchildren():
                if element.eid not in selected_elements_ids:
                    g.remove(element)
            # Write document to a temporary file
            document.write('/tmp/a.svg')
            # Export PNG file to SVG file
            command = [
                'inkscape',
                '/tmp/a.svg',
                '--export-area-drawing',
                '--export-dpi=600',
                '--export-type', 'png',
                '--export-filename', '/tmp/a.png',
            ]
            try:
                subprocess.check_call(command)
            except Exception as e:
                raise Exception(
                    f'Failed to convert.')
            # TODO: Insert PNG file to clipboard
            #
            # Note: Calling xclip through subprocess.run makes Inkscape
            # get stuck. For the time being, I'll write a separate shell
            # scripts that inserts the image into the clipboard.
            #
            # subprocess.run(['xclip', '-selection', 'clipboard', '-t', 'image/png', '/tmp/a.png'])
    
    if __name__ == '__main__':script
        MyExportSelectionToPng().run()
    

     

    Sidenote: In Preferences, I then mapped "x" to call "Set opacity to 10%" and "c" to call "Export selection to PNG" so that I can quickly call those functions with my left hand. While I'm editing in Inkscape, I mainly use my right hand to do actions with the mouse and my left hand remains in the keyboard.

  5. #5
    inklinea inklinea @inklinea⛰️

    Heres an example that works for me on Ubuntu 22 and Inkscape 1.4.

    Pasting into Inkscape and Gimp works. I haven't tested it with Windows.

    It creates a dummy Gtk3 window object, with a Gtk3 pixbuf image element in it.

    It then uses the Gtk3 clipboard facility to get the pixbuf into the clipboard.

    Works for me but is a bit hacky,it uses Threads to exit Gtk3 after a given number of seconds.

    If you are copying a huge object this could fail if not enough time for the copy operation (say on a slow machine).

    For my own purposes I used a pruning function to isolate a single object. 

    However using python to isolate the object / selection you need is a bit pointless.

    select-invert has returned to the Inkscape 1.4 command line.

    It would be more efficient to pass your selection to the command line using select-by-id:id1,id2,id3...... etc

    Then use fit-canvas-to-selection, then select-invert, then object-set-property:display,none

    This leaves just your selection visible, and the canvas the correct size for the selection.

    Just pass deepcopy(self.svg) to the command line instead of pruned_svg.
    ------------------------------------------------------------------------------------------

     

    import gi
    gi.require_version('Gtk', '3.0')
    from gi.repository import Gtk, Gdk, GdkPixbuf
    
    import time
    from time import sleep
    import tempfile
    import os, shutil
    from copy import deepcopy
    
    import inkex
    from inkex import command
    
    
    def make_temp_folder(self):
    
        self.temp_folder = tempfile.mkdtemp()
        return self.temp_folder
    
    def save_temp_svg(self, svg, extension='.svg'):
    
        temp_file_name = str(time.time()).replace('.', '') + extension
    
        if hasattr(self, 'temp_folder'):
            temp_folder = self.temp_folder
        else:
            temp_folder = make_temp_folder(self)
    
        svg_temp_file_path = os.path.join(temp_folder, temp_file_name)
    
        with open(svg_temp_file_path, 'w') as svg_temp_file:
            my_svg_string = svg.tostring().decode("utf-8")
            svg_temp_file.write(my_svg_string)
            svg_temp_file.close()
    
        return svg_temp_file_path
    
    def inkscape_command_call(self, input_file, options_list, action_list):
    
        command.inkscape(input_file, options_list, f'--actions={action_list}')
    
    
    def export_element_to_png(self, element):
    
        pruned_svg = prune_object_tree(self, deepcopy(self.svg), element)
        svg_temp_file_path = save_temp_svg(self, pruned_svg)
        png_temp_file_path = svg_temp_file_path.split('.svg')[0] + '.png'
    
        action_list = f'export-type:png;export-dpi:600;export-filename:{png_temp_file_path};export-do;'
    
        inkscape_command_call(self, svg_temp_file_path, 'none', action_list)
    
        return png_temp_file_path
    
    def prune_object_tree(self, svg, element):
    
        # A list of tag we wish to process ( to enable defs / namedview etc to be ignored )
    
        element_tags = ['circle', 'ellipse', 'image', 'line', 'path', 'polygon', 'polyline', 'rect', 'text', 'textPath', 'title', 'tspan', 'use', 'g']
    
        ancestors = svg.getElementById(element.get_id()).ancestors()
    
        # Add element to ancestors elementlist to avoid repeating sibling code.
        ancestors.add(element)
    
        # Remove all siblings on each level of tree
    
        for ancestor in ancestors:
            if ancestor.getparent() != None:
                following_siblings = ancestor.xpath('./following-sibling::*')
                preceding_siblings = ancestor.xpath('./preceding-sibling::*')
                siblings = following_siblings + preceding_siblings
                for item in siblings:
                    if item.TAG in element_tags:
                        inkex.errormsg(item.TAG)
                        svg.getElementById(item.get_id()).delete()
    
        return svg
    
    class MyWindow(Gtk.Window):
        def __init__(self):
            super().__init__(title="Dummy Gtk for clipboard")
    
            gtk_image = Gtk.Image()
    
            pixbuf = GdkPixbuf.Pixbuf.new_from_file(InkRasterClipboard.png_temp_file_path)
    
            gtk_image.set_from_pixbuf(pixbuf)
    
            gtk_image.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
            gtk_image.clipboard.set_image(gtk_image.get_pixbuf())
    
    
    def run_gtk():
        win = MyWindow()
        win.connect("destroy", Gtk.main_quit)
        Gtk.main()
    
    # Sleep time can be adjusted to correct for slow machines
    def exit_gtk():
        sleep(2)
    
        Gtk.main_quit()
    
    class InkRasterClipboard(inkex.EffectExtension):
    
        def add_arguments(self, pars):
            pass
        
        def effect(self):
    
            selection_list = self.svg.selected
            if len(selection_list) < 1:
                inkex.errormsg('Please one object')
                return
            if len(selection_list) > 1:
                inkex.errormsg('Please select one object')
    
            # Create a png of the current selected object
    
            InkRasterClipboard.png_temp_file_path = export_element_to_png(self, selection_list[0])
    
            from threading import Thread
            Thread(target=exit_gtk).start()
    
            run_gtk()
    
            # Remove temp folder
            if hasattr(self, 'temp_folder'):
                shutil.rmtree(self.temp_folder)
    
            
    if __name__ == '__main__':
        InkRasterClipboard().run()
    
Inkscape Inkscape.org Inkscape Forum Creating New Extensions [beginner's question] Some guidance on mapping a key to a custom Python function to do repetitive tasks