Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Converting python plugin-in shellout.py from gimp 2.x to 3.x
#1
Dear Gimp developers,

Background: the shellout.py (7 years old script) is an approach for calling NikCollection Filters and using the result in GIMP. It still worked with win10 & win11 until GIMP 3.x.

I’m having trouble converting the Python plug-in script from GIMP 2.x to 3.x.
Thank @Ofnuts for pointing out the helloworld plugin example, that helped me understand the basics a little bit.

Currently, I’m stuck on adapting the part that opens a popup-menu with RATIO and OPTION (see youtube video) choices within the register() function (v2.x).

Here is the code snippet which can show ShellOut in menu Filters/ but still missing the functionality, I took a look in v3.0 API but got confuse..:
Code:
def plugin_main(procedure, run_mode, image, n_drawables, drawables, config, data):
 ...

class ShellOut(Gimp.PlugIn):

  def do_query_procedures(self):
      return [PROC_NAME]

  def do_create_procedure(self, name):
      procedure = Gimp.ImageProcedure.new(self, name,
                                         Gimp.PDBProcType.PLUGIN,
                                         plugin_main, None)
     
      procedure.set_image_types("RGB*, GRAY*")
      procedure.set_menu_label("ShellOut...")
      procedure.set_attribution(AUTHOR, COPYRIGHT, DATE)
      procedure.set_documentation(HELP, DOC, None)

      procedure.add_menu_path("<Image>/Filters/ShellOut...")

      # FIXME: How to open menu and get return values and pass into main_plugin()?
     
      return procedure

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

Could someone give me the concrete instructions how rewrite the script to make it work again in GIMP 3.x?
Thank you very much for your help!

Best regards,

PS: Attached is the 'in-progress' reworked version for v3.x in comparison to the original 2.x script could be found in the link above.


Attached Files
.txt   shellout_v3.txt (Size: 8 KB / Downloads: 15)
Reply
#2
TLDR:
1. What are the correct replacements for PF_RATIO & PF_OPTION? Can you provide corresponding code with GIMP 3.x procedure?
2. What is the appropriate way to debug python plugins?

------------------------------
1. I tried using add_enum_argument() and add_choice_argument(), but it seems I don’t fully understand the documentation.
2. I cannot properly debug the code:
    - Running "C:\Program Files\gimp3\bin\gimp-console.exe" does not print any useful information when the plugin code is incorrect; it simply does not appear in the menu.
    - Running the shellout.py script in VSCode using its interpreter "C:\Program Files\gimp3\bin\python.exe" is also not helpful. It executes but only outputs: "shellout.py is a GIMP plug-in and must be run by GIMP to be used."

Any help would be appreciated.
Reply
#3
(Yesterday, 04:35 PM)iiey Wrote: TLDR:
1. What are the correct replacements for PF_RATIO & PF_OPTION? Can you provide corresponding code with GIMP 3.x procedure?
2. What is the appropriate way to debug python plugins?
Hello iiey,

1. Here is a test dialog example: https://gitlab.gnome.org/GNOME/gimp/-/bl...og.py#L158
In regards to a choice, here is an example I created:
Code:
#!/usr/bin/env python3
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <https://www.gnu.org/licenses/>.

import gi
gi.require_version('Gimp', '3.0')
from gi.repository import Gimp
gi.require_version('GimpUi', '3.0')
from gi.repository import GimpUi
gi.require_version('Gegl', '0.4')
from gi.repository import Gegl
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gio
import time
import sys

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

'''
A Python plugin.
Tests GimpProcedureDialog.

Temporarily, just testing widgets for subclasses of GimpResource.
Temporarily, just testing Brush subclass of Resource.
FUTURE: For all the parameter types.
Formerly PF_ constants, now all parameters for which GimpParamSpecs exist.

Not localized, no i18n
'''


