Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Basic Tutorial Example
#1
I don't know if this will help any other beginners on Gimp 3 Python scripting.

Taking most of a day, I made a simple example that works, to my amazement.  I have Gimp 3.0.4 on Ubuntu 22.04, and this runs in GIMP from Filters -> Development -> Python-Fu -> Python Console.

It draws a red box on a blue background.

Code:
import gi
gi.require_version('Gimp', '3.0')
from gi.repository import Gimp

Gimp.context_set_foreground(Gegl.Color.new("red"))
Gimp.context_set_background(Gegl.Color.new("blue"))
image = Gimp.Image.new(300, 400, Gimp.ImageBaseType.RGB)
layer = Gimp.Layer.new(image, "layer 1", 300, 400, Gimp.ImageType.RGBA_IMAGE, 100, Gimp.LayerMode.NORMAL)
image.insert_layer(layer, None, 0)
layer.edit_fill(Gimp.FillType.BACKGROUND)
Gimp.Display.new(image)
Gimp.Image.select_rectangle(image, Gimp.ChannelOps.REPLACE, 60.0, 80.0, 100.0, 150.0)
layer.edit_fill(Gimp.FillType.FOREGROUND)
Gimp.Selection.none(image)
Gimp.displays_flush()

I learned that you can also set colors in this format:

Code:
Gimp.context_set_foreground(Gegl.Color.new("#B00000"))

I found the documentation terribly sparse.  For example, in https://developer.gimp.org/api/3.0/libgimp,

at https://developer.gimp.org/api/3.0/libgi...angle.html, which contains:

Code:
gboolean
gimp_image_select_rectangle (
  GimpImage* image,
  GimpChannelOps operation,
  gdouble x,
  gdouble y,
  gdouble width,
  gdouble height
)

My script has the line: 

Code:
Gimp.Image.select_rectangle(image, Gimp.ChannelOps.REPLACE, 60.0, 80.0, 100.0, 150.0)

How I got from "gimp_image_select_rectangle" to "Gimp.Image.select_rectangle" wasn't clear, and when I looked up the constants for Gimp.ChannelOps.REPLACE,

at https://developer.gimp.org/api/3.0/libgi...elOps.html

it shows GIMP_CHANNEL_OP_REPLACE

from which it wasn't clear to me that this could be Gimp.ChannelOps.REPLACE.

I would be happy to help improve the documentation, which I found sparse and confusing.

I would be grateful if anyone could tell me how to change the dark blue font in the GIMP python console (without changing the Dark Colors scheme), because I couldn't read it at all unless I highlighted it so it showed as reverse video. I couldn't figure out where, maybe in what css file, this is defined.  I'm using gnome and X11.

To do this, it was a lot of hit and miss, and searching for other people's scripts and trying to find similar code in theirs.  Also, I don't really understand how to convert from pdb to non-pdb function calls and back.

I guess my next step is to convert this to a plug-in (.py) file.
Reply
#2
(10-05-2025, 01:02 AM)elindarie Wrote: I don't know if this will help any other beginners on Gimp 3 Python scripting.
....snip...
To do this, it was a lot of hit and miss, and searching for other people's scripts and trying to find similar code in theirs.  Also, I don't really understand how to convert from pdb to non-pdb function calls and back.

I guess my next step is to convert this to a plug-in (.py) file.

I am in the same boat, looked at all the available advice, looked at numerous python plugins, left as totally baffled. I hate to say it but script-fu is a little (not much) easier.

It will come with time, there will be documentation that is above "hello world" in the python console but more readable than the present situation.

Anyway, as a dyed-in-the-wool dabbler and for my linear-code requirements I use a simple-ish shell. Your code applies like this, I cannot be bothered to change the registration Wink  The entry point where it goes is after #Create Filter (but filters are a bit of a mystery as well)   FWIW plugin attached.

   


Attached Files
.zip   for-gegl.zip (Size: 1.2 KB / Downloads: 51)
Reply
#3
Thank you, Rich!
Reply
#4
I have to thank you for your example of code. It has helped me.
Reply
#5
That was a great help.

It's now 1/2026, and there is still a serious lack of help and tutorials for gimp 3 plugins. I guess this is partly because of the transition and major changes from gimp 2 to gimp 3. There's not much knowledge about - though please prove me wrong, and those who have it are probably busy on other things.

I'm pretty new to gimp - used it for tweaking a few images in the past and that's it, and whilst I have a lot of s/w design experience, absolutely none of it was python which I looked at for the first time last week. I did, a long time ago, write some VB automation stuff for driving Photoshop, but the similarity is zero.

