01-22-2026, 02:52 PM
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:
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)