def process_args(brush, font, gradient, palette, pattern, choice_val):
   '''
   Test the args are sane.
   '''
   assert brush is not None
   assert isinstance(brush, Gimp.Brush)
   id = brush.get_id()
   name = brush.get_name()
   assert id is not None
   msg = "Brush: {} (ID: {})".format(name, id)
   print(msg)
   Gimp.message(msg)

   assert font is not None
   assert isinstance(font, Gimp.Font)
   id = font.get_id()
   name = font.get_name()
   assert id is not None
   msg = "Font: {} (ID: {})".format(name, id)
   print(msg)
   Gimp.message(msg)

   assert gradient is not None
   assert isinstance(gradient, Gimp.Gradient)
   id = gradient.get_id()
   name = gradient.get_name()
   assert id is not None
   msg = "Gradient: {} (ID: {})".format(name, id)
   print(msg)
   Gimp.message(msg)

   assert palette is not None
   assert isinstance(palette, Gimp.Palette)
   id = palette.get_id()
   name = palette.get_name()
   assert id is not None
   msg = "Palette: {} (ID: {})".format(name, id)
   print(msg)
   Gimp.message(msg)

   assert pattern is not None
   assert isinstance(pattern, Gimp.Pattern)
   id = pattern.get_id()
   name = pattern.get_name()
   assert id is not None
   msg = "Pattern: {} (ID: {})".format(name, id)
   print(msg)
   Gimp.message(msg)

   assert choice_val is not None
   assert isinstance(choice_val, str)
   msg = "Choice : {}".format(choice_val)
   print(msg)
   Gimp.message(msg)

   return


def test_dialog(procedure, run_mode, image, drawables, config, data):
   '''
   Just a standard shell for a plugin.
   '''
   
   if run_mode == Gimp.RunMode.INTERACTIVE:
       GimpUi.init('python-fu-test-dialog')
       Gegl.init(None)
       dialog = GimpUi.ProcedureDialog(procedure=procedure, config=config)
       dialog.fill(None)
       if not dialog.run():
           dialog.destroy()
           return procedure.new_return_values(Gimp.PDBStatusType.CANCEL, GLib.Error())
       else:
           dialog.destroy()

   brush = config.get_property('brush')
   font = config.get_property('font')
   gradient = config.get_property('gradient')
   palette = config.get_property('palette')
   pattern = config.get_property('pattern')
   choice_val = config.get_property('format')

   Gimp.context_push()

   process_args(brush, font, gradient, palette, pattern, choice_val)

   Gimp.displays_flush()

   Gimp.context_pop()
   

   return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())


class TestDialogPlugin (Gimp.PlugIn):
   # FUTURE all other Gimp classes that have GimpParamSpecs

   ## GimpPlugIn virtual methods ##
   def do_set_i18n(self, procname):
       return True, 'gimp30-python', None

   def do_query_procedures(self):
       return [ 'python-fu-test-dialog' ]

   def do_create_procedure(self, name):
       procedure = Gimp.ImageProcedure.new(self, name,
                                           Gimp.PDBProcType.PLUGIN,
                                           test_dialog, None)
       procedure.set_sensitivity_mask (Gimp.ProcedureSensitivityMask.NO_IMAGE)
       procedure.set_documentation ("Test dialog",
                                    "Test dialog",
                                    name)
       procedure.set_menu_label("Test dialog...")
       procedure.set_attribution("Lloyd Konneker",
                                 "Lloyd Konneker",
                                 "2022")
       # Top level menu "Test"
       procedure.add_menu_path ("<Image>/Filters/Development/Demos")

       procedure.add_brush_argument ("brush", "_Brush", "Brush", True,
                                     Gimp.Brush.get_by_name ("2. Hardness 025"),
                                     False,
                                     GObject.ParamFlags.READWRITE)
       procedure.add_font_argument ("font", "_Font", "Font", True,
                                    Gimp.Font.get_by_name ("Serif"),
                                    False,
                                    GObject.ParamFlags.READWRITE)
       procedure.add_gradient_argument ("gradient", "_Gradient",
                                        "Gradient", True,
                                        Gimp.Gradient.get_by_name ("Incandescent"),
                                        False,
                                        GObject.ParamFlags.READWRITE)
       procedure.add_palette_argument ("palette", "_Palette",
                                       "Palette", True,
                                       Gimp.Palette.get_by_name ("Default"),
                                       False,
                                       GObject.ParamFlags.READWRITE)
       procedure.add_pattern_argument ("pattern", "Pa_ttern",
                                       "Pattern", True,
                                       Gimp.Pattern.get_by_name ("Paper"),
                                       False,
                                       GObject.ParamFlags.READWRITE)

       export_format_choice = Gimp.Choice.new()
       export_format_choice.add("png", 1, "png", "png")
       export_format_choice.add("jpg", 2, "jpg", "jpg")
       procedure.add_choice_argument("format", "_Format",
                                   "Format", export_format_choice, "png",
                                   GObject.ParamFlags.READWRITE)

       return procedure

Gimp.main(TestDialogPlugin.__gtype__, sys.argv)
I'm not sure about PF_RATIO though.