There is documentation in the form of auto generated stuff, but, whilst that is useful in its place, it just describes the bits, not how the bits hang together as a system. For a beginner on this like me, that overall picture of architecture and structure is what I don't have.

I don't think it's reasonable to expect gimp developers to down tools and write loads of documents - they won't, anyway - so as far as I can see the only way to shed more light is for people like us to publish bits of code here and there, asking questions and for comments. Gradually knowledge may increase.

Cannot contribute much myself at the moment - I haven't got anything to work so far. I do have a shell plugin that registers, runs and pops up a dialog, but that's it - doesn't do anything else yet. It's supposed to load images one by one, make some changes, and write them out elsewhere as JPEGs - if only I can figure out how to open an image file, display it and write it out and discard it again. I can add that if it helps.
Reply
#6
(10-05-2025, 01:02 AM)elindarie Wrote:
Code:
Gimp.Image.select_rectangle(image, Gimp.ChannelOps.REPLACE, 60.0, 80.0, 100.0, 150.0)

How I got from "gimp_image_select_rectangle" to "Gimp.Image.select_rectangle" wasn't clear, and when I looked up the constants for Gimp.ChannelOps.REPLACE,

at https://developer.gimp.org/api/3.0/libgi...elOps.html

it shows GIMP_CHANNEL_OP_REPLACE

from which it wasn't clear to me that this could be Gimp.ChannelOps.REPLACE.

I would be happy to help improve the documentation, which I found sparse and confusing.

I would be grateful if anyone could tell me how to change the dark blue font in the GIMP python console (without changing the Dark Colors scheme), because I couldn't read it at all unless I highlighted it so it showed as reverse video. I couldn't figure out where, maybe in what css file, this is defined.  I'm using gnome and X11.

To do this, it was a lot of hit and miss, and searching for other people's scripts and trying to find similar code in theirs.  Also, I don't really understand how to convert from pdb to non-pdb function calls and back.

I guess my next step is to convert this to a plug-in (.py) file.

You're looking at the documentation for C, so it's normal that it's not obvious.

The same doc for Python is here:
https://lazka.github.io/pgi-docs/#Gimp-3..._rectangle
and the detail for the enum is there:
https://lazka.github.io/pgi-docs/Gimp-3....ChannelOps

One has still to figure out that Gimp.ChannelOps(value) can be written Gimp.ChannelOps.REPLACE, which I recognize is not obvious. Took me a few tries.
I guess (didn't try) the short form would be Gimp.ChannelOps(2), but it's less explicit.

If you want to go one step further, I'd suggest you, as an exercise, to try to select the current image instead of creating one. ;-)
Reply
#7
In the interests of passing knowledge around on writing plugins, particularly for Gimp 3, below is one (the first one) that I've just created. It's pretty simple - is given a source and dest directory, parses the src, and for each image creates a scaled down image and a thumbnail suitable for web display. I used to do this with VB automation and Photoshop, and it is specifically for the way I save photographs in a database. However, I found getting anyuthin g to work from scratch damn hard work, so if this helps anyone on the basics of writing a plugin, so much the better. Equally if anyone wants to comment, fine.

It has two problems at the moment:

1. paths returned from the plugin dialog are GFiles, and I could find no good way of turning them into a string path that I can manipulate. In the end I wrote a bodgey gfile_to_fullpath() that strips off the scheme for Windows and Linux.
2. the images are displayed as they are scaled, but not correctly. The final output files are fine, however. This didn't happen when testing it line by line in the python console - ie slowly, so I suspect it is something like the display and image manipulation are asynchronous, and the display doesn't keep up. But, don't really know.

Anyway, here it is for anyone interested, it seems to work in Ubuntu 24.04 and Windows 11, both on GIMP 3.0.6:

Code:
#!/usr/bin/env python3
#coding: utf-8

###################################
###################################
####    imports

import csv
import math
import sys
import os
import string

import gi

gi.require_version('Gimp', '3.0')
from gi.repository import Gimp
gi.require_version('GimpUi', '3.0')
from gi.repository import GimpUi
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gio
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk

###################################
###################################
####    constants

IMAGE_EXTENSIONS = (".jpg", ".jpeg", ".tif", ".tiff", ".psd", ".xcl")
JPEG_LEVELS = {"low-q": 0.3, "medium-q": 0.6, "high-q": 0.9, "max-q": 1.0}

DESCRIPTION = """This reads images from a source directory and
writes them out again to a destination directory.

For each source image, a destination image is created 'nn.jpg' which is
scaled down if necessary, maintaining aspect ratio, to have a width or
height no more than a maximum set side, then a second (thumbnail) is
created named t_nn.jpg scaled if necessary to be no larger than a maximum
thumbnail side. nn is a 1 based index incrememted as it reads the images.

The destination folder is wiped of .jpg images before it starts."""

###################################
###################################
####    helper functions

def purge_dest_folder(dest_folder):
    files_purged = 0
    with os.scandir(dest_folder) as iter:
        for entry in iter:
            if entry.is_file():
                entry_extn = os.path.splitext(entry.name)[1].lower();
                if entry_extn.lower() == ".jpg":
                    files_purged += 1
                    os.remove(entry.path)
    
    return files_purged

###################################

def check_extension(inpath):
    for e in IMAGE_EXTENSIONS:
        if inpath.lower().endswith(e):
            return True
        
    return False

###################################

def scale_image(img, max_allowed_side_px):
    width = img.get_width()
    height = img.get_height()
    
    max_side = width if width >= height else height
    
    scale = 1.0
    scaled = False
    if max_side > max_allowed_side_px:
        scale = float(max_allowed_side_px) / float(max_side)
        width = int(width * scale)
        height = int(height * scale)
        img.scale(width, height)
        scaled = True
    
    return scaled

###################################

def convert_to_jpeg(input_filepath, output_folder, file_index, max_allowed_image_side_px, max_allowed_thumbnail_side_px, jpeg_level):
    # load image, display and flatten layers
    
    infile = Gio.File.new_for_path(input_filepath)    #    creates a GFile that can be used in load
    img = Gimp.file_load(Gimp.RunMode.NONINTERACTIVE, infile)
    disp = Gimp.Display.new(img)
    img.flatten()
    
    ###    do any rotation here
    
    # prepare save procedure
    
    procedure = Gimp.get_pdb().lookup_procedure('file-jpeg-export')
    config = procedure.create_config()
    config.set_property('run-mode', Gimp.RunMode.NONINTERACTIVE)
    config.set_property('image', img)
    config.set_property('options', None)
    # rest of parameters left default, except maybe jpeg quality, see below
    
    # scale image
    
    scale_image(img, max_allowed_image_side_px)
    
    # save the image
    
    output_filepath = os.path.join(output_folder, str(file_index) + ".jpg")
    outfile = Gio.File.new_for_path(output_filepath)
    config.set_property('file', outfile)
    config.set_property('quality', JPEG_LEVELS[jpeg_level]) # set desired jpeg quality
    
    result = procedure.run(config)
    
    # scale thumbnail
    
    scale_image(img, max_allowed_thumbnail_side_px)
    
    # save the thumbnail
    
    output_filepath = os.path.join(output_folder, "t_" + str(file_index) + ".jpg")
    outfile = Gio.File.new_for_path(output_filepath)
    config.set_property('file', outfile)
    config.set_property('quality', JPEG_LEVELS["max-q"]) # set jpeg quality to max for thumbnail
    result = procedure.run(config)
    
    # tidy up and delete
    
    img.clean_all()
    disp.delete()

###################################

def parse_images(src_folder, dest_folder, max_allowed_image_side_px, max_allowed_thumbnail_side_px, jpeg_level):
    file_index = 0
    with os.scandir(src_folder) as iter:
        for entry in iter:
            if entry.is_file():
                entry_extn = os.path.splitext(entry.name)[1].lower();
                for extn in IMAGE_EXTENSIONS:
                    if extn == entry_extn:
                        file_index += 1
                        convert_to_jpeg(entry.path, dest_folder, file_index, max_allowed_image_side_px, max_allowed_thumbnail_side_px, jpeg_level)
                        break
    
    return file_index

###################################

def gfile_to_fullpath(gf):
    uri = gf.get_uri()
    full_path = uri
    length_uri = len(uri)
    file_scheme = "file:"
    length_file_scheme = len(file_scheme)
    index_scheme_start = uri.lower().find(file_scheme)
    if index_scheme_start == 0:    # found scheme at start of uri
        index = length_file_scheme
        if length_uri > length_file_scheme:    # more characters
            if uri[index] == "\\":    # windows style
                while index < length_uri and uri[index] == "\\":    # skip
                    index += 1

                full_path = uri[index:]    # rest of string

            elif uri[index] == "/":    # unix style
                while index < length_uri and uri[index] == "/":    # skip
                    index += 1

                index -= 1    # need the last '/'
                full_path = uri[index:]    # rest of string

            else:    # goodness knows
                pass

        else:    # nothing after the scheme, goodness knows
            pass

    elif index_scheme_start > 0:    # found scheme but not at start, goodness knows
        pass

    else:    # didn't find scheme
        pass

    return full_path

###################################
###################################
####    run procedure that gets called to run the plugin

def run(procedure, run_mode, image, layers, config, data):
    if run_mode == Gimp.RunMode.INTERACTIVE:
        GimpUi.init("python-fu-nmw-batch-save")

        dialog = GimpUi.ProcedureDialog.new(procedure, config, "Batch save")

        radio_frame = dialog.get_widget("JPEG level", GimpUi.IntRadioFrame)

        dialog.fill(None)

        if not dialog.run():
            return procedure.new_return_values(Gimp.PDBStatusType.CANCEL, GLib.Error())
        

    src_gfile = config.get_property("src-folder")
    src_folder = gfile_to_fullpath(src_gfile)
    dest_gfile = config.get_property("dest-folder")
    dest_folder = gfile_to_fullpath(dest_gfile)
    max_thumbnail_px = int(config.get_property("max-thumbnail-px"))
    max_image_px = int(config.get_property("max-image-px"))
    jpeg_level = str(config.get_property("jpeg-level"))

    if src_folder != "" and dest_folder != "" and src_folder != dest_folder:
        purge_dest_folder(dest_folder)
        parse_images(src_folder, dest_folder, max_image_px, max_thumbnail_px, jpeg_level)
    
    else:
        Gimp.message("src=" + src_folder)
        Gimp.message("dest=" + dest_folder)
        return procedure.new_return_values(Gimp.PDBStatusType.EXECUTION_ERROR, GLib.Error("src and/or dest folders incorrect"))

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

###################################
###################################
####    derived class for the plugin

class NmwBatchSave(Gimp.PlugIn):
    ####    GimpPlugIn virtual methods
    
    def do_set_i18n(self, procname):
        return False

    def do_query_procedures(self):
        return ['python-fu-nmw-batch-save']

    def do_create_procedure(self, name):
        procedure = None
        if name == 'python-fu-nmw-batch-save':
            procedure = Gimp.ImageProcedure.new(
                self,
                name,
                Gimp.PDBProcType.PLUGIN,
                run,
                None)

            procedure.set_image_types("*")
            procedure.set_documentation(
                "Creates images/thumbnails for stillimages database",
                DESCRIPTION,
                name)
            procedure.set_menu_label("Batch save")
            procedure.set_attribution("Nick W",
                                      "(c) GPL V3.0 or later",
                                      "2026")
            procedure.add_menu_path("<Image>/Tools/Automation/")
            procedure.set_sensitivity_mask(Gimp.ProcedureSensitivityMask.NO_IMAGE)

            # dialog box items

            procedure.add_file_argument("src-folder",
                "Source folder",
                "Location of files to parse",
                Gimp.FileChooserAction.CREATE_FOLDER,
                False,
                None,
                GObject.ParamFlags.READWRITE)
            procedure.add_file_argument("dest-folder",
                "Destination folder",
                "Location to save files/thumbnails",
                Gimp.FileChooserAction.CREATE_FOLDER,
                False,
                None,
                GObject.ParamFlags.READWRITE)
            procedure.add_int_argument(
                "max-thumbnail-px",
                "Maximum thumbnail size",
                "Maximum height or width of thumbnail (px)",
                50,
                150,
                70,
                GObject.ParamFlags.READWRITE)
            procedure.add_int_argument(
                "max-image-px",
                "Maximum image size",
                "Maximum height or width of image (px)",
                300,
                1000,
                600,
                GObject.ParamFlags.READWRITE)
            jpeg_level_choice = Gimp.Choice.new()
            jpeg_level_choice.add("low-q", 3, "Low", "Small size / low quality file")
            jpeg_level_choice.add("medium-q", 6, "Medium", "Medium size / quality file")
            jpeg_level_choice.add("high-q", 9, "High", "High quality / large size file")
            procedure.add_choice_argument(
                "jpeg-level",
                "JPEG level",
                "Quality of JPEG",
                jpeg_level_choice,
                "medium-q",
                GObject.ParamFlags.READWRITE)

        return procedure

###################################
###################################
####    main

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


Forum Jump: