Reducing boilerplate code in Python plugins

Proof-of-concept:

A plugin that applies a GEGL Gaussian blur vertically or horizontally after showing this:

image

The whole plugin code (everything else is in a gimphelpers Python module:

#!/usr/bin/env python3

# Demo for gimphelpers

# Demo of the choice arg with values

import gi
gi.require_version('Gimp', '3.0')
from gi.repository import Gimp
gi.require_version('Gegl', '0.4')
from gi.repository import Gegl
from gi.repository import GLib
import sys
from gimphelpers import *

def _(message): return GLib.dgettext(None, message)

# Direction choice. This is both the object used to generate the dialog
# and the one from which values are retrieved. Here we use functions
# just to show how powerful this can be

directions=Choice('direction','Direction','Choice of direction','H',
    [
       Option('H','Horizontal',help='Up and down',value=lambda v: (v,0.)),
       Option('V','Vertical', value=lambda v: (0.,v)),
    ])

#
# The actual plugin code, doest a vertical or horizontal blur, given a size and a dierction
#
def executeChoice(procedure, run_mode, image, drawables, config, data):
    trace(f'entering executeChoice({procedure=!r}, {run_mode=!r}, {image=!r}, {drawables=!r}, {config=!r}, {data=!r})')
    direction=config.get_property('direction')
    size=config.get_property('size')
    sizeX,sizeY=directions[direction](size)
    Gegl.init()
    layer=drawables[0]
    selected,x,y,w,h=layer.mask_intersect()
    if selected:
        applyGeglOnBuffers(layer.get_buffer(),layer.get_shadow_buffer(),'gegl:gaussian-blur',std_dev_x=sizeX,std_dev_y=sizeY)
        layer.merge_shadow(True)
        layer.update(x,y,w,h)
    return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())

#
# The "registration" code
#
class ChoiceDemo(HelpedPlugin):
    def __init__(self):
        super().__init__(
               [
                   ProcedureDescription(
                        'choice-demo',
                        'RGB*, GRAY*',Gimp.ProcedureSensitivityMask.DRAWABLE,
                        executeChoice,
                        menuLabel=_('Choice demo'), # I18N is can be used
                        menuPath='<Image>/'+_('Test'),
                        args= [
                                Double('size',_('Blur size'),'',0.,1500.,5.), # using order
                                directions,
                        ],
                    )
               ])

Gimp.main(ChoiceDemo.__gtype__, sys.argv)

For the curious, the code (pretty much a WIP right now) is here.

Interesting! As a note, ScriptFu scripts can't call GEGL filters since PDB compatibility procedures were obsoleted in rc1 (#12279) · Issues · GNOME / GIMP · GitLab has been marked for 3.0RC2, so we should have a built-in method to easily apply GEGL filters for plug-ins and scripts soon.

First shot at my GimpHelpers module. Registering a script is now down to:

class FullDialog(HelpedPlugin):
    procedures=[
            ProcedureDescription(
                'full-dialog',
                'RGB*, GRAY*', Gimp.ProcedureSensitivityMask.DRAWABLE,
                fullDialog,
                args=[
                    Boolean(    'boolean',  _('Boolean'),   _('Boolean arg'),   True),
                    Brush(      'brush',    _('Brush'),     _('Brush arg'),     _('Confetti')),
                    # This one defined above because it can contain other useful data
                    choiceDemo,
                    Channel(    'channel',  _('Channel'),   _('Channel arg')),
                    Color(      'color',    _('Color'),     _('Color arg'),     Gegl.Color.new('red'), hasAlpha=True),
                    Color(      'color2',    _('Color2'),     _('Color arg2'),     Gegl.Color.new('blue'), hasAlpha=False),
                    Double(     'double',   _('Double'),    _('Double arg'),    0, 100, 50),
                    Drawable(   'drawable', _('Drawable'),  _('Drawable arg')),
                    File(       'file',     _('File'),      _('File arg')),
                    Font(       'font',     _('Font'),      _('Font arg'),      _('Monospace Bold Italic'),noneOK=False),
                    Gradient(   'gradient', _('Gradient'),  _('Gradient arg'),  _('Sunrise')),
                    # 'image-' because 'image' is already used by the standard argument
                    Image(      'image-',   _('Image'),     _('Image arg'),     noneOK=False),
                    Int(        'int',      _('Integer'),   _('Integer arg'),   -10, 10, 0),
                    Layer(      'layer',    _('Layer'),     _('Layer arg'),     noneOK=False),
                    Palette(    'palette',  _('Palette'),   _('Palette arg'),   _('Ega')),
                    Pattern(    'pattern',  _('Pattern'),   _('Pattern arg'),   _('Nops')),
                    String(     'string',   _('String'),    _('String arg'),    _('Déjà vu')),
                ],
                menuLabel=_('Full demo'),
                menuPath=['<Image>/'+_('Test'), '<Layers>'],  # Added to two menus
                descShort=_('Show all widgets for Python dialogs'),
                descLong=_('Show all widgets for Python dialogs, to demonstrate the gimphelpers module'),
                beforeDialog=beforeFullDialog,
                afterDialog=afterFullDialog,
    )]
    domain=getDomain(__file__)
    icon=getIcon(__file__)
    author='Ofnuts'
    year='2024'

Gimp.main(FullDialog.__gtype__, sys.argv)

and for that price your code is wrapped into a some more code that shows the dialog (with optional peek/tweak at args before/after dialog, starts/end an undo group, and runs the code within a try/except block so that the plugin exits nicely in most cases.

The module is here.