2. In regards to debugging, I scatter Gimp.message statements and try/except to catch bugs. If someone has an alternative strategy, I'm open to it!
Reply
#4
Hi nchen,

Thank you for your reply, I will take your example with choice as reference and test it out.
Reply
#5
(Yesterday, 04:50 PM)iiey Wrote: Hi nchen,

Thank you for your reply, I will take your example with choice as reference and test it out.

Hello iiey,

You're welcome! Hope that helps.
Were you trying to get Radio options working?
You can theoretically use any GTK 3 widgets with the GimpUI dialog.
I just learned GTK recently because I didn't know how to use GimpUI...

Here is an example I just created with Radio option:

Code:
#!/usr/bin/env python3
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <https://www.gnu.org/licenses/>.

import gi
gi.require_version('Gimp', '3.0')
from gi.repository import Gimp
gi.require_version('GimpUi', '3.0')
from gi.repository import GimpUi
gi.require_version('Gegl', '0.4')
from gi.repository import Gegl
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gtk
from gi.repository import Gdk
import time
import sys

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

'''
A Python plugin.
Tests GimpProcedureDialog.

Temporarily, just testing widgets for subclasses of GimpResource.
Temporarily, just testing Brush subclass of Resource.
FUTURE: For all the parameter types.
Formerly PF_ constants, now all parameters for which GimpParamSpecs exist.

Not localized, no i18n
'''


def process_args(brush, font, gradient, palette, pattern, choice_val):
   '''
   Test the args are sane.
   '''
   assert brush is not None
   assert isinstance(brush, Gimp.Brush)
   id = brush.get_id()
   name = brush.get_name()
   assert id is not None
   msg = "Brush: {} (ID: {})".format(name, id)
   print(msg)
   Gimp.message(msg)

   assert font is not None
   assert isinstance(font, Gimp.Font)
   id = font.get_id()
   name = font.get_name()
   assert id is not None
   msg = "Font: {} (ID: {})".format(name, id)
   print(msg)
   Gimp.message(msg)

   assert gradient is not None
   assert isinstance(gradient, Gimp.Gradient)
   id = gradient.get_id()
   name = gradient.get_name()
   assert id is not None
   msg = "Gradient: {} (ID: {})".format(name, id)
   print(msg)
   Gimp.message(msg)

   assert palette is not None
   assert isinstance(palette, Gimp.Palette)
   id = palette.get_id()
   name = palette.get_name()
   assert id is not None
   msg = "Palette: {} (ID: {})".format(name, id)
   print(msg)
   Gimp.message(msg)

   assert pattern is not None
   assert isinstance(pattern, Gimp.Pattern)
   id = pattern.get_id()
   name = pattern.get_name()
   assert id is not None
   msg = "Pattern: {} (ID: {})".format(name, id)
   print(msg)
   Gimp.message(msg)

   assert choice_val is not None
   assert isinstance(choice_val, str)
   msg = "Choice : {}".format(choice_val)
   print(msg)
   Gimp.message(msg)

   return

class CustomProcedureDialog(GimpUi.ProcedureDialog):
   def __init__(self, procedure, config):
       super().__init__(procedure=procedure, config=config)
       self.set_default_size(600, 100)

       # Apply CSS
       self.apply_css()

       # Get the main content area of the ProcedureDialog
       content_area = self.get_content_area()

       # Create a scrolled window for the entire dialog
       scrolled_window = Gtk.ScrolledWindow()
       scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
       scrolled_window.set_size_request(400, 300)  # Adjust scrolled window size request dims

       # Create a container to hold all the UI elements inside the scroll area
       content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
       content_box.set_border_width(10)
       content_box.get_style_context().add_class("content_box")

       # Populate GIMP's default argument UI elements,
       # This adds the GIMP argument widgets to the content area
       self.fill(None)

       # Retrieve all GIMP UI elements that were just added
       for child in content_area.get_children():
           # Skip the GtkBox and GtkButtonBox elements (ok buttons at the bottom to add them later)
           if child.get_name() == "GtkBox" or child.get_name() == "GtkButtonBox":
               continue
           content_area.remove(child)  # Remove them temporarily
           content_box.pack_start(child, False, False, 5)  # Repack them inside the scrollable box

       # Create custom Radio options
       export_box = Gtk.Box(spacing=10)
       export_box.get_style_context().add_class("export_box")
       export_label = Gtk.Label(label="Export As")
       export_label.get_style_context().add_class("export_label")
       self.png_radio = Gtk.RadioButton.new_with_label(None, "PNG")
       self.jpg_radio = Gtk.RadioButton.new_with_label_from_widget(self.png_radio, "JPG")

       # Add the radio to the content box
       export_box.pack_start(export_label, False, False, 0)
       export_box.pack_start(self.png_radio, False, False, 0)
       export_box.pack_start(self.jpg_radio, False, False, 0)
       content_box.pack_start(export_box, False, False, 0)

       # Add the content box to the scrolled window
       scrolled_window.add(content_box)

       # Add the scrolled window into the GimpUi dialog
       content_area.pack_start(scrolled_window, True, True, 0)

       self.show_all()

   def apply_css(self):
       css = """
           .content_box {
               padding-left: 20px;
               padding-right: 20px;
           }
           
       """
       style_provider = Gtk.CssProvider()
       style_provider.load_from_data(css.encode("utf-8"))
       Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)

   def get_radio_result(self):
       """Store the radio button selection before closing."""
       if self.png_radio.get_active():
           self.selected_format = "png"
       elif self.jpg_radio.get_active():
           self.selected_format = "jpg"


def test_dialog(procedure, run_mode, image, drawables, config, data):
   '''
   Just a standard shell for a plugin.
   '''
   
   if run_mode == Gimp.RunMode.INTERACTIVE:
       GimpUi.init('python-fu-test-dialog')
       Gegl.init(None)
       dialog = CustomProcedureDialog(procedure, config)
       response = dialog.run()

       dialog.get_radio_result()
       selected_format = dialog.selected_format
       Gimp.message(f"Selected Format: {selected_format}")
       dialog.destroy()

   brush = config.get_property('brush')
   font = config.get_property('font')
   gradient = config.get_property('gradient')
   palette = config.get_property('palette')
   pattern = config.get_property('pattern')
   choice_val = config.get_property('format')

   Gimp.context_push()

   # process_args(brush, font, gradient, palette, pattern, choice_val)

   Gimp.displays_flush()

   Gimp.context_pop()
   

   return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())


class TestDialogPlugin (Gimp.PlugIn):
   # FUTURE all other Gimp classes that have GimpParamSpecs

   ## GimpPlugIn virtual methods ##
   def do_set_i18n(self, procname):
       return True, 'gimp30-python', None

   def do_query_procedures(self):
       return [ 'python-fu-test-dialog' ]

   def do_create_procedure(self, name):
       procedure = Gimp.ImageProcedure.new(self, name,
                                           Gimp.PDBProcType.PLUGIN,
                                           test_dialog, None)
       procedure.set_sensitivity_mask (Gimp.ProcedureSensitivityMask.NO_IMAGE)
       procedure.set_documentation ("Test dialog",
                                    "Test dialog",
                                    name)
       procedure.set_menu_label("Test dialog...")
       procedure.set_attribution("Lloyd Konneker",
                                 "Lloyd Konneker",
                                 "2022")
       # Top level menu "Test"
       procedure.add_menu_path ("<Image>/Filters/Development/Demos")

       procedure.add_brush_argument ("brush", "_Brush", "Brush", True,
                                     Gimp.Brush.get_by_name ("2. Hardness 025"),
                                     False,
                                     GObject.ParamFlags.READWRITE)
       procedure.add_font_argument ("font", "_Font", "Font", True,
                                    Gimp.Font.get_by_name ("Serif"),
                                    False,
                                    GObject.ParamFlags.READWRITE)
       procedure.add_gradient_argument ("gradient", "_Gradient",
                                        "Gradient", True,
                                        Gimp.Gradient.get_by_name ("Incandescent"),
                                        False,
                                        GObject.ParamFlags.READWRITE)
       procedure.add_palette_argument ("palette", "_Palette",
                                       "Palette", True,
                                       Gimp.Palette.get_by_name ("Default"),
                                       False,
                                       GObject.ParamFlags.READWRITE)
       procedure.add_pattern_argument ("pattern", "Pa_ttern",
                                       "Pattern", True,
                                       Gimp.Pattern.get_by_name ("Paper"),
                                       False,
                                       GObject.ParamFlags.READWRITE)

       export_format_choice = Gimp.Choice.new()
       export_format_choice.add("png", 1, "png", "png")
       export_format_choice.add("jpg", 2, "jpg", "jpg")
       procedure.add_choice_argument("format", "_Format",
                                   "Format", export_format_choice, "png",
                                   GObject.ParamFlags.READWRITE)

       return procedure

Gimp.main(TestDialogPlugin.__gtype__, sys.argv)
Reply


Forum Jump: